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