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