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( TEXT_EVAL::ENVIRONMENT* 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,
112 path.GetPath( (int) wxPATH_GET_VOLUME ),
113 aArgument,
114 aOptions,
115 contextFile },
116 aRead );
117 }
118
119 return aRead();
120 }
121
122
123 git_repository* OpenRepo( const std::string& aPath )
124 {
125 if( !GetGitBackend() )
126 return nullptr;
127
128 wxFileName effective( ResolveEffectivePath( aPath ) );
129
130 if( ( !aPath.empty() && aPath != "." ) || tl_contextIsFile )
131 {
132 effective.MakeAbsolute();
133 return KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( TO_UTF8( effective.GetPath() ) );
134 }
135
136 return KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( TO_UTF8( effective.GetFullPath() ) );
137 }
138
139 void CloseRepo( git_repository* aRepo )
140 {
141 if( aRepo )
142 git_repository_free( aRepo );
143 }
144
145 git_oid MakeZeroOid()
146 {
147 git_oid oid;
148 git_oid_fromstrn( &oid, "0000000000000000000000000000000000000000", 40 );
149 return oid;
150 }
151
152 git_oid GetFileCommit( git_repository* aRepo, const std::string& aPath )
153 {
154 if( !aRepo )
155 return MakeZeroOid();
156
157 const git_oid head_oid = KIGIT::PROJECT_GIT_UTILS::GetCapturedHeadOid( aRepo );
158
159 if( git_oid_is_zero( &head_oid ) )
160 return MakeZeroOid();
161
162 // For repo-level query (empty or "."), just return HEAD
163 if( aPath.empty() || aPath == "." )
164 return head_oid;
165
166 const char* workdir = git_repository_workdir( aRepo );
167
168 if( !workdir )
169 return MakeZeroOid();
170
171 wxFileName file( ResolveEffectivePath( aPath ) );
172
174 file.GetPath(), wxString::FromUTF8( workdir ) );
175
176 if( !file.MakeRelativeTo( base ) )
177 return MakeZeroOid();
178
179 const std::string treePath = file.GetFullPath( wxPATH_UNIX ).ToStdString( wxConvUTF8 );
180
181 if( treePath.empty() || treePath == "." || treePath == ".." || treePath.starts_with( "../" ) )
182 return MakeZeroOid();
183
184 // For file-specific query, walk history to find last commit that touched this file
185 git_revwalk* walker = nullptr;
186
187 if( git_revwalk_new( &walker, aRepo ) != 0 )
188 return MakeZeroOid();
189
190 git_revwalk_sorting( walker, GIT_SORT_TIME );
191
192 if( git_revwalk_push( walker, &head_oid ) != 0 )
193 {
194 git_revwalk_free( walker );
195 return MakeZeroOid();
196 }
197
198 // Walk through commits to find when the file was last modified
199 git_oid result = MakeZeroOid();
200 git_oid commit_oid;
201 git_oid prev_blob_oid = MakeZeroOid();
202 bool first_commit = true;
203
204 while( git_revwalk_next( &commit_oid, walker ) == 0 )
205 {
206 git_commit* commit = nullptr;
207
208 if( git_commit_lookup( &commit, aRepo, &commit_oid ) != 0 )
209 continue;
210
211 // Get the tree for this commit
212 git_tree* tree = nullptr;
213
214 if( git_commit_tree( &tree, commit ) == 0 )
215 {
216 // Try to find the file in this tree
217 git_tree_entry* entry = nullptr;
218
219 if( git_tree_entry_bypath( &entry, tree, treePath.c_str() ) == 0 )
220 {
221 const git_oid* blob_oid = git_tree_entry_id( entry );
222
223 if( first_commit )
224 {
225 // First time we see this file, remember its blob ID
226 git_oid_cpy( &prev_blob_oid, blob_oid );
227 git_oid_cpy( &result, &commit_oid );
228 first_commit = false;
229 }
230 else if( git_oid_cmp( blob_oid, &prev_blob_oid ) != 0 )
231 {
232 // File content changed - previous commit is where it changed
233 git_tree_entry_free( entry );
234 git_tree_free( tree );
235 git_commit_free( commit );
236 break;
237 }
238 else
239 {
240 // File unchanged, keep looking
241 git_oid_cpy( &result, &commit_oid );
242 }
243
244 git_tree_entry_free( entry );
245 }
246 else if( !first_commit )
247 {
248 // File doesn't exist in this commit, but existed before
249 // So the previous commit is where it was added/last modified
250 git_tree_free( tree );
251 git_commit_free( commit );
252 break;
253 }
254
255 git_tree_free( tree );
256 }
257
258 git_commit_free( commit );
259 }
260
261 git_revwalk_free( walker );
262 return result;
263 }
264
265 struct DESCRIBE_INFO
266 {
267 std::string tag;
268 int distance;
269 };
270
271 DESCRIBE_INFO ReadDescribeInfo( const std::string& aMatch, bool aAnyTags )
272 {
273 git_repository* repo = OpenRepo( "." );
274
275 if( !repo )
276 return { std::string(), 0 };
277
278 const git_oid head_oid = KIGIT::PROJECT_GIT_UTILS::GetCapturedHeadOid( repo );
279
280 if( git_oid_is_zero( &head_oid ) )
281 {
282 CloseRepo( repo );
283 return { std::string(), 0 };
284 }
285
286 git_strarray tag_names;
287
288 if( git_tag_list_match( &tag_names, aMatch.empty() ? "*" : aMatch.c_str(), repo ) != 0 )
289 {
290 CloseRepo( repo );
291 return { std::string(), 0 };
292 }
293
294 // Build map of commit OID -> tag name upfront
295 std::map<git_oid, std::string, decltype(
296 [](const git_oid& a, const git_oid& b)
297 {
298 return git_oid_cmp(&a, &b) < 0;
299 } )> commit_to_tag;
300
301 for( size_t i = 0; i < tag_names.count; ++i )
302 {
303 git_object* tag_obj = nullptr;
304
305 if( git_revparse_single( &tag_obj, repo, tag_names.strings[i] ) == 0 )
306 {
307 git_object_t type = git_object_type( tag_obj );
308
309 if( type == GIT_OBJECT_TAG )
310 {
311 git_object* target = nullptr;
312
313 if( git_tag_peel( &target, (git_tag*) tag_obj ) == 0 )
314 {
315 commit_to_tag[*git_object_id( target )] = tag_names.strings[i];
316 git_object_free( target );
317 }
318 }
319 else if( aAnyTags && type == GIT_OBJECT_COMMIT )
320 {
321 commit_to_tag[*git_object_id( tag_obj )] = tag_names.strings[i];
322 }
323
324 git_object_free( tag_obj );
325 }
326 }
327
328 git_strarray_dispose( &tag_names );
329
330 git_revwalk* walker = nullptr;
331
332 if( git_revwalk_new( &walker, repo ) != 0 )
333 {
334 CloseRepo( repo );
335 return { std::string(), 0 };
336 }
337
338 git_revwalk_sorting( walker, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME );
339
340 if( git_revwalk_push( walker, &head_oid ) != 0 )
341 {
342 git_revwalk_free( walker );
343 CloseRepo( repo );
344 return { std::string(), 0 };
345 }
346
347 DESCRIBE_INFO result{ std::string(), 0 };
348 int distance = 0;
349 git_oid commit_oid;
350
351 while( git_revwalk_next( &commit_oid, walker ) == 0 )
352 {
353 auto it = commit_to_tag.find( commit_oid );
354
355 if( it != commit_to_tag.end() )
356 {
357 result.tag = it->second;
358 result.distance = distance;
359 break;
360 }
361
362 distance++;
363 }
364
365 git_revwalk_free( walker );
366 CloseRepo( repo );
367 return result;
368 }
369
370 DESCRIBE_INFO GetDescribeInfo( const std::string& aMatch, bool aAnyTags )
371 {
372 const VCS_VALUE value = Capture( VCS_QUERY::DESCRIPTION, ".", aMatch, aAnyTags,
373 [&]() -> VCS_VALUE
374 {
375 const DESCRIBE_INFO description = ReadDescribeInfo( aMatch, aAnyTags );
376 return { description.tag, description.distance };
377 } );
378 return { value.text, static_cast<int>( value.number ) };
379 }
380
381 std::string ReadCommitSignatureField( const std::string& aPath, bool aUseCommitter, bool aGetEmail )
382 {
383 git_repository* repo = OpenRepo( aPath );
384
385 if( !repo )
386 return std::string();
387
388 git_oid oid = GetFileCommit( repo, aPath );
389
390 if( git_oid_is_zero( &oid ) )
391 {
392 CloseRepo( repo );
393 return std::string();
394 }
395
396 git_commit* commit = nullptr;
397 std::string result;
398
399 if( git_commit_lookup( &commit, repo, &oid ) == 0 )
400 {
401 const git_signature* sig = aUseCommitter ? git_commit_committer( commit ) : git_commit_author( commit );
402
403 if( sig )
404 {
405 const char* field = aGetEmail ? sig->email : sig->name;
406
407 if( field )
408 result = field;
409 }
410
411 git_commit_free( commit );
412 }
413
414 CloseRepo( repo );
415 return result;
416 }
417
418 std::string GetCommitSignatureField( const std::string& aPath, bool aUseCommitter, bool aGetEmail )
419 {
420 return Capture( VCS_QUERY::SIGNATURE, aPath, aPath, ( aUseCommitter ? 2 : 0 ) | ( aGetEmail ? 1 : 0 ),
421 [&]() -> VCS_VALUE
422 {
423 return { ReadCommitSignatureField( aPath, aUseCommitter, aGetEmail ) };
424 } ).text;
425 }
426
427} // anonymous namespace
428
429
430static std::string ReadCommitHash( const std::string& aPath )
431{
432 git_repository* repo = OpenRepo( aPath );
433
434 if( !repo )
435 return std::string();
436
437 git_oid oid = GetFileCommit( repo, aPath );
438
439 if( git_oid_is_zero( &oid ) )
440 {
441 CloseRepo( repo );
442 return std::string();
443 }
444
445 char hash[GIT_OID_HEXSZ + 1];
446 git_oid_tostr( hash, sizeof( hash ), &oid );
447
448 CloseRepo( repo );
449 return hash;
450}
451
452
453std::string GetCommitHash( const std::string& aPath, int aLength )
454{
455 const VCS_VALUE value = Capture( VCS_QUERY::HASH, aPath, aPath, 0,
456 [&]() -> VCS_VALUE
457 {
458 return { ReadCommitHash( aPath ) };
459 } );
460 return value.text.substr( 0, std::clamp( aLength, 4, GIT_OID_HEXSZ ) );
461}
462
463
464std::string GetNearestTag( const std::string& aMatch, bool aAnyTags )
465{
466 return GetDescribeInfo( aMatch, aAnyTags ).tag;
467}
468
469
470int GetDistanceFromTag( const std::string& aMatch, bool aAnyTags )
471{
472 return GetDescribeInfo( aMatch, aAnyTags ).distance;
473}
474
475
476static bool ReadIsDirty( bool aIncludeUntracked )
477{
478 git_repository* repo = OpenRepo( "." );
479
480 if( !repo )
481 return false;
482
483 git_status_list* status = nullptr;
484 git_status_options statusOpts;
485 git_status_options_init( &statusOpts, GIT_STATUS_OPTIONS_VERSION );
486
487 statusOpts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
488 statusOpts.flags = aIncludeUntracked ? GIT_STATUS_OPT_INCLUDE_UNTRACKED : GIT_STATUS_OPT_EXCLUDE_SUBMODULES;
489
490 bool isDirty = false;
491
492 if( git_status_list_new( &status, repo, &statusOpts ) == 0 )
493 {
494 isDirty = git_status_list_entrycount( status ) > 0;
495 git_status_list_free( status );
496 }
497
498 CloseRepo( repo );
499 return isDirty;
500}
501
502
503bool IsDirty( bool aIncludeUntracked )
504{
505 return Capture( VCS_QUERY::DIRTY, ".", "", aIncludeUntracked,
506 [&]() -> VCS_VALUE
507 {
508 return { {}, ReadIsDirty( aIncludeUntracked ) };
509 } ).number != 0;
510}
511
512
513std::string GetAuthor( const std::string& aPath )
514{
515 return GetCommitSignatureField( aPath, false, false );
516}
517
518
519std::string GetAuthorEmail( const std::string& aPath )
520{
521 return GetCommitSignatureField( aPath, false, true );
522}
523
524
525std::string GetCommitter( const std::string& aPath )
526{
527 return GetCommitSignatureField( aPath, true, false );
528}
529
530
531std::string GetCommitterEmail( const std::string& aPath )
532{
533 return GetCommitSignatureField( aPath, true, true );
534}
535
536
537static std::string ReadBranch()
538{
539 git_repository* repo = OpenRepo( "." );
540
541 if( !repo )
542 return std::string();
543
544 KIGIT_COMMON common( repo );
545 wxString branchName = common.GetCurrentBranchName();
546
547 CloseRepo( repo );
548 return branchName.ToStdString();
549}
550
551
552std::string GetBranch()
553{
554 return Capture( VCS_QUERY::BRANCH, ".", "", 0,
555 []() -> VCS_VALUE
556 {
557 return { ReadBranch() };
558 } ).text;
559}
560
561
562static int64_t ReadCommitTimestamp( const std::string& aPath )
563{
564 git_repository* repo = OpenRepo( aPath );
565
566 if( !repo )
567 return 0;
568
569 git_oid oid = GetFileCommit( repo, aPath );
570
571 if( git_oid_is_zero( &oid ) )
572 {
573 CloseRepo( repo );
574 return 0;
575 }
576
577 git_commit* commit = nullptr;
578 int64_t timestamp = 0;
579
580 if( git_commit_lookup( &commit, repo, &oid ) == 0 )
581 {
582 timestamp = static_cast<int64_t>( git_commit_time( commit ) );
583 git_commit_free( commit );
584 }
585
586 CloseRepo( repo );
587 return timestamp;
588}
589
590
591int64_t GetCommitTimestamp( const std::string& aPath )
592{
593 return Capture( VCS_QUERY::TIMESTAMP, aPath, aPath, 0,
594 [&]() -> VCS_VALUE
595 {
596 return { {}, ReadCommitTimestamp( aPath ) };
597 } ).number;
598}
599
600
601std::string GetCommitDate( const std::string& aPath )
602{
603 int64_t timestamp = GetCommitTimestamp( aPath );
604 return timestamp > 0 ? std::to_string( timestamp ) : std::string();
605}
606
607
609{
610 const auto read =
611 [&]() -> VCS_VALUE
612 {
613 const auto& [query, path, argument, options, contextFile] = aKey;
614
615 if( query == VCS_QUERY::HEAD )
616 {
617 if( !GetGitBackend() )
618 return {};
619
620 git_repository* raw = nullptr;
621
622 // Discovery could select the main repository instead of this linked worktree's HEAD.
623 if( git_repository_open( &raw, path.ToUTF8().data() ) != 0 )
624 return {};
625
626 KIGIT::GitRepositoryPtr repo( raw );
627 git_oid oid{};
628
629 if( git_reference_name_to_id( &oid, repo.get(), "HEAD" ) != 0 )
630 return {};
631
632 char hash[GIT_OID_HEXSZ + 1];
633 git_oid_tostr( hash, sizeof( hash ), &oid );
634 return { hash };
635 }
636
637 const bool fileQuery = ( query == VCS_QUERY::HASH
638 || query == VCS_QUERY::SIGNATURE
639 || query == VCS_QUERY::TIMESTAMP )
640 && !argument.empty()
641 && argument != ".";
642 const CONTEXT_PATH_SCOPE context( fileQuery || contextFile ? wxFileName( path ).GetPath() : path );
643 const std::string file = fileQuery ? path.ToStdString( wxConvUTF8 ) : std::string();
644
645 switch( query )
646 {
647 case VCS_QUERY::HASH:
648 return { ReadCommitHash( file ) };
649
650 case VCS_QUERY::DESCRIPTION:
651 {
652 const DESCRIBE_INFO description = ReadDescribeInfo( argument, options != 0 );
653 return { description.tag, description.distance };
654 }
655
656 case VCS_QUERY::SIGNATURE:
657 return { ReadCommitSignatureField( file, ( options & 2 ) != 0, ( options & 1 ) != 0 ) };
658
659 case VCS_QUERY::BRANCH:
660 return { ReadBranch() };
661
662 case VCS_QUERY::DIRTY:
663 return { {}, ReadIsDirty( options != 0 ) };
664
665 case VCS_QUERY::TIMESTAMP:
666 return { {}, ReadCommitTimestamp( file ) };
667
668 case VCS_QUERY::HEAD:
669 break;
670 }
671
672 return {};
673 };
674
676 return environment->VcsValue( aKey, read );
677
678 return read();
679}
680
681} // 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
A text evaluation frame with one frozen clock and memoized external queries.
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()
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
Definition idf_helpers.h:37
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)
std::string path
wxString result
Test unit parsing edge cases and error handling.