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 along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
22#include <git/git_backend.h>
25#include <string_utils.h>
26#include <wx/string.h>
27#include <wx/arrstr.h> // REQUIRED for wxString vector export on MSVC
28#include <map>
29
31{
32// Per-thread override that anchors repo-scoped queries to a specific path (for example the
33// loaded project directory). When empty, repo discovery falls back to the process cwd.
34namespace
35{
36 thread_local wxString tl_contextPath;
37}
38
39
40void SetContextPath( const wxString& aPath )
41{
42 tl_contextPath = aPath;
43}
44
45
47{
48 return tl_contextPath.IsEmpty() ? wxString( wxT( "." ) ) : tl_contextPath;
49}
50
51
53 m_previous( tl_contextPath )
54{
55 tl_contextPath = aPath;
56}
57
58
63
64
65// Private implementation details
66namespace
67{
68 // Resolve the effective path for repo discovery. Inputs of "." are replaced with the
69 // current context path (which itself falls back to ".").
70 wxString ResolveEffectivePath( const std::string& aPath )
71 {
72 if( aPath.empty() || aPath == "." )
73 return GetContextPath();
74
75 return wxString::FromUTF8( aPath );
76 }
77
78
79 git_repository* OpenRepo( const std::string& aPath )
80 {
81 if( !GetGitBackend() )
82 return nullptr;
83
84 const wxString effective = ResolveEffectivePath( aPath );
86 }
87
88 void CloseRepo( git_repository* aRepo )
89 {
90 if( aRepo )
91 git_repository_free( aRepo );
92 }
93
94 git_oid MakeZeroOid()
95 {
96 git_oid oid;
97 git_oid_fromstrn( &oid, "0000000000000000000000000000000000000000", 40 );
98 return oid;
99 }
100
101 git_oid GetFileCommit( git_repository* aRepo, const std::string& aPath )
102 {
103 if( !aRepo )
104 return MakeZeroOid();
105
106 // Get HEAD commit
107 git_oid head_oid;
108
109 if( git_reference_name_to_id( &head_oid, aRepo, "HEAD" ) != 0 )
110 return MakeZeroOid();
111
112 // For repo-level query (empty or "."), just return HEAD
113 if( aPath.empty() || aPath == "." )
114 return head_oid;
115
116 // For file-specific query, walk history to find last commit that touched this file
117 git_revwalk* walker = nullptr;
118
119 if( git_revwalk_new( &walker, aRepo ) != 0 )
120 return MakeZeroOid();
121
122 git_revwalk_sorting( walker, GIT_SORT_TIME );
123 git_revwalk_push( walker, &head_oid );
124
125 // Walk through commits to find when the file was last modified
126 git_oid result = MakeZeroOid();
127 git_oid commit_oid;
128 git_oid prev_blob_oid = MakeZeroOid();
129 bool first_commit = true;
130
131 while( git_revwalk_next( &commit_oid, walker ) == 0 )
132 {
133 git_commit* commit = nullptr;
134
135 if( git_commit_lookup( &commit, aRepo, &commit_oid ) != 0 )
136 continue;
137
138 // Get the tree for this commit
139 git_tree* tree = nullptr;
140
141 if( git_commit_tree( &tree, commit ) == 0 )
142 {
143 // Try to find the file in this tree
144 git_tree_entry* entry = nullptr;
145
146 if( git_tree_entry_bypath( &entry, tree, aPath.c_str() ) == 0 )
147 {
148 const git_oid* blob_oid = git_tree_entry_id( entry );
149
150 if( first_commit )
151 {
152 // First time we see this file, remember its blob ID
153 git_oid_cpy( &prev_blob_oid, blob_oid );
154 git_oid_cpy( &result, &commit_oid );
155 first_commit = false;
156 }
157 else if( git_oid_cmp( blob_oid, &prev_blob_oid ) != 0 )
158 {
159 // File content changed - previous commit is where it changed
160 git_tree_entry_free( entry );
161 git_tree_free( tree );
162 git_commit_free( commit );
163 break;
164 }
165 else
166 {
167 // File unchanged, keep looking
168 git_oid_cpy( &result, &commit_oid );
169 }
170
171 git_tree_entry_free( entry );
172 }
173 else if( !first_commit )
174 {
175 // File doesn't exist in this commit, but existed before
176 // So the previous commit is where it was added/last modified
177 git_tree_free( tree );
178 git_commit_free( commit );
179 break;
180 }
181
182 git_tree_free( tree );
183 }
184
185 git_commit_free( commit );
186 }
187
188 git_revwalk_free( walker );
189 return result;
190 }
191
192 struct DescribeInfo
193 {
194 std::string tag;
195 int distance;
196 };
197
198 DescribeInfo GetDescribeInfo( const std::string& aMatch, bool aAnyTags )
199 {
200 git_repository* repo = OpenRepo( "." );
201
202 if( !repo )
203 return { std::string(), 0 };
204
205 git_oid head_oid;
206
207 if( git_reference_name_to_id( &head_oid, repo, "HEAD" ) != 0 )
208 {
209 CloseRepo( repo );
210 return { std::string(), 0 };
211 }
212
213 git_strarray tag_names;
214
215 if( git_tag_list_match( &tag_names, aMatch.empty() ? "*" : aMatch.c_str(), repo ) != 0 )
216 {
217 CloseRepo( repo );
218 return { std::string(), 0 };
219 }
220
221 // Build map of commit OID -> tag name upfront
222 std::map<git_oid, std::string, decltype(
223 [](const git_oid& a, const git_oid& b)
224 {
225 return git_oid_cmp(&a, &b) < 0;
226 } )> commit_to_tag;
227
228 for( size_t i = 0; i < tag_names.count; ++i )
229 {
230 git_object* tag_obj = nullptr;
231
232 if( git_revparse_single( &tag_obj, repo, tag_names.strings[i] ) == 0 )
233 {
234 git_object_t type = git_object_type( tag_obj );
235
236 if( type == GIT_OBJECT_TAG )
237 {
238 git_object* target = nullptr;
239
240 if( git_tag_peel( &target, (git_tag*) tag_obj ) == 0 )
241 {
242 commit_to_tag[*git_object_id( target )] = tag_names.strings[i];
243 git_object_free( target );
244 }
245 }
246 else if( aAnyTags && type == GIT_OBJECT_COMMIT )
247 {
248 commit_to_tag[*git_object_id( tag_obj )] = tag_names.strings[i];
249 }
250
251 git_object_free( tag_obj );
252 }
253 }
254
255 git_strarray_dispose( &tag_names );
256
257 git_revwalk* walker = nullptr;
258
259 if( git_revwalk_new( &walker, repo ) != 0 )
260 {
261 CloseRepo( repo );
262 return { std::string(), 0 };
263 }
264
265 git_revwalk_sorting( walker, GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME );
266 git_revwalk_push( walker, &head_oid );
267
268 DescribeInfo result{ std::string(), 0 };
269 int distance = 0;
270 git_oid commit_oid;
271
272 while( git_revwalk_next( &commit_oid, walker ) == 0 )
273 {
274 auto it = commit_to_tag.find( commit_oid );
275
276 if( it != commit_to_tag.end() )
277 {
278 result.tag = it->second;
279 result.distance = distance;
280 break;
281 }
282
283 distance++;
284 }
285
286 git_revwalk_free( walker );
287 CloseRepo( repo );
288 return result;
289 }
290
291 std::string GetCommitSignatureField( const std::string& aPath, bool aUseCommitter, bool aGetEmail )
292 {
293 git_repository* repo = OpenRepo( aPath );
294
295 if( !repo )
296 return std::string();
297
298 git_oid oid = GetFileCommit( repo, aPath );
299
300 if( git_oid_is_zero( &oid ) )
301 {
302 CloseRepo( repo );
303 return std::string();
304 }
305
306 git_commit* commit = nullptr;
307 std::string result;
308
309 if( git_commit_lookup( &commit, repo, &oid ) == 0 )
310 {
311 const git_signature* sig = aUseCommitter ? git_commit_committer( commit ) : git_commit_author( commit );
312
313 if( sig )
314 {
315 const char* field = aGetEmail ? sig->email : sig->name;
316
317 if( field )
318 result = field;
319 }
320
321 git_commit_free( commit );
322 }
323
324 CloseRepo( repo );
325 return result;
326 }
327
328} // anonymous namespace
329
330
331std::string GetCommitHash( const std::string& aPath, int aLength )
332{
333 git_repository* repo = OpenRepo( aPath );
334
335 if( !repo )
336 return std::string();
337
338 git_oid oid = GetFileCommit( repo, aPath );
339
340 if( git_oid_is_zero( &oid ) )
341 {
342 CloseRepo( repo );
343 return std::string();
344 }
345
346 int length = std::max( 4, std::min( aLength, GIT_OID_HEXSZ ) );
347 char hash[GIT_OID_HEXSZ + 1];
348 git_oid_tostr( hash, length + 1, &oid );
349
350 CloseRepo( repo );
351 return hash;
352}
353
354
355std::string GetNearestTag( const std::string& aMatch, bool aAnyTags )
356{
357 return GetDescribeInfo( aMatch, aAnyTags ).tag;
358}
359
360
361int GetDistanceFromTag( const std::string& aMatch, bool aAnyTags )
362{
363 return GetDescribeInfo( aMatch, aAnyTags ).distance;
364}
365
366
367bool IsDirty( bool aIncludeUntracked )
368{
369 git_repository* repo = OpenRepo( "." );
370
371 if( !repo )
372 return false;
373
374 git_status_list* status = nullptr;
375 git_status_options statusOpts;
376 git_status_options_init( &statusOpts, GIT_STATUS_OPTIONS_VERSION );
377
378 statusOpts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
379 statusOpts.flags = aIncludeUntracked ? GIT_STATUS_OPT_INCLUDE_UNTRACKED : GIT_STATUS_OPT_EXCLUDE_SUBMODULES;
380
381 bool isDirty = false;
382
383 if( git_status_list_new( &status, repo, &statusOpts ) == 0 )
384 {
385 isDirty = git_status_list_entrycount( status ) > 0;
386 git_status_list_free( status );
387 }
388
389 CloseRepo( repo );
390 return isDirty;
391}
392
393
394std::string GetAuthor( const std::string& aPath )
395{
396 return GetCommitSignatureField( aPath, false, false );
397}
398
399
400std::string GetAuthorEmail( const std::string& aPath )
401{
402 return GetCommitSignatureField( aPath, false, true );
403}
404
405
406std::string GetCommitter( const std::string& aPath )
407{
408 return GetCommitSignatureField( aPath, true, false );
409}
410
411
412std::string GetCommitterEmail( const std::string& aPath )
413{
414 return GetCommitSignatureField( aPath, true, true );
415}
416
417
418std::string GetBranch()
419{
420 git_repository* repo = OpenRepo( "." );
421
422 if( !repo )
423 return std::string();
424
425 KIGIT_COMMON common( repo );
426 wxString branchName = common.GetCurrentBranchName();
427
428 CloseRepo( repo );
429 return branchName.ToStdString();
430}
431
432
433int64_t GetCommitTimestamp( const std::string& aPath )
434{
435 git_repository* repo = OpenRepo( aPath );
436
437 if( !repo )
438 return 0;
439
440 git_oid oid = GetFileCommit( repo, aPath );
441
442 if( git_oid_is_zero( &oid ) )
443 {
444 CloseRepo( repo );
445 return 0;
446 }
447
448 git_commit* commit = nullptr;
449 int64_t timestamp = 0;
450
451 if( git_commit_lookup( &commit, repo, &oid ) == 0 )
452 {
453 timestamp = static_cast<int64_t>( git_commit_time( commit ) );
454 git_commit_free( commit );
455 }
456
457 CloseRepo( repo );
458 return timestamp;
459}
460
461
462std::string GetCommitDate( const std::string& aPath )
463{
464 int64_t timestamp = GetCommitTimestamp( aPath );
465 return timestamp > 0 ? std::to_string( timestamp ) : std::string();
466}
467
468} // namespace TEXT_EVAL_VCS
wxString GetCommitHash()
Get the commit hash as a string.
static git_repository * GetRepositoryForFile(const char *aFilename)
Discover and open the repository that contains the given file.
wxString GetCurrentBranchName() const
CONTEXT_PATH_SCOPE(const wxString &aPath)
GIT_BACKEND * GetGitBackend()
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.
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 as the repository-discovery starting point for repo-scoped VCS 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.
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.
wxString result
Test unit parsing edge cases and error handling.