From 0aadc17d28be5a2cfab92a8e963ae8f7ddbcdd1e Mon Sep 17 00:00:00 2001 From: Steve Streeting Date: Thu, 21 May 2020 13:23:25 +0100 Subject: [PATCH] Implement unlock-on-push, if changes had been committed without pushing before --- .../Private/GitSourceControlOperations.cpp | 74 ++++++++++++++++++- .../Private/GitSourceControlOperations.h | 4 + .../Private/GitSourceControlUtils.cpp | 45 ++++++++--- .../Private/GitSourceControlUtils.h | 20 +++++ 4 files changed, 130 insertions(+), 13 deletions(-) diff --git a/Source/GitSourceControl/Private/GitSourceControlOperations.cpp b/Source/GitSourceControl/Private/GitSourceControlOperations.cpp index 6b6a9f3c..71b5dcf0 100644 --- a/Source/GitSourceControl/Private/GitSourceControlOperations.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlOperations.cpp @@ -472,6 +472,56 @@ FName FGitPushWorker::GetName() const bool FGitPushWorker::Execute(FGitSourceControlCommand& InCommand) { + + // If we have any locked files, check if we should unlock them + TArray FilesToUnlock; + if (InCommand.bUsingGitLfsLocking) + { + TMap Locks; + // Get locks as relative paths + GitSourceControlUtils::GetAllLocks(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false, InCommand.ErrorMessages, Locks); + if(Locks.Num() > 0) + { + // test to see what lfs files we would push, and compare to locked files, unlock after if push OK + FString BranchName; + GitSourceControlUtils::GetBranchName(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, BranchName); + + TArray LfsPushParameters; + LfsPushParameters.Add(TEXT("push")); + LfsPushParameters.Add(TEXT("--dry-run")); + LfsPushParameters.Add(TEXT("origin")); + LfsPushParameters.Add(BranchName); + TArray LfsPushInfoMessages; + TArray LfsPushErrMessages; + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("lfs"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, LfsPushParameters, TArray(), LfsPushInfoMessages, LfsPushErrMessages); + + if(InCommand.bCommandSuccessful) + { + // Result format is of the form + // push f4ee401c063058a78842bb3ed98088e983c32aa447f346db54fa76f844a7e85e => Path/To/Asset.uasset + // With some potential informationals we can ignore + for (auto& Line : LfsPushInfoMessages) + { + if (Line.StartsWith(TEXT("push"))) + { + FString Prefix, Filename; + if (Line.Split(TEXT("=>"), &Prefix, &Filename)) + { + Filename = Filename.TrimStartAndEnd(); + if (Locks.Contains(Filename)) + { + // We do not need to check user or if the file has local modifications before attempting unlocking, git-lfs will reject the unlock if so + // No point duplicating effort here + FilesToUnlock.Add(Filename); + UE_LOG(LogSourceControl, Log, TEXT("Post-push will try to unlock: %s"), *Filename); + } + } + } + } + } + } + + } // push the branch to its default remote // (works only if the default remote "origin" is set and does not require authentication) TArray Parameters; @@ -480,14 +530,34 @@ bool FGitPushWorker::Execute(FGitSourceControlCommand& InCommand) Parameters.Add(TEXT("HEAD")); InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages); - // NOTE: no need to update status of our files + if(InCommand.bCommandSuccessful && InCommand.bUsingGitLfsLocking && FilesToUnlock.Num() > 0) + { + // unlock files: execute the LFS command on relative filenames + for(const auto& FileToUnlock : FilesToUnlock) + { + TArray OneFile; + OneFile.Add(FileToUnlock); + bool bUnlocked = GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages); + if (!bUnlocked) + { + // Report but don't fail, it's not essential + UE_LOG(LogSourceControl, Log, TEXT("Unlock failed for %s"), *FileToUnlock); + } + } + + // We need to update status if we unlock + // This command needs absolute filenames + TArray AbsFilesToUnlock = GitSourceControlUtils::AbsoluteFilenames(FilesToUnlock, InCommand.PathToRepositoryRoot); + GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, AbsFilesToUnlock, InCommand.ErrorMessages, States); + + } return InCommand.bCommandSuccessful; } bool FGitPushWorker::UpdateStates() const { - return false; + return GitSourceControlUtils::UpdateCachedStates(States); } FName FGitUpdateStatusWorker::GetName() const diff --git a/Source/GitSourceControl/Private/GitSourceControlOperations.h b/Source/GitSourceControl/Private/GitSourceControlOperations.h index b6612060..16151fc5 100644 --- a/Source/GitSourceControl/Private/GitSourceControlOperations.h +++ b/Source/GitSourceControl/Private/GitSourceControlOperations.h @@ -138,6 +138,10 @@ class FGitPushWorker : public IGitSourceControlWorker virtual FName GetName() const override; virtual bool Execute(class FGitSourceControlCommand& InCommand) override; virtual bool UpdateStates() const override; + +public: + /** Temporary states for results */ + TArray States; }; /** Get source control status of files on local working copy. */ diff --git a/Source/GitSourceControl/Private/GitSourceControlUtils.cpp b/Source/GitSourceControl/Private/GitSourceControlUtils.cpp index 61f7b8f9..509a514d 100644 --- a/Source/GitSourceControl/Private/GitSourceControlUtils.cpp +++ b/Source/GitSourceControl/Private/GitSourceControlUtils.cpp @@ -646,7 +646,7 @@ Content\ThirdPersonBP\Blueprints\ThirdPersonGameMode.uasset SRombauts class FGitLfsLocksParser { public: - FGitLfsLocksParser(const FString& InRepositoryRoot, const FString& InStatus) + FGitLfsLocksParser(const FString& InRepositoryRoot, const FString& InStatus, const bool bAbsolutePaths = true) { TArray Informations; InStatus.ParseIntoArray(Informations, TEXT("\t"), true); @@ -654,7 +654,10 @@ class FGitLfsLocksParser { Informations[0].TrimEndInline(); // Trim whitespace from the end of the filename Informations[1].TrimEndInline(); // Trim whitespace from the end of the username - LocalFilename = FPaths::ConvertRelativePathToFull(InRepositoryRoot, Informations[0]); + if (bAbsolutePaths) + LocalFilename = FPaths::ConvertRelativePathToFull(InRepositoryRoot, Informations[0]); + else + LocalFilename = Informations[0]; LockUser = MoveTemp(Informations[1]); } } @@ -995,6 +998,22 @@ static void ParseStatusResults(const FString& InPathToGitBinary, const FString& } } +bool GetAllLocks(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool bAbsolutePaths, TArray& OutErrorMessages, TMap& OutLocks) +{ + TArray Results; + TArray ErrorMessages; + const bool bResult = RunCommand(TEXT("lfs locks"), InPathToGitBinary, InRepositoryRoot, TArray(), TArray(), Results, ErrorMessages); + for(const FString& Result : Results) + { + FGitLfsLocksParser LockFile(InRepositoryRoot, Result, bAbsolutePaths); + // TODO LFS Debug log + UE_LOG(LogSourceControl, Log, TEXT("LockedFile(%s, %s)"), *LockFile.LocalFilename, *LockFile.LockUser); + OutLocks.Add(MoveTemp(LockFile.LocalFilename), MoveTemp(LockFile.LockUser)); + } + + return bResult; +} + // Run a batch of Git "status" command to update status of given files and/or directories. bool RunUpdateStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, TArray& OutErrorMessages, TArray& OutStates) { @@ -1004,16 +1023,8 @@ bool RunUpdateStatus(const FString& InPathToGitBinary, const FString& InReposito // 0) Issue a "git lfs locks" command at the root of the repository if(InUsingLfsLocking) { - TArray Results; TArray ErrorMessages; - bool bResult = RunCommand(TEXT("lfs locks"), InPathToGitBinary, InRepositoryRoot, TArray(), TArray(), Results, ErrorMessages); - for(const FString& Result : Results) - { - FGitLfsLocksParser LockFile(InRepositoryRoot, Result); - // TODO LFS Debug log - UE_LOG(LogSourceControl, Log, TEXT("LockedFile(%s, %s)"), *LockFile.LocalFilename, *LockFile.LockUser); - LockedFiles.Add(MoveTemp(LockFile.LocalFilename), MoveTemp(LockFile.LockUser)); - } + GetAllLocks(InPathToGitBinary, InRepositoryRoot, true, ErrorMessages, LockedFiles); } // Git status does not show any "untracked files" when called with files from different subdirectories! (issue #3) @@ -1444,6 +1455,18 @@ TArray RelativeFilenames(const TArray& InFileNames, const FStr return RelativeFiles; } +TArray AbsoluteFilenames(const TArray& InFileNames, const FString& InRelativeTo) +{ + TArray AbsFiles; + + for(FString FileName : InFileNames) // string copy to be able to convert it inplace + { + AbsFiles.Add(FPaths::Combine(InRelativeTo, FileName)); + } + + return AbsFiles; +} + bool UpdateCachedStates(const TArray& InStates) { FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked( "GitSourceControl" ); diff --git a/Source/GitSourceControl/Private/GitSourceControlUtils.h b/Source/GitSourceControl/Private/GitSourceControlUtils.h index b39f6855..6c02d858 100644 --- a/Source/GitSourceControl/Private/GitSourceControlUtils.h +++ b/Source/GitSourceControl/Private/GitSourceControlUtils.h @@ -185,6 +185,14 @@ bool RunGetHistory(const FString& InPathToGitBinary, const FString& InRepository */ TArray RelativeFilenames(const TArray& InFileNames, const FString& InRelativeTo); +/** + * Helper function to convert a filename array to absolute paths. + * @param InFileNames The filename array (relative paths) + * @param InRelativeTo Path to the WorkspaceRoot + * @return an array of filenames, transformed into absolute paths + */ +TArray AbsoluteFilenames(const TArray& InFileNames, const FString& InRelativeTo); + /** * Helper function for various commands to update cached states. * @returns true if any states were updated @@ -197,4 +205,16 @@ bool UpdateCachedStates(const TArray& InStates); */ void RemoveRedundantErrors(FGitSourceControlCommand& InCommand, const FString& InFilter); +/** + * Run 'git lfs locks" to extract all lock information for all files in the repository + * + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param bAbsolutePaths Whether to report absolute filenames, false for repo-relative + * @param OutErrorMessages Any errors (from StdErr) as an array per-line + * @param OutLocks The lock results (file, username) + * @returns true if the command succeeded and returned no errors + */ +bool GetAllLocks(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool bAbsolutePaths, TArray& OutErrorMessages, TMap& OutLocks); + }