KiCad PCB EDA Suite
Loading...
Searching...
No Matches
text_eval_vcs.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
23#include <git/git_backend.h>
26#include <string_utils.h>
27#include <wx/filename.h>
28#include <wx/string.h>
29#include <wx/arrstr.h> // REQUIRED for wxString vector export on MSVC
30#include <algorithm>
31#include <map>
32
34{
35// Per-thread override that anchors repo-scoped queries to a specific path (for example the
36// loaded project directory). When empty, repo discovery falls back to the process cwd.
37namespace
38{
39 thread_local wxString tl_contextPath;
40 thread_local bool tl_contextIsFile = false;
41}
42
43
44void SetContextPath( const wxString& aPath )
45{
46 const bool isFile = !aPath.IsEmpty() && wxFileName( aPath ).FileExists();
47 tl_contextPath = aPath;
48 tl_contextIsFile = isFile;
49}
50
51
53{
54 return tl_contextPath.IsEmpty() ? wxString( wxT( "." ) ) : tl_contextPath;
55}
56
57
59{
60 return tl_contextIsFile;
61}
62
63
65 m_previous( tl_contextPath ),
66 m_previousIsFile( tl_contextIsFile )
67{
68 SetContextPath( aPath );
69}
70
71
73{
74 tl_contextPath = m_previous;
75 tl_contextIsFile = m_previousIsFile;
76}
77
78
79// Private implementation details
80namespace
81{
82 wxString ResolveEffectivePath( const std::string& aPath )
83 {
84 if( aPath.empty() || aPath == "." )
85 return GetContextPath();
86
87 wxFileName path( wxString::FromUTF8( aPath ) );
88
89 if( path.IsRelative() )
90 {
91 wxFileName context( GetContextPath() );
92 context.MakeAbsolute();
93 path.MakeAbsolute( tl_contextIsFile ? context.GetPath() : context.GetFullPath() );
94 }
95
96 return path.GetFullPath();
97 }
98
99
100 using VCS_QUERY = TEXT_EVAL::ENVIRONMENT::VCS_QUERY;
101 using VCS_VALUE = TEXT_EVAL::ENVIRONMENT::VCS_VALUE;
102
103 VCS_VALUE Capture( VCS_QUERY aQuery, const std::string& aPath, const std::string& aArgument,
104 int aOptions, const std::function<VCS_VALUE()>& aRead )
105 {
106 if( auto* environment = TEXT_EVAL::ENVIRONMENT::Current() )
107 {
108 wxFileName path = wxFileName::DirName( ResolveEffectivePath( aPath ) );
109 path.MakeAbsolute();
110 const bool contextFile = tl_contextIsFile && ( aPath.empty() || aPath == "." );
111 return environment->VcsValue( { aQuery, path.GetPath( wxPATH_GET_VOLUME ), aArgument, aOptions,
112 contextFile }, aRead );
113 }
114
115 return aRead();
116 }
117
118
119 git_repository* OpenRepo( const std::string& aPath )
120 {
121 if( !GetGitBackend() )
122 return nullptr;
123
124 wxFileName effective( ResolveEffectivePath( aPath ) );
125
126 if( ( !aPath.empty() && aPath != "." ) || tl_contextIsFile )
127 {
128 effective.MakeAbsolute();
129 return KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( TO_UTF8( effective.GetPath() ) );
130 }
131
132 return KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( TO_UTF8( effective.GetFullPath() ) );
133 }
134
135 void CloseRepo( git_repository* aRepo )
136 {
137 if( aRepo )
138 git_repository_free( aRepo );
139 }
140
141 git_oid MakeZeroOid()
142 {
143 git_oid oid;
144 git_oid_fromstrn( &oid, "0000000000000000000000000000000000000000", 40 );
145 return oid;
146 }
147
148 git_oid GetFileCommit( git_repository* aRepo, const std::string& aPath )
149 {
150 if( !aRepo )
151 return MakeZeroOid();
152
153 const git_oid head_oid = KIGIT::PROJECT_GIT_UTILS::GetCapturedHeadOid( aRepo );
154
155 if( git_oid_is_zero( &head_oid ) )
156 return MakeZeroOid();
157
158 // For repo-level query (empty or "."), just return HEAD
159 if( aPath.empty() || aPath == "." )
160 return head_oid;
161
162 const char* workdir = git_repository_workdir( aRepo );
163
164 if( !workdir )
165 return MakeZeroOid();
166
167 wxFileName file( ResolveEffectivePath( aPath ) );
168
170 file.GetPath(), wxString::FromUTF8( workdir ) );
171
172 if( !file.MakeRelativeTo( base ) )
173 return MakeZeroOid();
174
175 const std::string treePath = file.GetFullPath( wxPATH_UNIX ).ToStdString( wxConvUTF8 );
176
177 if( treePath.empty() || treePath == "." || treePath == ".." || treePath.starts_with( "../" ) )
178 return MakeZeroOid();
179
180 // For file-specific query, walk history to find last commit that touched this file
181 git_revwalk* walker = nullptr;
182
183 if( git_revwalk_new( &walker, aRepo ) != 0 )
184 return MakeZeroOid();
185
186 git_revwalk_sorting( walker, GIT_SORT_TIME );
187 git_revwalk_push( walker, &head_oid );
188
189 // Walk through commits to find when the file was last modified
190 git_oid result = MakeZeroOid();
191 git_oid commit_oid;
192 git_oid prev_blob_oid = MakeZeroOid();
193 bool first_commit = true;
194
195 while( git_revwalk_next( &commit_oid, walker ) == 0 )
196 {
197 git_commit* commit = nullptr;
198
199 if( git_commit_lookup( &commit, aRepo, &commit_oid ) != 0 )
200 continue;
201
202 // Get the tree for this commit
203 git_tree* tree = nullptr;
204
205 if( git_commit_tree( &tree, commit ) == 0 )
206 {
207 // Try to find the file in this tree
208 git_tree_entry* entry = nullptr;
209
210 if( git_tree_entry_bypath( &entry, tree, treePath.c_str() ) == 0 )
211 {
212 const git_oid* blob_oid = git_tree_entry_id( entry );
213
214 if( first_commit )
215 {
216 // First time we see this file, remember its blob ID
217 git_oid_cpy( &prev_blob_oid, blob_oid );
218 git_oid_cpy( &result, &commit_oid );
219 first_commit = false;
220 }
221 else if( git_oid_cmp( blob_oid, &prev_blob_oid ) != 0 )
222 {
223 // File content changed - previous commit is where it changed
224 git_tree_entry_free( entry );
225 git_tree_free( tree );
226 git_commit_free( commit );
227 break;
228 }
229 else
230 {
231 // File unchanged, keep looking
232 git_oid_cpy( &result, &commit_oid );
233 }
234
235 git_tree_entry_free( entry );
236 }
237 else if( !first_commit )
238 {
239 // File doesn't exist in this commit, but existed before
240 // So the previous commit is where it was added/last modified
241 git_tree_free( tree );
242 git_commit_free( commit );
243 break;
244 }
245
246 git_tree_free( tree );
247 }
248
249 git_commit_free( commit );
250 }
251
252 git_revwalk_free( walker );
253 return result;
254 }
255
256 struct DescribeInfo
257 {
258 std::string tag;
259 int distance;
260 };
261
262 DescribeInfo ReadDescribeInfo( const std::string& aMatch, bool aAnyTags )
263 {
264 git_repository* repo = OpenRepo( "." );
265
266 if( !repo )
267 return { std::string(), 0 };
268
269 const git_oid head_oid = KIGIT::PROJECT_GIT_UTILS::GetCapturedHeadOid( repo );
270
271 if( git_oid_is_zero( &head_oid ) )
272 {
273 CloseRepo( repo );
274 return { std::string(), 0 };
275 }
276
277 git_strarray tag_names;
278
279 if( git_tag_list_match( &tag_names, aMatch.empty() ? "*" : aMatch.c_str(), repo ) != 0 )
280 {
281 CloseRepo( repo );
282 return { std::string(), 0 };
283 }
284
285 // Build map of commit OID -> tag name upfront
286 std::map<git_oid, std::string, decltype(
287 [](const git_oid& a, const git_oid& b)
288 {
289 return git_oid_cmp(&a, &b) < 0;
290 } )> commit_to_tag;
291
292 for( size_t i = 0; i < tag_names.count; ++i )
293 {
294 git_object* tag_obj = nullptr;
295
296 if( git_revparse_single( &tag_obj, repo, tag_names.strings[i] ) == 0 )
297 {
298 git_object_t type = git_object_type( tag_obj );
299
300 if( type == GIT_OBJECT_TAG )
301 {
302 git_object* target = nullptr;
303
304 if( git_tag_peel( &target, (git_tag*) tag_obj ) == 0 )
305 {
306 commit_to_tag[*git_object_id( target )] = tag_names.strings[i];
307 git_object_free( target );
308 }
309 }
310 else if( aAnyTags && type == GIT_OBJECT_COMMIT )
311 {
312 commit_to_tag[*git_object_id( tag_obj )] = tag_names.strings[i];
313 }
314
315 git_object_free( tag_obj );
316 }
317 }
318
319 git_strarray_dispose( &tag_names );
320
321 git_revwalk* walker = nullptr;
322
323 if( git_revwalk_new( &walker, repo ) != 0 )
324 {
325 CloseRepo( repo );
326 return { std::string(), 0 };
327 }
328
329 git_revwalk_sorting( walker, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME );
330 git_revwalk_push( walker, &head_oid );
331
332 DescribeInfo result{ std::string(), 0 };
333 int distance = 0;
334 git_oid commit_oid;
335
336 while( git_revwalk_next( &commit_oid, walker ) == 0 )
337 {
338 auto it = commit_to_tag.find( commit_oid );
339
340 if( it != commit_to_tag.end() )
341 {
342 result.tag = it->second;
343 result.distance = distance;
344 break;
345 }
346
347 distance++;
348 }
349
350 git_revwalk_free( walker );
351 CloseRepo( repo );
352 return result;
353 }
354
355 DescribeInfo GetDescribeInfo( const std::string& aMatch, bool aAnyTags )
356 {
357 const auto value = Capture( VCS_QUERY::DESCRIPTION, ".", aMatch, aAnyTags,
358 [&]() -> VCS_VALUE
359 {
360 const auto description = ReadDescribeInfo( aMatch, aAnyTags );
361 return { description.tag, description.distance };
362 } );
363 return { value.text, static_cast<int>( value.number ) };
364 }
365
366 std::string ReadCommitSignatureField( const std::string& aPath, bool aUseCommitter, bool aGetEmail )
367 {
368 git_repository* repo = OpenRepo( aPath );
369
370 if( !repo )
371 return std::string();
372
373 git_oid oid = GetFileCommit( repo, aPath );
374
375 if( git_oid_is_zero( &oid ) )
376 {
377 CloseRepo( repo );
378 return std::string();
379 }
380
381 git_commit* commit = nullptr;
382 std::string result;
383
384 if( git_commit_lookup( &commit, repo, &oid ) == 0 )
385 {
386 const git_signature* sig = aUseCommitter ? git_commit_committer( commit ) : git_commit_author( commit );
387
388 if( sig )
389 {
390 const char* field = aGetEmail ? sig->email : sig->name;
391
392 if( field )
393 result = field;
394 }
395
396 git_commit_free( commit );
397 }
398
399 CloseRepo( repo );
400 return result;
401 }
402
403 std::string GetCommitSignatureField( const std::string& aPath, bool aUseCommitter, bool aGetEmail )
404 {
405 return Capture( VCS_QUERY::SIGNATURE, aPath, aPath, ( aUseCommitter ? 2 : 0 ) | ( aGetEmail ? 1 : 0 ),
406 [&]() -> VCS_VALUE { return { ReadCommitSignatureField( aPath, aUseCommitter, aGetEmail ) }; } ).text;
407 }
408
409} // anonymous namespace
410
411
412static std::string ReadCommitHash( const std::string& aPath )
413{
414 git_repository* repo = OpenRepo( aPath );
415
416 if( !repo )
417 return std::string();
418
419 git_oid oid = GetFileCommit( repo, aPath );
420
421 if( git_oid_is_zero( &oid ) )
422 {
423 CloseRepo( repo );
424 return std::string();
425 }
426
427 char hash[GIT_OID_HEXSZ + 1];
428 git_oid_tostr( hash, sizeof( hash ), &oid );
429
430 CloseRepo( repo );
431 return hash;
432}
433
434
435std::string GetCommitHash( const std::string& aPath, int aLength )
436{
437 const auto value = Capture( VCS_QUERY::HASH, aPath, aPath, 0,
438 [&]() -> VCS_VALUE { return { ReadCommitHash( aPath ) }; } );
439 return value.text.substr( 0, std::clamp( aLength, 4, GIT_OID_HEXSZ ) );
440}
441
442
443std::string GetNearestTag( const std::string& aMatch, bool aAnyTags )
444{
445 return GetDescribeInfo( aMatch, aAnyTags ).tag;
446}
447
448
449int GetDistanceFromTag( const std::string& aMatch, bool aAnyTags )
450{
451 return GetDescribeInfo( aMatch, aAnyTags ).distance;
452}
453
454
455static bool ReadIsDirty( bool aIncludeUntracked )
456{
457 git_repository* repo = OpenRepo( "." );
458
459 if( !repo )
460 return false;
461
462 git_status_list* status = nullptr;
463 git_status_options statusOpts;
464 git_status_options_init( &statusOpts, GIT_STATUS_OPTIONS_VERSION );
465
466 statusOpts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
467 statusOpts.flags = aIncludeUntracked ? GIT_STATUS_OPT_INCLUDE_UNTRACKED : GIT_STATUS_OPT_EXCLUDE_SUBMODULES;
468
469 bool isDirty = false;
470
471 if( git_status_list_new( &status, repo, &statusOpts ) == 0 )
472 {
473 isDirty = git_status_list_entrycount( status ) > 0;
474 git_status_list_free( status );
475 }
476
477 CloseRepo( repo );
478 return isDirty;
479}
480
481
482bool IsDirty( bool aIncludeUntracked )
483{
484 return Capture( VCS_QUERY::DIRTY, ".", "", aIncludeUntracked,
485 [&]() -> VCS_VALUE { return { {}, ReadIsDirty( aIncludeUntracked ) }; } ).number != 0;
486}
487
488
489std::string GetAuthor( const std::string& aPath )
490{
491 return GetCommitSignatureField( aPath, false, false );
492}
493
494
495std::string GetAuthorEmail( const std::string& aPath )
496{
497 return GetCommitSignatureField( aPath, false, true );
498}
499
500
501std::string GetCommitter( const std::string& aPath )
502{
503 return GetCommitSignatureField( aPath, true, false );
504}
505
506
507std::string GetCommitterEmail( const std::string& aPath )
508{
509 return GetCommitSignatureField( aPath, true, true );
510}
511
512
513static std::string ReadBranch()
514{
515 git_repository* repo = OpenRepo( "." );
516
517 if( !repo )
518 return std::string();
519
520 KIGIT_COMMON common( repo );
521 wxString branchName = common.GetCurrentBranchName();
522
523 CloseRepo( repo );
524 return branchName.ToStdString();
525}
526
527
528std::string GetBranch()
529{
530 return Capture( VCS_QUERY::BRANCH, ".", "", 0,
531 []() -> VCS_VALUE { return { ReadBranch() }; } ).text;
532}
533
534
535static int64_t ReadCommitTimestamp( const std::string& aPath )
536{
537 git_repository* repo = OpenRepo( aPath );
538
539 if( !repo )
540 return 0;
541
542 git_oid oid = GetFileCommit( repo, aPath );
543
544 if( git_oid_is_zero( &oid ) )
545 {
546 CloseRepo( repo );
547 return 0;
548 }
549
550 git_commit* commit = nullptr;
551 int64_t timestamp = 0;
552
553 if( git_commit_lookup( &commit, repo, &oid ) == 0 )
554 {
555 timestamp = static_cast<int64_t>( git_commit_time( commit ) );
556 git_commit_free( commit );
557 }
558
559 CloseRepo( repo );
560 return timestamp;
561}
562
563
564int64_t GetCommitTimestamp( const std::string& aPath )
565{
566 return Capture( VCS_QUERY::TIMESTAMP, aPath, aPath, 0,
567 [&]() -> VCS_VALUE { return { {}, ReadCommitTimestamp( aPath ) }; } ).number;
568}
569
570
571std::string GetCommitDate( const std::string& aPath )
572{
573 int64_t timestamp = GetCommitTimestamp( aPath );
574 return timestamp > 0 ? std::to_string( timestamp ) : std::string();
575}
576
577
579{
580 const auto read = [&]() -> VCS_VALUE
581 {
582 const auto& [query, path, argument, options, contextFile] = aKey;
583
584 if( query == VCS_QUERY::HEAD )
585 {
586 if( !GetGitBackend() )
587 return {};
588
589 git_repository* raw = nullptr;
590
591 // Discovery could select the main repository instead of this linked worktree's HEAD.
592 if( git_repository_open( &raw, path.ToUTF8().data() ) != 0 )
593 return {};
594
595 KIGIT::GitRepositoryPtr repo( raw );
596 git_oid oid{};
597
598 if( git_reference_name_to_id( &oid, repo.get(), "HEAD" ) != 0 )
599 return {};
600
601 char hash[GIT_OID_HEXSZ + 1];
602 git_oid_tostr( hash, sizeof( hash ), &oid );
603 return { hash };
604 }
605
606 const bool fileQuery = ( query == VCS_QUERY::HASH || query == VCS_QUERY::SIGNATURE
607 || query == VCS_QUERY::TIMESTAMP )
608 && !argument.empty() && argument != ".";
609 const CONTEXT_PATH_SCOPE context( fileQuery || contextFile ? wxFileName( path ).GetPath() : path );
610 const std::string file = fileQuery ? path.ToStdString( wxConvUTF8 ) : std::string();
611
612 switch( query )
613 {
614 case VCS_QUERY::HASH:
615 return { ReadCommitHash( file ) };
616
617 case VCS_QUERY::DESCRIPTION:
618 {
619 const auto description = ReadDescribeInfo( argument, options != 0 );
620 return { description.tag, description.distance };
621 }
622
623 case VCS_QUERY::SIGNATURE:
624 return { ReadCommitSignatureField( file, ( options & 2 ) != 0, ( options & 1 ) != 0 ) };
625
626 case VCS_QUERY::BRANCH:
627 return { ReadBranch() };
628
629 case VCS_QUERY::DIRTY:
630 return { {}, ReadIsDirty( options != 0 ) };
631
632 case VCS_QUERY::TIMESTAMP:
633 return { {}, ReadCommitTimestamp( file ) };
634
635 case VCS_QUERY::HEAD:
636 break;
637 }
638
639 return {};
640 };
641
642 if( auto* environment = TEXT_EVAL::ENVIRONMENT::Current() )
643 return environment->VcsValue( aKey, read );
644
645 return read();
646}
647
648} // namespace TEXT_EVAL_VCS
wxString GetCommitHash()
Get the commit hash as a string.
static git_oid GetCapturedHeadOid(git_repository *aRepo)
Return HEAD for commit-based text queries.
static git_repository * GetRepositoryForFile(const char *aFilename)
Discover and open the repository that contains the given file.
static wxString ComputeSymlinkPreservingWorkDir(const wxString &aUserProjectPath, const wxString &aCanonicalWorkDir)
Compute a working directory path that preserves symlinks from the user's project path.
wxString GetCurrentBranchName() const
static ENVIRONMENT * Current()
std::tuple< VCS_QUERY, wxString, std::string, int, bool > VCS_KEY
Query, absolute path (Git directory for HEAD), selector or pattern, options, and whether the context ...
RAII helper that sets the VCS context path on construction and restores the previous value on destruc...
CONTEXT_PATH_SCOPE(const wxString &aPath)
GIT_BACKEND * GetGitBackend()
std::unique_ptr< git_repository, decltype([](git_repository *aRepo) { git_repository_free(aRepo); })> GitRepositoryPtr
A unique pointer for git_repository objects with automatic cleanup.
VCS (Version Control System) utility functions for text evaluation.
std::string GetAuthor(const std::string &aPath)
Get the author name of the HEAD commit.
bool IsDirty(bool aIncludeUntracked)
Check if the repository has uncommitted changes.
wxString GetContextPath()
Return the current context path for repo-scoped VCS queries.
bool GetContextIsFile()
File/directory classification captured when the current context was activated.
static std::string ReadBranch()
static int64_t ReadCommitTimestamp(const std::string &aPath)
TEXT_EVAL::ENVIRONMENT::VCS_VALUE ReadSource(const TEXT_EVAL::ENVIRONMENT::VCS_KEY &aKey)
Re-read an owned query descriptor using any active frame memo.
static std::string ReadCommitHash(const std::string &aPath)
std::string GetCommitterEmail(const std::string &aPath)
Get the committer email of the HEAD commit.
std::string GetCommitDate(const std::string &aPath)
Get the commit date of the HEAD commit as a timestamp string.
std::string GetAuthorEmail(const std::string &aPath)
Get the author email of the HEAD commit.
void SetContextPath(const wxString &aPath)
Set the filesystem path used for repository discovery and relative file queries.
std::string GetCommitter(const std::string &aPath)
Get the committer name of the HEAD commit.
std::string GetBranch()
Get the current branch name.
std::string GetNearestTag(const std::string &aMatch, bool aAnyTags)
Get the nearest tag/label from HEAD.
static bool ReadIsDirty(bool aIncludeUntracked)
int64_t GetCommitTimestamp(const std::string &aPath)
Get the commit timestamp (Unix time) of the HEAD commit.
int GetDistanceFromTag(const std::string &aMatch, bool aAnyTags)
Get the number of commits since the nearest matching tag.
static float distance(const SFVEC2UI &a, const SFVEC2UI &b)
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
std::string path
wxString result
Test unit parsing edge cases and error handling.