KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_kigit_common.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
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU 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
21
24
25#include <git2.h>
26
27#include <wx/filename.h>
28#include <wx/string.h>
29#include <wx/stdpaths.h>
30
31#include <chrono>
32#include <fstream>
33#include <memory>
34#include <cstdlib>
35
36
37namespace
38{
39
40// Throwaway keys, generated for these tests and used nowhere else.
41
42const char* c_opensshEncrypted = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
43 "b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAxS2G8mY\n"
44 "+85LKkVcFtJi1GAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKB0oSTjiyvxC+qh\n"
45 "cJ2zEnPDzZWArHcl90v2Se1SkmoGAAAAkKKrIYipMtR+JUEsuoYSmnfD0b/ng57Fq2uj25\n"
46 "oHDRqrDyPKS0xqQJa9+IQvPeM+E3G1fAGvsOYTQXsLJzXuQDiLX3vceEuN9AmJkA9Naea4\n"
47 "qAPy8CTlBx/ktdd4YgnYXBETi6Ab1f/h7LAVlLJTOc9Ta9HilKzO8Vl5p3EUS1PWkfGGqe\n"
48 "QLo1/ryfJjC3ZZbA==\n"
49 "-----END OPENSSH PRIVATE KEY-----\n";
50
51
52const char* c_opensshPlain = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
53 "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n"
54 "QyNTUxOQAAACD4Xd9qfxjO+vpBXO94Otoa0kg/p5H8kMYhN6eFe7v8MgAAAIjYZhWO2GYV\n"
55 "jgAAAAtzc2gtZWQyNTUxOQAAACD4Xd9qfxjO+vpBXO94Otoa0kg/p5H8kMYhN6eFe7v8Mg\n"
56 "AAAEBNlyz61Kiufy1a3WO6i2I7vB+7W2iQc8R81H0qrBheSfhd32p/GM76+kFc73g62hrS\n"
57 "SD+nkfyQxiE3p4V7u/wyAAAAAXQBAgME\n"
58 "-----END OPENSSH PRIVATE KEY-----\n";
59
60
61const char* c_opensshTruncated = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
62 "b3BlbnNzaC1r\n"
63 "-----END OPENSSH PRIVATE KEY-----\n";
64
65
66const char* c_pemEncrypted = "-----BEGIN EC PRIVATE KEY-----\n"
67 "Proc-Type: 4,ENCRYPTED\n"
68 "DEK-Info: AES-128-CBC,10297828DB5031A5C22276D18E077B61\n"
69 "\n"
70 "rHfBqMZtByvcsRb7zgNtkSjhFgnRY3jpyyagjM8jxNoEDRaSSHNSugON80psxytE\n"
71 "HBTGShcJbDXOC7EER7QZOTjM/d+3DB+FEL4EwnJxgnVfWN/5p9hP3CoJLgeItkQV\n"
72 "POqt3mQ1V8AnJibunyn13YrczHM39IFruBB9JvNKKG+i/7yf4Jy4jCB9nicH7TC+\n"
73 "hQCu/mOMde02ziwULJiWIU3EALSYo6JhDJxTRIqUr7TDGXEAPcxpQEvMDz+HAgzY\n"
74 "9zTU6LC/CSbHQNjHYSvgDY4zRDFWxeHgNeQVfqrlOPGGE8NC5HCeo5i1r+rSVPn+\n"
75 "kzqXcGpwN682lM/oJV4ix3her0jf59RnAPKYwyWXfhUwYFtzFwroOoquFi6+HHVT\n"
76 "LlzJacbVVj/GTBOdHBSsu8cfiVLymHhEEzQCyIYY05DeHFe8GiSwDDdvMCPppX38\n"
77 "/R3PnNaEYwOEUK4o50nsWEfPHQw8kZaUkiUcX7uIyV4=\n"
78 "-----END EC PRIVATE KEY-----\n";
79
80
81const char* c_pemPlain = "-----BEGIN EC PRIVATE KEY-----\n"
82 "MIIBaAIBAQQgGuajOowPzHaTJKkj1W+RQS0p87K8bEkZbgbw5pmVArmggfowgfcC\n"
83 "AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////\n"
84 "MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr\n"
85 "vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE\n"
86 "axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W\n"
87 "K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8\n"
88 "YyVRAgEBoUQDQgAELhPMgsc2xVYFBrKzqXq8CBRS8t9BBCnznM96FcZFv1MvdbIK\n"
89 "yn1nix57Bwq0VoyooErjaZ3SpXl47JlJpIsm4A==\n"
90 "-----END EC PRIVATE KEY-----\n";
91
92
93const char* c_pkcs8Encrypted = "-----BEGIN ENCRYPTED PRIVATE KEY-----\n"
94 "MIIBzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQImaRUWsA/SFECAggA\n"
95 "MB0GCWCGSAFlAwQBAgQQiywEwYp7En3NzOemv0ek9QSCAYAIh56t3GB2r1S/B7Cy\n"
96 "UFS4qn1DvGDuhubEl6cw3n8Y1PnWj6ryq6O9Pw/DKdnKonzVN+PVaofXg26ZPLdR\n"
97 "q+J7fmcMPIEC0DqV/6T+oGqWp2iaiTZ11BBOoyBkd8ev59c/TxngOWlbJvvmGhEF\n"
98 "mOiCwXKoKjzU8kSVbmL+vFe1thZjrCaihL79JsZE6Ws5bd7yAn6grPBPI8sBexNi\n"
99 "fERG81T9Nu2mxCEd7zkNAoK0cZBnUwFvxDama18wMvXRYzEHETCUCWevoNFdcwo/\n"
100 "IkGgLciMxtSBBOXyWiVvAzTloSGHa5wafptTsgyTc1HKZBTR0ZTNvBX37aziggWE\n"
101 "u4fMHcHlAJwP4UXgCACtgcfktVhjohgNQdBm5RB95qEUdNAQDQSe4Ro27HfT0YyR\n"
102 "AHZYNXziRjZHsO6wqGarcxdZkH1Cy8Ca9FJr3uRWWr+8CWIjE9nDEkFbXUyQGOPq\n"
103 "sTAC7FpnQHVczHcaa45jXYaPmU/iNFnPkO2DzxWADe/1xIg=\n"
104 "-----END ENCRYPTED PRIVATE KEY-----\n";
105
106
107struct GitInitGuard
108{
109 GitInitGuard() { git_libgit2_init(); }
110 ~GitInitGuard() { git_libgit2_shutdown(); }
111};
112
113
114struct ScopedTempDir
115{
116 wxString path;
117
119 {
120 auto now = std::chrono::steady_clock::now().time_since_epoch();
121 long long ticks = std::chrono::duration_cast<std::chrono::nanoseconds>( now ).count();
122
123 wxString base = wxFileName::GetTempDir();
124 wxFileName fn;
125 fn.AssignDir( base );
126 fn.AppendDir( wxString::Format( "kicad-qa-git-%lld-%p", ticks, this ) );
127 wxFileName::Mkdir( fn.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
128
129#ifdef __WXMAC__
130 // Store the realpath-resolved form. libgit2 canonicalizes the repo workdir with
131 // realpath(3), so on macOS -- where /var is a symlink to /private/var -- any path
132 // derived from the repo differs textually from the one we built above unless we
133 // resolve symlinks here too. Comparisons against GetGitRootDirectory() rely on this.
134 char* resolved = realpath( fn.GetPath().utf8_string().c_str(), nullptr );
135
136 if( resolved )
137 {
138 path = wxString::FromUTF8( resolved );
139 free( resolved );
140 }
141 else
142 {
143 path = fn.GetPath();
144 }
145#else
146 path = fn.GetPath();
147#endif
148 }
149
151 {
152 if( !path.IsEmpty() )
153 wxFileName::Rmdir( path, wxPATH_RMDIR_RECURSIVE );
154 }
155};
156
157
158// Initialize a git repository at the given path with one initial commit on "main"
159git_repository* makeRepoWithCommit( const wxString& aRepoPath, const wxString& aFileName,
160 const std::string& aFileContents )
161{
162 git_repository* repo = nullptr;
163 git_repository_init_options init_opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
164 init_opts.flags = GIT_REPOSITORY_INIT_MKPATH;
165 init_opts.initial_head = "main";
166
167 BOOST_REQUIRE_EQUAL( git_repository_init_ext( &repo, aRepoPath.utf8_string().c_str(),
168 &init_opts ),
169 GIT_OK );
170
171 // Write a file
172 wxFileName filePath( aRepoPath, aFileName );
173 {
174 std::ofstream f( filePath.GetFullPath().utf8_string() );
175 f << aFileContents;
176 }
177
178 // Configure user
179 git_config* cfg = nullptr;
180 BOOST_REQUIRE_EQUAL( git_repository_config( &cfg, repo ), GIT_OK );
181 KIGIT::GitConfigPtr cfgPtr( cfg );
182 git_config_set_string( cfg, "user.name", "QA Test" );
183 git_config_set_string( cfg, "user.email", "[email protected]" );
184
185 // Stage and commit
186 git_index* index = nullptr;
187 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
188 KIGIT::GitIndexPtr indexPtr( index );
189 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, aFileName.utf8_string().c_str() ), GIT_OK );
190 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
191
192 git_oid tree_oid;
193 BOOST_REQUIRE_EQUAL( git_index_write_tree( &tree_oid, index ), GIT_OK );
194
195 git_tree* tree = nullptr;
196 BOOST_REQUIRE_EQUAL( git_tree_lookup( &tree, repo, &tree_oid ), GIT_OK );
197 KIGIT::GitTreePtr treePtr( tree );
198
199 git_signature* sig = nullptr;
200 BOOST_REQUIRE_EQUAL( git_signature_now( &sig, "QA Test", "[email protected]" ), GIT_OK );
201 KIGIT::GitSignaturePtr sigPtr( sig );
202
203 git_oid commit_oid;
204 BOOST_REQUIRE_EQUAL( git_commit_create( &commit_oid, repo, "HEAD", sig, sig, nullptr,
205 "initial", tree, 0, nullptr ),
206 GIT_OK );
207
208 return repo;
209}
210
211} // namespace
212
213
214BOOST_AUTO_TEST_SUITE( KiGitCommon )
215
216
217// GetGitRootDirectory must return the working directory (project root) and not the
218// internal .git folder. Older code returned git_repository_path() which produced a path
219// ending in /.git/, breaking the version-control popup-menu detection logic for whether a
220// project lives at the repository root.
221BOOST_AUTO_TEST_CASE( GitRootDirectoryReturnsWorkdir )
222{
223 GitInitGuard libgit;
224 ScopedTempDir tmp;
225
226 git_repository* repo = makeRepoWithCommit( tmp.path, "file.txt", "data\n" );
227 KIGIT::GitRepositoryPtr repoPtr( repo );
228
229 KIGIT_COMMON common( repo );
230 wxString root = common.GetGitRootDirectory();
231
232 BOOST_CHECK( !root.IsEmpty() );
233 BOOST_CHECK_MESSAGE( !root.Contains( wxS( "/.git" ) ) && !root.Contains( wxS( "\\.git" ) ),
234 "GetGitRootDirectory should return the working directory, got: "
235 + root.ToStdString() );
236
237 // Should be the repo path (with trailing separator).
238 wxFileName rootFn;
239 rootFn.AssignDir( root );
240 rootFn.MakeAbsolute();
241 wxFileName tmpFn;
242 tmpFn.AssignDir( tmp.path );
243 tmpFn.MakeAbsolute();
244 BOOST_CHECK_EQUAL( rootFn.GetFullPath().ToStdString(), tmpFn.GetFullPath().ToStdString() );
245}
246
247
248// HasPushAndPullRemote must report true for repositories whose remote uses a name other
249// than "origin", because libgit2 allows arbitrary remote names and many users rename or
250// add additional remotes.
251BOOST_AUTO_TEST_CASE( HasPushAndPullRemoteAcceptsNonOriginName )
252{
253 GitInitGuard libgit;
254 ScopedTempDir tmp;
255
256 git_repository* repo = makeRepoWithCommit( tmp.path, "file.txt", "data\n" );
257 KIGIT::GitRepositoryPtr repoPtr( repo );
258
259 // No remotes configured.
260 KIGIT_COMMON common( repo );
261 BOOST_CHECK( !common.HasPushAndPullRemote() );
262
263 // Add a remote with a non-default name.
264 git_remote* remote = nullptr;
265 BOOST_REQUIRE_EQUAL( git_remote_create( &remote, repo, "github",
266 "[email protected]:example/repo.git" ),
267 GIT_OK );
268 KIGIT::GitRemotePtr remotePtr( remote );
269
270 BOOST_CHECK( common.HasPushAndPullRemote() );
271}
272
273
274BOOST_AUTO_TEST_CASE( HasPushAndPullRemoteFindsOrigin )
275{
276 GitInitGuard libgit;
277 ScopedTempDir tmp;
278
279 git_repository* repo = makeRepoWithCommit( tmp.path, "file.txt", "data\n" );
280 KIGIT::GitRepositoryPtr repoPtr( repo );
281
282 git_remote* remote = nullptr;
283 BOOST_REQUIRE_EQUAL( git_remote_create( &remote, repo, "origin",
284 "[email protected]:repo.git" ),
285 GIT_OK );
286 KIGIT::GitRemotePtr remotePtr( remote );
287
288 KIGIT_COMMON common( repo );
289 BOOST_CHECK( common.HasPushAndPullRemote() );
290}
291
292
293// GetDifferentFiles previously walked unbounded history when no upstream OID was available
294// and dumped every file in the root commit's tree into the modified set, falsely flagging
295// every file as ahead of the remote. With no upstream configured, both sets must be empty.
296BOOST_AUTO_TEST_CASE( GetDifferentFilesEmptyWithoutUpstream )
297{
298 GitInitGuard libgit;
299 ScopedTempDir tmp;
300
301 git_repository* repo = makeRepoWithCommit( tmp.path, "file.txt", "data\n" );
302 KIGIT::GitRepositoryPtr repoPtr( repo );
303
304 KIGIT_COMMON common( repo );
305 auto [local, remote] = common.GetDifferentFiles();
306
307 BOOST_CHECK_MESSAGE( local.empty(),
308 "Expected no AHEAD files when no upstream is configured, got "
309 + std::to_string( local.size() ) );
310 BOOST_CHECK_MESSAGE( remote.empty(),
311 "Expected no BEHIND files when no upstream is configured, got "
312 + std::to_string( remote.size() ) );
313}
314
315
316// Regression test for the root-commit dump that this commit fixes. Set up an upstream
317// tracking ref whose history is disjoint from the local HEAD's history (separate root
318// commits, no shared ancestors). Pre-fix, get_modified_files() walked the local history
319// past the unrelated upstream and reached the local root commit. Because the root commit
320// has no parent, the code dumped *every* file in its tree into the AHEAD set, including
321// files like "untouched.txt" that were neither created nor modified relative to the
322// upstream's view. The fix bails out before we can include these phantom changes.
323BOOST_AUTO_TEST_CASE( GetDifferentFilesHandlesUnrelatedHistories )
324{
325 GitInitGuard libgit;
326 ScopedTempDir tmp;
327
328 // Local root commit with two files. The bug previously listed both as AHEAD.
329 git_repository* repo = makeRepoWithCommit( tmp.path, "untouched.txt", "stable\n" );
330 KIGIT::GitRepositoryPtr repoPtr( repo );
331
332 // Add a second file to local HEAD so we have more than the bare initial tree.
333 {
334 wxFileName extra( tmp.path, wxS( "second.txt" ) );
335 std::ofstream f( extra.GetFullPath().utf8_string() );
336 f << "more\n";
337 }
338
339 git_index* index = nullptr;
340 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
341 KIGIT::GitIndexPtr indexPtr( index );
342 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, "second.txt" ), GIT_OK );
343 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
344
345 git_oid newTreeOid;
346 BOOST_REQUIRE_EQUAL( git_index_write_tree( &newTreeOid, index ), GIT_OK );
347
348 git_tree* newTree = nullptr;
349 BOOST_REQUIRE_EQUAL( git_tree_lookup( &newTree, repo, &newTreeOid ), GIT_OK );
350 KIGIT::GitTreePtr newTreePtr( newTree );
351
352 git_reference* head = nullptr;
353 BOOST_REQUIRE_EQUAL( git_repository_head( &head, repo ), GIT_OK );
354 KIGIT::GitReferencePtr headPtr( head );
355
356 git_commit* parentCommit = nullptr;
357 BOOST_REQUIRE_EQUAL(
358 git_commit_lookup( &parentCommit, repo, git_reference_target( head ) ), GIT_OK );
359 KIGIT::GitCommitPtr parentCommitPtr( parentCommit );
360
361 git_signature* sig = nullptr;
362 BOOST_REQUIRE_EQUAL( git_signature_now( &sig, "QA Test", "[email protected]" ), GIT_OK );
363 KIGIT::GitSignaturePtr sigPtr( sig );
364
365 const git_commit* parents[1] = { parentCommit };
366 git_oid secondCommitOid;
367 BOOST_REQUIRE_EQUAL( git_commit_create( &secondCommitOid, repo, "HEAD", sig, sig, nullptr,
368 "second", newTree, 1, parents ),
369 GIT_OK );
370
371 // Build a disjoint upstream history with its own root commit and a different file.
372 git_treebuilder* tb = nullptr;
373 BOOST_REQUIRE_EQUAL( git_treebuilder_new( &tb, repo, nullptr ), GIT_OK );
374
375 git_oid blobOid;
376 const std::string upstreamData = "upstream\n";
377 BOOST_REQUIRE_EQUAL( git_blob_create_from_buffer( &blobOid, repo, upstreamData.data(),
378 upstreamData.size() ),
379 GIT_OK );
380 BOOST_REQUIRE_EQUAL( git_treebuilder_insert( nullptr, tb, "remote_only.txt", &blobOid,
381 GIT_FILEMODE_BLOB ),
382 GIT_OK );
383
384 git_oid remoteTreeOid;
385 BOOST_REQUIRE_EQUAL( git_treebuilder_write( &remoteTreeOid, tb ), GIT_OK );
386 git_treebuilder_free( tb );
387
388 git_tree* remoteTree = nullptr;
389 BOOST_REQUIRE_EQUAL( git_tree_lookup( &remoteTree, repo, &remoteTreeOid ), GIT_OK );
390 KIGIT::GitTreePtr remoteTreePtr( remoteTree );
391
392 git_oid remoteCommitOid;
393 BOOST_REQUIRE_EQUAL( git_commit_create( &remoteCommitOid, repo, nullptr, sig, sig, nullptr,
394 "upstream root", remoteTree, 0, nullptr ),
395 GIT_OK );
396
397 git_reference* remoteRef = nullptr;
398 BOOST_REQUIRE_EQUAL( git_reference_create( &remoteRef, repo, "refs/remotes/origin/main",
399 &remoteCommitOid, true, nullptr ),
400 GIT_OK );
401 KIGIT::GitReferencePtr remoteRefPtr( remoteRef );
402
403 git_config* cfg = nullptr;
404 BOOST_REQUIRE_EQUAL( git_repository_config( &cfg, repo ), GIT_OK );
405 KIGIT::GitConfigPtr cfgPtr( cfg );
406 BOOST_REQUIRE_EQUAL( git_config_set_string( cfg, "branch.main.remote", "origin" ),
407 GIT_OK );
408 BOOST_REQUIRE_EQUAL( git_config_set_string( cfg, "branch.main.merge", "refs/heads/main" ),
409 GIT_OK );
410
411 KIGIT_COMMON common( repo );
412 auto [local, remote] = common.GetDifferentFiles();
413
414 // With unrelated histories there is no merge base, so the implementation cannot
415 // attribute changes to either side. Pre-fix, the revwalk reached both root commits
416 // and dumped both trees in full; the new merge-base approach short-circuits and
417 // reports no AHEAD/BEHIND files in this ambiguous state. In particular, the
418 // never-touched-locally "untouched.txt" must not appear in the AHEAD set.
419 BOOST_CHECK_MESSAGE( local.find( wxS( "untouched.txt" ) ) == local.end(),
420 "untouched.txt should not be reported as AHEAD when the local and "
421 "remote histories share no ancestor" );
422 BOOST_CHECK_MESSAGE( remote.find( wxS( "untouched.txt" ) ) == remote.end(),
423 "untouched.txt should not be reported as BEHIND when the local "
424 "and remote histories share no ancestor" );
425}
426
427
428// When local has commits ahead of a real upstream, only files that actually changed in those
429// local commits should be reported as AHEAD. Files that exist in both trees unchanged must
430// not show up as ahead. This is the common-case behaviour the issue reproducer exercises.
431BOOST_AUTO_TEST_CASE( GetDifferentFilesReportsOnlyTouchedFilesWhenAhead )
432{
433 GitInitGuard libgit;
434 ScopedTempDir tmp;
435
436 // Initial commit becomes the shared base. Two files: only one will be modified later.
437 git_repository* repo = makeRepoWithCommit( tmp.path, "untouched.txt", "stable\n" );
438 KIGIT::GitRepositoryPtr repoPtr( repo );
439
440 {
441 wxFileName extra( tmp.path, wxS( "touched.txt" ) );
442 std::ofstream f( extra.GetFullPath().utf8_string() );
443 f << "v1\n";
444 }
445
446 git_index* index = nullptr;
447 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
448 KIGIT::GitIndexPtr indexPtr( index );
449 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, "touched.txt" ), GIT_OK );
450 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
451
452 git_oid treeOid;
453 BOOST_REQUIRE_EQUAL( git_index_write_tree( &treeOid, index ), GIT_OK );
454
455 git_tree* tree = nullptr;
456 BOOST_REQUIRE_EQUAL( git_tree_lookup( &tree, repo, &treeOid ), GIT_OK );
457 KIGIT::GitTreePtr treePtr( tree );
458
459 git_reference* head = nullptr;
460 BOOST_REQUIRE_EQUAL( git_repository_head( &head, repo ), GIT_OK );
461 KIGIT::GitReferencePtr headPtr( head );
462
463 git_commit* parent = nullptr;
464 BOOST_REQUIRE_EQUAL( git_commit_lookup( &parent, repo, git_reference_target( head ) ),
465 GIT_OK );
466 KIGIT::GitCommitPtr parentPtr( parent );
467
468 git_signature* sig = nullptr;
469 BOOST_REQUIRE_EQUAL( git_signature_now( &sig, "QA Test", "[email protected]" ), GIT_OK );
470 KIGIT::GitSignaturePtr sigPtr( sig );
471
472 const git_commit* parents[1] = { parent };
473 git_oid base_oid;
474 BOOST_REQUIRE_EQUAL( git_commit_create( &base_oid, repo, "HEAD", sig, sig, nullptr,
475 "add touched", tree, 1, parents ),
476 GIT_OK );
477
478 // Mark this commit as the upstream tip and configure a remote so libgit2 accepts the
479 // branch.main.remote = origin pointer below.
480 git_remote* origin = nullptr;
481 BOOST_REQUIRE_EQUAL( git_remote_create( &origin, repo, "origin",
482 "[email protected]:repo.git" ),
483 GIT_OK );
484 KIGIT::GitRemotePtr originPtr( origin );
485
486 git_reference* upstream_ref = nullptr;
487 BOOST_REQUIRE_EQUAL( git_reference_create( &upstream_ref, repo, "refs/remotes/origin/main",
488 &base_oid, true, nullptr ),
489 GIT_OK );
490 KIGIT::GitReferencePtr upstreamRefPtr( upstream_ref );
491
492 git_config* cfg = nullptr;
493 BOOST_REQUIRE_EQUAL( git_repository_config( &cfg, repo ), GIT_OK );
494 KIGIT::GitConfigPtr cfgPtr( cfg );
495 BOOST_REQUIRE_EQUAL( git_config_set_string( cfg, "branch.main.remote", "origin" ),
496 GIT_OK );
497 BOOST_REQUIRE_EQUAL( git_config_set_string( cfg, "branch.main.merge", "refs/heads/main" ),
498 GIT_OK );
499
500 // Local-only commit modifying just touched.txt.
501 {
502 wxFileName extra( tmp.path, wxS( "touched.txt" ) );
503 std::ofstream f( extra.GetFullPath().utf8_string() );
504 f << "v2\n";
505 }
506
507 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, "touched.txt" ), GIT_OK );
508 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
509
510 git_oid newTreeOid;
511 BOOST_REQUIRE_EQUAL( git_index_write_tree( &newTreeOid, index ), GIT_OK );
512
513 git_tree* newTree = nullptr;
514 BOOST_REQUIRE_EQUAL( git_tree_lookup( &newTree, repo, &newTreeOid ), GIT_OK );
515 KIGIT::GitTreePtr newTreePtr( newTree );
516
517 git_commit* baseCommit = nullptr;
518 BOOST_REQUIRE_EQUAL( git_commit_lookup( &baseCommit, repo, &base_oid ), GIT_OK );
519 KIGIT::GitCommitPtr baseCommitPtr( baseCommit );
520
521 const git_commit* aheadParents[1] = { baseCommit };
522 git_oid aheadOid;
523 BOOST_REQUIRE_EQUAL( git_commit_create( &aheadOid, repo, "HEAD", sig, sig, nullptr,
524 "modify touched", newTree, 1, aheadParents ),
525 GIT_OK );
526
527 KIGIT_COMMON common( repo );
528 auto [local, remote] = common.GetDifferentFiles();
529
530 BOOST_CHECK( remote.empty() );
531 BOOST_CHECK_MESSAGE( local.find( wxS( "touched.txt" ) ) != local.end(),
532 "touched.txt should be reported as AHEAD" );
533 BOOST_CHECK_MESSAGE( local.find( wxS( "untouched.txt" ) ) == local.end(),
534 "untouched.txt should NOT be reported as AHEAD - this was the "
535 "regression in issue 21576" );
536}
537
538
539BOOST_AUTO_TEST_CASE( PrivateKeyEncryptionOpenSSHFormat )
540{
541 BOOST_CHECK_MESSAGE( KIGIT::IsPrivateKeyEncrypted( c_opensshEncrypted ),
542 "A passphrase-protected OpenSSH key must be reported as encrypted" );
543
544 BOOST_CHECK_MESSAGE( !KIGIT::IsPrivateKeyEncrypted( c_opensshPlain ),
545 "An OpenSSH key with cipher \"none\" must be reported as unencrypted" );
546
547 BOOST_CHECK_MESSAGE( !KIGIT::IsPrivateKeyEncrypted( c_opensshTruncated ),
548 "An OpenSSH key with an unreadable body must not be reported as encrypted" );
549}
550
551
552BOOST_AUTO_TEST_CASE( PrivateKeyEncryptionClassicPEMFormat )
553{
554 BOOST_CHECK_MESSAGE( KIGIT::IsPrivateKeyEncrypted( c_pemEncrypted ),
555 "A PEM key with Proc-Type: 4,ENCRYPTED must be reported as encrypted" );
556
557 BOOST_CHECK_MESSAGE( !KIGIT::IsPrivateKeyEncrypted( c_pemPlain ),
558 "A PEM key with no Proc-Type must be reported as unencrypted" );
559}
560
561
562BOOST_AUTO_TEST_CASE( PrivateKeyEncryptionPKCS8Format )
563{
564 BOOST_CHECK_MESSAGE( KIGIT::IsPrivateKeyEncrypted( c_pkcs8Encrypted ),
565 "A PKCS#8 key headed ENCRYPTED PRIVATE KEY must be reported as encrypted" );
566}
567
568
569BOOST_AUTO_TEST_CASE( PrivateKeyEncryptionRejectsNonKeys )
570{
571 BOOST_CHECK( !KIGIT::IsPrivateKeyEncrypted( wxEmptyString ) );
572
573 BOOST_CHECK_MESSAGE( !KIGIT::IsPrivateKeyEncrypted( "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKB0 t\n" ),
574 "A public key is not an encrypted private key" );
575}
576
577
int index
wxString GetGitRootDirectory() const
std::pair< std::set< wxString >, std::set< wxString > > GetDifferentFiles() const
Return a pair of sets of files that differ locally from the remote repository The first set is files ...
bool HasPushAndPullRemote() const
std::unique_ptr< git_tree, decltype([](git_tree *aTree) { git_tree_free(aTree); })> GitTreePtr
A unique pointer for git_tree objects with automatic cleanup.
bool IsPrivateKeyEncrypted(const wxString &aKeyText)
Report whether an SSH private key is protected by a passphrase.
std::unique_ptr< git_repository, decltype([](git_repository *aRepo) { git_repository_free(aRepo); })> GitRepositoryPtr
A unique pointer for git_repository objects with automatic cleanup.
std::unique_ptr< git_commit, decltype([](git_commit *aCommit) { git_commit_free(aCommit); })> GitCommitPtr
A unique pointer for git_commit objects with automatic cleanup.
std::unique_ptr< git_config, decltype([](git_config *aConfig) { git_config_free(aConfig); })> GitConfigPtr
A unique pointer for git_config objects with automatic cleanup.
std::unique_ptr< git_reference, decltype([](git_reference *aRef) { git_reference_free(aRef); })> GitReferencePtr
A unique pointer for git_reference objects with automatic cleanup.
std::unique_ptr< git_signature, decltype([](git_signature *aSignature) { git_signature_free(aSignature); })> GitSignaturePtr
A unique pointer for git_signature objects with automatic cleanup.
std::unique_ptr< git_index, decltype([](git_index *aIndex) { git_index_free(aIndex); })> GitIndexPtr
A unique pointer for git_index objects with automatic cleanup.
std::unique_ptr< git_remote, decltype([](git_remote *aRemote) { git_remote_free(aRemote); })> GitRemotePtr
A unique pointer for git_remote objects with automatic cleanup.
Scoped temporary directory used by the tests below.
ScopedTempDir(const wxString &aTag)
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(GitRootDirectoryReturnsWorkdir)
BOOST_CHECK_EQUAL(result, "25.4")