diff --git a/common/git/git_repo_mixin.h b/common/git/git_repo_mixin.h index 583b627eb1..5c77535e41 100644 --- a/common/git/git_repo_mixin.h +++ b/common/git/git_repo_mixin.h @@ -174,6 +174,15 @@ public: return m_common->GetGitRootDirectory(); } + /** + * @brief Get the project directory path, preserving symlinks if set + * @return The project directory path + */ + wxString GetProjectDir() const + { + return m_common->GetProjectDir(); + } + /** * @brief Return the connection types that have been tested for authentication * @return The connection types that have been tested for authentication diff --git a/common/git/kicad_git_common.cpp b/common/git/kicad_git_common.cpp index b122b3b092..8608771653 100644 --- a/common/git/kicad_git_common.cpp +++ b/common/git/kicad_git_common.cpp @@ -46,6 +46,7 @@ KIGIT_COMMON::KIGIT_COMMON( git_repository* aRepo ) : KIGIT_COMMON::KIGIT_COMMON( const KIGIT_COMMON& aOther ) : // Initialize base class and member variables m_repo( aOther.m_repo ), + m_projectDir( aOther.m_projectDir ), m_connType( aOther.m_connType ), m_remote( aOther.m_remote ), m_hostname( aOther.m_hostname ), @@ -69,6 +70,24 @@ git_repository* KIGIT_COMMON::GetRepo() const return m_repo; } + +wxString KIGIT_COMMON::GetProjectDir() const +{ + if( !m_projectDir.IsEmpty() ) + return m_projectDir; + + if( m_repo ) + { + const char* workdir = git_repository_workdir( m_repo ); + + if( workdir ) + return wxString( workdir ); + } + + return wxEmptyString; +} + + wxString KIGIT_COMMON::GetCurrentBranchName() const { wxCHECK( m_repo, wxEmptyString ); diff --git a/common/git/kicad_git_common.h b/common/git/kicad_git_common.h index 81f27d0ede..2bda4b32ff 100644 --- a/common/git/kicad_git_common.h +++ b/common/git/kicad_git_common.h @@ -118,6 +118,18 @@ public: wxString GetRemotename() const; + /** + * Set the project directory path, preserving any symlinks in the path. + * This is used to ensure git status paths match the paths used in the project tree. + */ + void SetProjectDir( const wxString& aProjectDir ) { m_projectDir = aProjectDir; } + + /** + * Get the project directory path. If a symlink-preserving path was set via + * SetProjectDir(), returns that path. Otherwise falls back to git_repository_workdir(). + */ + wxString GetProjectDir() const; + void ResetNextKey() { m_nextPublicKey = 0; } wxString GetNextPublicKey() @@ -165,6 +177,8 @@ public: protected: git_repository* m_repo; + wxString m_projectDir; // Project directory path preserving symlinks + GIT_CONN_TYPE m_connType; wxString m_remote; // This is the full connection string wxString m_hostname; // This is just the hostname without the protocol, username, or password diff --git a/common/git/libgit_backend.cpp b/common/git/libgit_backend.cpp index 9e7768aaaf..7f7b216461 100644 --- a/common/git/libgit_backend.cpp +++ b/common/git/libgit_backend.cpp @@ -402,7 +402,7 @@ std::map LIBGIT_BACKEND::GetFileStatus( GIT_STATUS_HANDLER KIGIT::GitStatusListPtr statusListPtr( status_list ); size_t count = git_status_list_entrycount( status_list ); - wxString repoWorkDir( git_repository_workdir( repo ) ); + wxString repoWorkDir = aHandler->GetProjectDir(); for( size_t ii = 0; ii < count; ++ii ) { @@ -453,7 +453,7 @@ void LIBGIT_BACKEND::UpdateRemoteStatus( GIT_STATUS_HANDLER* aHandler, if( !repo ) return; - wxString repoWorkDir( git_repository_workdir( repo ) ); + wxString repoWorkDir = aHandler->GetProjectDir(); for( auto& [absPath, fileStatus] : aFileStatus ) { @@ -484,32 +484,12 @@ void LIBGIT_BACKEND::UpdateRemoteStatus( GIT_STATUS_HANDLER* aHandler, wxString LIBGIT_BACKEND::GetWorkingDirectory( GIT_STATUS_HANDLER* aHandler ) { - git_repository* repo = aHandler->GetRepo(); - - if( !repo ) - return wxEmptyString; - - const char* workdir = git_repository_workdir( repo ); - - if( !workdir ) - return wxEmptyString; - - return wxString( workdir ); + return aHandler->GetProjectDir(); } wxString LIBGIT_BACKEND::GetWorkingDirectory( GIT_CONFIG_HANDLER* aHandler ) { - git_repository* repo = aHandler->GetRepo(); - - if( !repo ) - return wxEmptyString; - - const char* workdir = git_repository_workdir( repo ); - - if( !workdir ) - return wxEmptyString; - - return wxString( workdir ); + return aHandler->GetProjectDir(); } bool LIBGIT_BACKEND::GetConfigString( GIT_CONFIG_HANDLER* aHandler, const wxString& aKey, diff --git a/common/git/project_git_utils.cpp b/common/git/project_git_utils.cpp index 8664a6eac0..263815b058 100644 --- a/common/git/project_git_utils.cpp +++ b/common/git/project_git_utils.cpp @@ -24,9 +24,15 @@ #include "project_git_utils.h" #include "git_backend.h" +#include #include #include +#ifndef __WINDOWS__ +#include +#include +#endif + namespace KIGIT { @@ -76,4 +82,79 @@ wxString PROJECT_GIT_UTILS::GetCurrentHash( const wxString& aProjectFile, bool a return result; } + +wxString PROJECT_GIT_UTILS::ComputeSymlinkPreservingWorkDir( const wxString& aUserProjectPath, + const wxString& aCanonicalWorkDir ) +{ +#ifdef __WINDOWS__ + return aCanonicalWorkDir; +#else + if( aUserProjectPath.IsEmpty() || aCanonicalWorkDir.IsEmpty() ) + return aCanonicalWorkDir; + + char resolvedPath[PATH_MAX]; + + if( realpath( aUserProjectPath.mb_str(), resolvedPath ) == nullptr ) + return aCanonicalWorkDir; + + wxString canonicalUserPath = wxString::FromUTF8( resolvedPath ); + + if( !canonicalUserPath.EndsWith( wxFileName::GetPathSeparator() ) ) + canonicalUserPath += wxFileName::GetPathSeparator(); + + wxString canonicalWorkDirNorm = aCanonicalWorkDir; + + if( !canonicalWorkDirNorm.EndsWith( wxFileName::GetPathSeparator() ) ) + canonicalWorkDirNorm += wxFileName::GetPathSeparator(); + + // The workdir could be at or above the project directory + if( canonicalUserPath.StartsWith( canonicalWorkDirNorm ) ) + { + return aUserProjectPath.EndsWith( wxFileName::GetPathSeparator() ) + ? aUserProjectPath + : aUserProjectPath + wxFileName::GetPathSeparator(); + } + + // The workdir is above the user path - find the portion that corresponds to the workdir + wxFileName userFn( aUserProjectPath ); + wxFileName workDirFn( aCanonicalWorkDir ); + wxArrayString workDirParts = workDirFn.GetDirs(); + size_t workDirDepth = workDirParts.GetCount(); + + wxFileName canonicalUserFn( canonicalUserPath ); + wxArrayString canonicalUserParts = canonicalUserFn.GetDirs(); + + if( canonicalUserParts.GetCount() < workDirDepth ) + return aCanonicalWorkDir; + + wxArrayString canonicalWorkDirParts = workDirFn.GetDirs(); + + for( size_t i = 0; i < workDirDepth; ++i ) + { + if( canonicalUserParts[i] != canonicalWorkDirParts[i] ) + return aCanonicalWorkDir; + } + + wxArrayString userParts = userFn.GetDirs(); + + if( userParts.GetCount() < workDirDepth ) + return aCanonicalWorkDir; + + wxString result = userFn.GetVolume(); + + if( !result.IsEmpty() ) + result += wxFileName::GetVolumeSeparator(); + + result += wxFileName::GetPathSeparator(); + + for( size_t i = 0; i < workDirDepth; ++i ) + { + result += userParts[i]; + result += wxFileName::GetPathSeparator(); + } + + return result; +#endif +} + } // namespace KIGIT diff --git a/common/git/project_git_utils.h b/common/git/project_git_utils.h index 88844455e0..16fa39f969 100644 --- a/common/git/project_git_utils.h +++ b/common/git/project_git_utils.h @@ -74,6 +74,22 @@ public: */ static bool RemoveVCS( git_repository*& aRepo, const wxString& aProjectPath = wxEmptyString, bool aRemoveGitDir = false, wxString* aErrors = nullptr ); + + /** + * Compute a working directory path that preserves symlinks from the user's project path. + * + * When a project is opened via a symlinked path, git_repository_workdir() returns the + * canonical (symlink-resolved) path. This causes path mismatches between the tree cache + * (which uses the user-provided symlinked path) and the git status results (which use + * canonical paths). This function computes a working directory path that preserves + * the symlinks from the user's original project path. + * + * @param aUserProjectPath The path the user used to open the project (may contain symlinks) + * @param aCanonicalWorkDir The canonical workdir from git_repository_workdir() + * @return Working directory path with symlinks preserved, or aCanonicalWorkDir on failure + */ + static wxString ComputeSymlinkPreservingWorkDir( const wxString& aUserProjectPath, + const wxString& aCanonicalWorkDir ); }; } // namespace KIGIT diff --git a/kicad/project_tree_pane.cpp b/kicad/project_tree_pane.cpp index cea43cb812..381991965b 100644 --- a/kicad/project_tree_pane.cpp +++ b/kicad/project_tree_pane.cpp @@ -693,6 +693,15 @@ void PROJECT_TREE_PANE::ReCreateTreePrj() if( m_TreeProject->GetGitRepo() ) { + const char* canonicalWorkDir = git_repository_workdir( m_TreeProject->GetGitRepo() ); + + if( canonicalWorkDir ) + { + wxString symlinkWorkDir = KIGIT::PROJECT_GIT_UTILS::ComputeSymlinkPreservingWorkDir( + fn.GetPath(), wxString::FromUTF8( canonicalWorkDir ) ); + m_TreeProject->GitCommon()->SetProjectDir( symlinkWorkDir ); + } + m_TreeProject->GitCommon()->SetUsername( Prj().GetLocalSettings().m_GitRepoUsername ); m_TreeProject->GitCommon()->SetSSHKey( Prj().GetLocalSettings().m_GitSSHKey ); m_TreeProject->GitCommon()->UpdateCurrentBranchInfo(); @@ -1917,6 +1926,15 @@ void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent ) if( m_TreeProject->GetGitRepo() ) { + const char* canonicalWorkDir = git_repository_workdir( m_TreeProject->GetGitRepo() ); + + if( canonicalWorkDir ) + { + wxString symlinkWorkDir = KIGIT::PROJECT_GIT_UTILS::ComputeSymlinkPreservingWorkDir( + fn.GetPath(), wxString::FromUTF8( canonicalWorkDir ) ); + m_TreeProject->GitCommon()->SetProjectDir( symlinkWorkDir ); + } + m_TreeProject->GitCommon()->SetUsername( localSettings.m_GitRepoUsername ); m_TreeProject->GitCommon()->SetSSHKey( localSettings.m_GitSSHKey ); }