26#include <wx/filename.h>
41 wxString* aTrustCommand )
59 return backend->
CreateBranch( aRepo, aBranchName, wxEmptyString );
64 bool aRemoveGitDir, wxString* aErrors )
71 return backend->
RemoveVCS( aRepo, aProjectPath, aRemoveGitDir, aErrors );
81 const auto read = [&]() -> ENVIRONMENT::VCS_VALUE
85 if( git_reference_name_to_id( &oid, aRepo,
"HEAD" ) != 0 )
88 char hash[GIT_OID_HEXSZ + 1];
89 git_oid_tostr( hash,
sizeof( hash ), &oid );
92 auto* environment = ENVIRONMENT::Current();
93 const char*
path = git_repository_path( aRepo );
94 const auto value = environment &&
path
95 ? environment->VcsValue( { ENVIRONMENT::VCS_QUERY::HEAD, wxString::FromUTF8(
path ),
"", 0,
false }, read )
99 if( value.text.empty() || git_oid_fromstr( &oid, value.text.c_str() ) != 0 )
108 const auto read = [&]
113 if( git_oid_is_zero( &oid ) )
114 return wxString(
"no hash" );
116 char hash[GIT_OID_HEXSZ + 1];
117 git_oid_tostr( hash,
sizeof( hash ), &oid );
118 return wxString::FromUTF8( hash );
121 const wxString hash = environment ? environment->GitHash( aProjectFile, read ) : read();
122 return aShort ? hash.Left( 8 ) : hash;
134 char resolved[PATH_MAX];
136 if( aPath.IsEmpty() || realpath( aPath.mb_str(), resolved ) ==
nullptr )
139 wxString out = wxString::FromUTF8( resolved );
141 if( !out.EndsWith( wxFileName::GetPathSeparator() ) && wxFileName::DirExists( out ) )
142 out += wxFileName::GetPathSeparator();
150 const wxString& aCanonicalWorkDir )
153 return aCanonicalWorkDir;
155 if( aUserProjectPath.IsEmpty() || aCanonicalWorkDir.IsEmpty() )
156 return aCanonicalWorkDir;
158 char resolvedPath[PATH_MAX];
160 if( realpath( aUserProjectPath.mb_str(), resolvedPath ) ==
nullptr )
161 return aCanonicalWorkDir;
163 wxString canonicalUserPath = wxString::FromUTF8( resolvedPath );
165 if( !canonicalUserPath.EndsWith( wxFileName::GetPathSeparator() ) )
166 canonicalUserPath += wxFileName::GetPathSeparator();
168 wxString canonicalWorkDirNorm = aCanonicalWorkDir;
170 if( !canonicalWorkDirNorm.EndsWith( wxFileName::GetPathSeparator() ) )
171 canonicalWorkDirNorm += wxFileName::GetPathSeparator();
177 if( canonicalUserPath == canonicalWorkDirNorm )
179 return aUserProjectPath.EndsWith( wxFileName::GetPathSeparator() )
181 : aUserProjectPath + wxFileName::GetPathSeparator();
188 userFn.AssignDir( aUserProjectPath );
190 wxFileName canonicalUserFn;
191 canonicalUserFn.AssignDir( canonicalUserPath );
193 wxFileName canonicalWorkDirFn;
194 canonicalWorkDirFn.AssignDir( canonicalWorkDirNorm );
196 while( canonicalUserFn.GetFullPath() != canonicalWorkDirFn.GetFullPath() )
198 if( canonicalUserFn.GetDirCount() == 0 || userFn.GetDirCount() == 0 )
199 return aCanonicalWorkDir;
201 canonicalUserFn.RemoveLastDir();
202 userFn.RemoveLastDir();
208 if(
canonicalDir( userFn.GetPathWithSep() ) != canonicalWorkDirNorm )
209 return aCanonicalWorkDir;
211 return userFn.GetPathWithSep();
216static bool pathIsUnder(
const wxString& aAbsPath,
const wxString& aProjectPath )
218 wxString absPath = aAbsPath;
219 wxString projectPath = aProjectPath;
222 absPath.Replace( wxT(
"\\" ), wxT(
"/" ) );
223 projectPath.Replace( wxT(
"\\" ), wxT(
"/" ) );
226 if( !projectPath.EndsWith( wxT(
"/" ) ) )
227 projectPath += wxT(
"/" );
230 return absPath.Lower().StartsWith( projectPath.Lower() );
232 return absPath.StartsWith( projectPath );
239 if( aProjectPath.IsEmpty() )
251static std::optional<wxString>
prefixUnder(
const wxString& aWorkDir,
const wxString& aProjectPath )
253 wxString workDir = aWorkDir;
254 wxString projectPath = aProjectPath;
256 workDir.Replace( wxS(
"\\" ), wxS(
"/" ) );
257 projectPath.Replace( wxS(
"\\" ), wxS(
"/" ) );
259 if( !workDir.EndsWith( wxS(
"/" ) ) )
260 workDir += wxS(
"/" );
262 if( !projectPath.EndsWith( wxS(
"/" ) ) )
263 projectPath += wxS(
"/" );
267 if( !projectPath.Lower().StartsWith( workDir.Lower() ) )
270 if( !projectPath.StartsWith( workDir ) )
274 return projectPath.Mid( workDir.length() );
280 if( aWorkDir.IsEmpty() || aProjectPath.IsEmpty() )
283 std::optional<wxString> prefix =
prefixUnder( aWorkDir, aProjectPath );
295 if( !aRepo || aPath.IsEmpty() )
298 const char* value =
nullptr;
299 git_attr_options opts = {};
300 opts.version = GIT_ATTR_OPTIONS_VERSION;
301 opts.flags = GIT_ATTR_CHECK_INDEX_THEN_FILE;
306 opts.flags |= GIT_ATTR_CHECK_INCLUDE_COMMIT;
307 git_oid_cpy( &opts.attr_commit_id, aCommit );
310 if( git_attr_get_ext( &value, aRepo, &opts, aPath.utf8_string().c_str(),
"filter" ) != GIT_OK )
315 return git_attr_value( value ) == GIT_ATTR_VALUE_STRING && strcmp( value,
"lfs" ) == 0;
327 const git_oid* commit =
nullptr;
331 int lfsWalkEntry(
const char* aRoot,
const git_tree_entry* aEntry,
void* aPayload )
333 LFS_WALK* walk =
static_cast<LFS_WALK*
>( aPayload );
334 std::string
path = std::string( aRoot ) + git_tree_entry_name( aEntry );
336 if( git_tree_entry_type( aEntry ) == GIT_OBJECT_TREE )
338 std::string dir =
path +
"/";
341 if( walk->prefix.compare( 0, dir.length(), dir ) != 0
342 && dir.compare( 0, walk->prefix.length(), walk->prefix ) != 0 )
350 if( git_tree_entry_type( aEntry ) != GIT_OBJECT_BLOB )
353 if(
path.compare( 0, walk->prefix.length(), walk->prefix ) != 0 )
358 walk->example = wxString::FromUTF8(
path.c_str() );
370 if( !aCommon || !aCommon->
GetRepo() )
371 return wxEmptyString;
375 return _(
"Could not work out which folder this project occupies in the repository, "
376 "so the operation was cancelled." );
382 return wxEmptyString;
384 return wxString::Format(
_(
"This project keeps '%s' in Git LFS, which KiCad cannot read or "
385 "write. Use git on the command line for this repository." ),
401 git_oid_cpy( &commitOid, aCommit );
402 else if( git_reference_name_to_id( &commitOid, repo,
"HEAD" ) != GIT_OK )
405 git_commit* commit =
nullptr;
407 if( git_commit_lookup( &commit, repo, &commitOid ) != GIT_OK )
411 git_tree* tree =
nullptr;
413 if( git_commit_tree( &tree, commit ) != GIT_OK )
417 LFS_WALK walk{ repo, prefix->utf8_string(), wxEmptyString, aCommit };
419 git_tree_walk( tree, GIT_TREEWALK_PRE, lfsWalkEntry, &walk );
422 *aExample = walk.example;
424 return !walk.example.IsEmpty();
430 static const std::vector<wxString> patterns = {
433 wxS(
"_autosave-*" ),
434 wxS(
"fp-info-cache" ),
436 wxS(
"*.[lL][cC][kK]" ),
438 wxS(
"*.[kK][iI][cC][aA][dD]_[pP][rR][lL]" ),
442 wxS(
"*.mine.[0-9]" ),
443 wxS(
"*.mine.[0-9][0-9]" ),
445 wxS(
"*.theirs.[0-9]" ),
446 wxS(
"*.theirs.[0-9][0-9]" ),
456 using PATHSPEC_PTR = std::unique_ptr<git_pathspec,
decltype( &git_pathspec_free )>;
459 PATHSPEC_PTR transientPathspec(
bool aDirectory )
461 std::vector<std::string> patterns;
465 if( pattern.EndsWith( wxS(
"/" ) ) )
470 pattern.RemoveLast();
473 patterns.push_back( pattern.utf8_string() );
476 std::vector<char*> paths;
478 for( std::string& pattern : patterns )
479 paths.push_back( pattern.data() );
481 git_strarray pathArray = { paths.data(), paths.size() };
482 git_pathspec* spec =
nullptr;
484 if( git_pathspec_new( &spec, &pathArray ) != GIT_OK )
485 return PATHSPEC_PTR(
nullptr, git_pathspec_free );
487 return PATHSPEC_PTR( spec, git_pathspec_free );
495 static const PATHSPEC_PTR filePatterns = transientPathspec(
false );
496 static const PATHSPEC_PTR directoryPatterns = transientPathspec(
true );
498 if( filePatterns && git_pathspec_matches_path( filePatterns.get(), 0, aFile.GetFullName().utf8_str() ) )
501 for(
const wxString& dir : aFile.GetDirs() )
503 if( directoryPatterns && git_pathspec_matches_path( directoryPatterns.get(), 0, dir.utf8_str() ) )
518 aCommon->SetShuttingDown(
true );
529 aCommon->SetRepo(
nullptr );
534 std::unique_lock<std::mutex> gitLock = aCommon->TryLockAction();
536 if( gitLock.owns_lock() )
538 aCommon->SetRepo(
nullptr );
539 git_repository_free( repo );
546 auto cleanup = [repo, old = std::move( aCommon )]()
mutable
548 std::unique_lock<std::mutex> g = old->LockAction();
549 old->SetRepo(
nullptr );
550 git_repository_free( repo );
565 const wxString& aDestPath )
567 if( !aRepo || aRef.IsEmpty() || aPath.IsEmpty() )
575 git_tree_entry* entry =
nullptr;
577 if( git_tree_entry_bypath( &entry, treePtr.get(), aPath.ToUTF8().data() ) != 0 )
582 if( git_tree_entry_type( entry ) != GIT_OBJECT_BLOB )
585 git_object* blobObj =
nullptr;
587 if( git_tree_entry_to_object( &blobObj, aRepo, entry ) != 0 )
591 git_blob* blob =
reinterpret_cast<git_blob*
>( blobObj );
597 if( !out.Create( aDestPath,
true, wxS_IRUSR | wxS_IWUSR ) )
600 const void* raw = git_blob_rawcontent( blob );
601 git_off_t size = git_blob_rawsize( blob );
603 return out.Write( raw,
static_cast<size_t>( size ) ) ==
static_cast<size_t>( size );
609 if( !aRepo || aRef.IsEmpty() || aDestDir.IsEmpty() )
618 if( !wxFileName::Mkdir( aDestDir, 0700, wxPATH_MKDIR_FULL ) )
621 std::string destUtf8 = aDestDir.utf8_string();
622 git_checkout_options opts;
623 git_checkout_options_init( &opts, GIT_CHECKOUT_OPTIONS_VERSION );
627 opts.checkout_strategy = GIT_CHECKOUT_FORCE | GIT_CHECKOUT_RECREATE_MISSING | GIT_CHECKOUT_DONT_UPDATE_INDEX;
628 opts.target_directory = destUtf8.c_str();
630 return git_checkout_tree( aRepo,
reinterpret_cast<git_object*
>( treePtr.get() ), &opts ) == GIT_OK;
virtual int CreateBranch(git_repository *aRepo, const wxString &aBranchName, const wxString &aFrom)=0
KIGIT_ORPHAN_REGISTRY & OrphanRegistry()
Return the process-wide orphan thread registry owned by this backend.
virtual git_repository * GetRepositoryForFile(const char *aFilename, wxString *aError, wxString *aTrustCommand)=0
aError receives why git refused to open the repository, and aTrustCommand the command that trusts it ...
virtual bool RemoveVCS(git_repository *&aRepo, const wxString &aProjectPath, bool aRemoveGitDir, wxString *aErrors)=0
static bool WriteBlobToFile(git_repository *aRepo, const wxString &aRef, const wxString &aPath, const wxString &aDestPath)
Write a file as it exists at a git ref to a path on disk, so a committed version can be diffed.
static bool PathUsesGitLfs(git_repository *aRepo, const wxString &aPath, const git_oid *aCommit=nullptr)
Whether git routes aPath through the Git LFS filter.
static git_oid GetCapturedHeadOid(git_repository *aRepo)
Return HEAD for commit-based text queries.
static wxString GetCurrentHash(const wxString &aProjectFile, bool aShort)
Return the current HEAD commit hash for the repository containing aProjectFile.
static git_repository * GetRepositoryForFile(const char *aFilename, wxString *aError=nullptr, wxString *aTrustCommand=nullptr)
Discover and open the repository that contains the given file.
static bool IsTransientGitFile(const wxFileName &aFile)
Match a project-relative path against the shared transient patterns.
static const std::vector< wxString > & TransientGitIgnorePatterns()
Shared patterns for new .gitignore files and the untracked-file filter.
static void SafeFreeRepository(std::shared_ptr< KIGIT_COMMON > aCommon, const std::string &aLabel)
Free the session's repository handle without pulling it out from under a running operation.
static wxString ComputeSymlinkPreservingWorkDir(const wxString &aUserProjectPath, const wxString &aCanonicalWorkDir)
Compute a working directory path that preserves symlinks from the user's project path.
static bool WriteTreeToDir(git_repository *aRepo, const wxString &aRef, const wxString &aDestDir)
Check out a whole tree into a directory, leaving the repository's working copy and index alone.
static bool ProjectUsesGitLfs(const KIGIT_COMMON &aCommon, wxString *aExample=nullptr, const git_oid *aCommit=nullptr)
Whether any committed file in the project's folder goes through Git LFS.
static bool IsWithinProjectPath(const wxString &aAbsPath, const wxString &aProjectPath)
Test whether an absolute file path lives inside the current project directory.
static bool RemoveVCS(git_repository *&aRepo, const wxString &aProjectPath=wxEmptyString, bool aRemoveGitDir=false, wxString *aErrors=nullptr)
Remove version control from a directory by freeing the repository and optionally removing the ....
static wxString LfsRefusal(KIGIT_COMMON *aCommon)
Return a refusal message for LFS files or an unknown project folder, otherwise empty.
static int CreateBranch(git_repository *aRepo, const wxString &aBranchName)
Create a new branch based on HEAD.
static std::optional< wxString > ProjectPrefix(const wxString &aWorkDir, const wxString &aProjectPath)
The project's folder inside the repository, ending in a slash.
The project's git session.
std::optional< wxString > GetProjectPrefix() const
Project folder relative to the repository, with a trailing slash.
git_repository * GetRepo() const
bool Register(const std::string &aLabel, F &&aWork)
Spawn a tracked orphan thread running aWork.
A text evaluation frame with one frozen clock and memoized external queries.
static ENVIRONMENT * Current()
GIT_BACKEND * GetGitBackend()
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
std::unique_ptr< git_tree, decltype([](git_tree *aTree) { git_tree_free(aTree); })> GitTreePtr
A unique pointer for git_tree objects with automatic cleanup.
static bool pathIsUnder(const wxString &aAbsPath, const wxString &aProjectPath)
wxString BranchNameError(const wxString &aName)
Empty when aName is a branch name git would accept, otherwise why it would not, in the words to show ...
std::unique_ptr< git_repository, decltype([](git_repository *aRepo) { git_repository_free(aRepo); })> GitRepositoryPtr
A unique pointer for git_repository objects with automatic cleanup.
git_tree * ResolveRefToTree(git_repository *aRepo, const wxString &aRef)
Resolve a string ref (branch name, short OID, full OID, tag) to its tree.
std::unique_ptr< git_commit, decltype([](git_commit *aCommit) { git_commit_free(aCommit); })> GitCommitPtr
A unique pointer for git_commit objects with automatic cleanup.
static wxString canonicalDir(const wxString &aPath)
std::unique_ptr< git_tree_entry, decltype([](git_tree_entry *aEntry) { git_tree_entry_free(aEntry); })> GitTreeEntryPtr
A unique pointer for git_tree_entry objects with automatic cleanup.
static std::optional< wxString > prefixUnder(const wxString &aWorkDir, const wxString &aProjectPath)
std::unique_ptr< git_object, decltype([](git_object *aObject) { git_object_free(aObject); })> GitObjectPtr
A unique pointer for git_object objects with automatic cleanup.