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