20
20
LintException ,
21
21
RemoteChallengeNotFound ,
22
22
)
23
- from ctfcli .utils .git import get_git_repo_head_branch
23
+ from ctfcli .utils .git import check_if_git_subrepo_is_installed , get_git_repo_head_branch
24
24
25
25
log = logging .getLogger ("ctfcli.cli.challenges" )
26
26
@@ -119,17 +119,24 @@ def templates(self) -> int:
119
119
120
120
return TemplatesCommand .list ()
121
121
122
- def add (self , repo : str , directory : str = None , yaml_path : str = None ) -> int :
123
- log .debug (f"add: { repo } (directory={ directory } , yaml_path={ yaml_path } )" )
122
+ def add (
123
+ self , repo : str , directory : str = None , branch : str = None , force : bool = False , yaml_path : str = None
124
+ ) -> int :
125
+ log .debug (f"add: { repo } (directory={ directory } , branch={ branch } , force={ force } , yaml_path={ yaml_path } )" )
124
126
config = Config ()
125
127
126
- # check if we're working with a remote challenge which has to be pulled first
128
+ # Check if we're working with a remote challenge which has to be pulled first
127
129
if repo .endswith (".git" ):
130
+ use_subrepo = config ["config" ].getboolean ("use_subrepo" , fallback = False )
131
+ if use_subrepo and not check_if_git_subrepo_is_installed ():
132
+ click .secho ("This project is configured to use git subrepo, but it's not installed." )
133
+ return 1
134
+
128
135
# Get a relative path from project root to current directory
129
136
project_path = config .project_path
130
137
project_relative_cwd = Path .cwd ().relative_to (project_path )
131
138
132
- # Get a new directory that will add the git subtree
139
+ # Get a new directory that will add the git subtree / git subrepo
133
140
repository_basename = Path (repo ).stem
134
141
135
142
# Use the custom subdirectory for the challenge if one was provided
@@ -148,29 +155,25 @@ def add(self, repo: str, directory: str = None, yaml_path: str = None) -> int:
148
155
149
156
# Add a new challenge to the config
150
157
config ["challenges" ][str (challenge_key )] = repo
151
- head_branch = get_git_repo_head_branch (repo )
152
158
153
- log .debug (
154
- f"call(['git', 'subtree', 'add', '--prefix', '{ challenge_path } ', "
155
- f"'{ repo } ', '{ head_branch } ', '--squash'], cwd='{ project_path } ')"
156
- )
157
- git_subtree_add = subprocess .call (
158
- [
159
- "git" ,
160
- "subtree" ,
161
- "add" ,
162
- "--prefix" ,
163
- challenge_path ,
164
- repo ,
165
- head_branch ,
166
- "--squash" ,
167
- ],
168
- cwd = project_path ,
169
- )
159
+ if use_subrepo :
160
+ # Clone with subrepo if configured
161
+ cmd = ["git" , "subrepo" , "clone" , repo , challenge_path ]
170
162
171
- if git_subtree_add != 0 :
163
+ if branch is not None :
164
+ cmd += ["-b" , branch ]
165
+
166
+ if force :
167
+ cmd += ["-f" ]
168
+ else :
169
+ # Otherwise default to the built-in subtree
170
+ head_branch = get_git_repo_head_branch (repo )
171
+ cmd = ["git" , "subtree" , "add" , "--prefix" , challenge_path , repo , head_branch , "--squash" ]
172
+
173
+ log .debug (f"call({ cmd } , cwd='{ project_path } ')" )
174
+ if subprocess .call (cmd , cwd = project_path ) != 0 :
172
175
click .secho (
173
- "Could not add the challenge subtree. " " Please check git error messages above." ,
176
+ "Could not add the challenge repository. Please check git error messages above." ,
174
177
fg = "red" ,
175
178
)
176
179
return 1
@@ -186,7 +189,7 @@ def add(self, repo: str, directory: str = None, yaml_path: str = None) -> int:
186
189
187
190
if any (r != 0 for r in [git_add , git_commit ]):
188
191
click .secho (
189
- "Could not commit the challenge subtree. " " Please check git error messages above." ,
192
+ "Could not commit the challenge repository. Please check git error messages above." ,
190
193
fg = "red" ,
191
194
)
192
195
return 1
@@ -205,7 +208,7 @@ def add(self, repo: str, directory: str = None, yaml_path: str = None) -> int:
205
208
return 1
206
209
207
210
def push (self , challenge : str = None , no_auto_pull : bool = False , quiet = False ) -> int :
208
- log .debug (f"push: (challenge={ challenge } )" )
211
+ log .debug (f"push: (challenge={ challenge } , no_auto_pull= { no_auto_pull } , quiet= { quiet } )" )
209
212
config = Config ()
210
213
211
214
if challenge :
@@ -224,6 +227,11 @@ def push(self, challenge: str = None, no_auto_pull: bool = False, quiet=False) -
224
227
else :
225
228
context = click .progressbar (challenges , label = "Pushing challenges" )
226
229
230
+ use_subrepo = config ["config" ].getboolean ("use_subrepo" , fallback = False )
231
+ if use_subrepo and not check_if_git_subrepo_is_installed ():
232
+ click .secho ("This project is configured to use git subrepo, but it's not installed." )
233
+ return 1
234
+
227
235
with context as context_challenges :
228
236
for challenge_instance in context_challenges :
229
237
click .echo ()
@@ -256,7 +264,6 @@ def push(self, challenge: str = None, no_auto_pull: bool = False, quiet=False) -
256
264
continue
257
265
258
266
click .secho (f"Pushing '{ challenge_path } ' to '{ challenge_repo } '" , fg = "blue" )
259
- head_branch = get_git_repo_head_branch (challenge_repo )
260
267
261
268
log .debug (
262
269
f"call(['git', 'status', '--porcelain'], cwd='{ config .project_path / challenge_path } ',"
@@ -287,32 +294,22 @@ def push(self, challenge: str = None, no_auto_pull: bool = False, quiet=False) -
287
294
288
295
if any (r != 0 for r in [git_add , git_commit ]):
289
296
click .secho (
290
- "Could not commit the challenge changes. " " Please check git error messages above." ,
297
+ "Could not commit the challenge changes. Please check git error messages above." ,
291
298
fg = "red" ,
292
299
)
293
300
failed_pushes .append (challenge_instance )
294
301
continue
295
302
296
- log .debug (
297
- f"call(['git', 'subtree', 'push', '--prefix', '{ challenge_path } ', '{ challenge_repo } ', "
298
- f"'{ head_branch } '], cwd='{ config .project_path / challenge_path } ')"
299
- )
300
- git_subtree_push = subprocess .call (
301
- [
302
- "git" ,
303
- "subtree" ,
304
- "push" ,
305
- "--prefix" ,
306
- challenge_path ,
307
- challenge_repo ,
308
- head_branch ,
309
- ],
310
- cwd = config .project_path ,
311
- )
303
+ if use_subrepo :
304
+ cmd = ["git" , "subrepo" , "push" , challenge_path ]
305
+ else :
306
+ head_branch = get_git_repo_head_branch (challenge_repo )
307
+ cmd = ["git" , "subtree" , "push" , "--prefix" , challenge_path , challenge_repo , head_branch ]
312
308
313
- if git_subtree_push != 0 :
309
+ log .debug (f"call({ cmd } , cwd='{ config .project_path / challenge_path } ')" )
310
+ if subprocess .call (cmd , cwd = config .project_path ) != 0 :
314
311
click .secho (
315
- "Could not push the challenge subtree. " " Please check git error messages above." ,
312
+ "Could not push the challenge repository. Please check git error messages above." ,
316
313
fg = "red" ,
317
314
)
318
315
failed_pushes .append (challenge_instance )
@@ -335,8 +332,8 @@ def push(self, challenge: str = None, no_auto_pull: bool = False, quiet=False) -
335
332
336
333
return 1
337
334
338
- def pull (self , challenge : str = None , quiet = False ) -> int :
339
- log .debug (f"pull: (challenge={ challenge } )" )
335
+ def pull (self , challenge : str = None , strategy : str = "fast-forward" , quiet : bool = False ) -> int :
336
+ log .debug (f"pull: (challenge={ challenge } , quiet= { quiet } )" )
340
337
config = Config ()
341
338
342
339
if challenge :
@@ -353,6 +350,11 @@ def pull(self, challenge: str = None, quiet=False) -> int:
353
350
else :
354
351
context = click .progressbar (challenges , label = "Pulling challenges" )
355
352
353
+ use_subrepo = config ["config" ].getboolean ("use_subrepo" , fallback = False )
354
+ if use_subrepo and not check_if_git_subrepo_is_installed ():
355
+ click .secho ("This project is configured to use git subrepo, but it's not installed." )
356
+ return 1
357
+
356
358
failed_pulls = []
357
359
with context as context_challenges :
358
360
for challenge_instance in context_challenges :
@@ -386,18 +388,25 @@ def pull(self, challenge: str = None, quiet=False) -> int:
386
388
continue
387
389
388
390
click .secho (f"Pulling latest '{ challenge_repo } ' to '{ challenge_path } '" , fg = "blue" )
389
- head_branch = get_git_repo_head_branch (challenge_repo )
390
-
391
- log .debug (
392
- f"call(['git', 'subtree', 'pull', '--prefix', '{ challenge_path } ', "
393
- f"'{ challenge_repo } ', '{ head_branch } ', '--squash'], cwd='{ config .project_path } ')"
394
- )
395
391
396
392
pull_env = os .environ .copy ()
397
- pull_env ["GIT_MERGE_AUTOEDIT" ] = "no"
398
-
399
- git_subtree_pull = subprocess .call (
400
- [
393
+ if use_subrepo :
394
+ cmd = ["git" , "subrepo" , "pull" , challenge_path ]
395
+
396
+ if strategy == "rebase" :
397
+ cmd += ["--rebase" ]
398
+ elif strategy == "merge" :
399
+ cmd += ["--merge" ]
400
+ elif strategy == "force" :
401
+ cmd += ["--force" ]
402
+ elif strategy == "fast-forward" :
403
+ pass # fast-forward is the default strategy
404
+ else :
405
+ click .secho (f"Cannot pull challenge - '{ strategy } ' is not a valid pull strategy" , fg = "red" )
406
+ else :
407
+ head_branch = get_git_repo_head_branch (challenge_repo )
408
+ pull_env ["GIT_MERGE_AUTOEDIT" ] = "no"
409
+ cmd = [
401
410
"git" ,
402
411
"subtree" ,
403
412
"pull" ,
@@ -406,12 +415,10 @@ def pull(self, challenge: str = None, quiet=False) -> int:
406
415
challenge_repo ,
407
416
head_branch ,
408
417
"--squash" ,
409
- ],
410
- cwd = config .project_path ,
411
- env = pull_env ,
412
- )
418
+ ]
413
419
414
- if git_subtree_pull != 0 :
420
+ log .debug (f"call({ cmd } , cwd='{ config .project_path } )" )
421
+ if subprocess .call (cmd , cwd = config .project_path , env = pull_env ) != 0 :
415
422
click .secho (
416
423
f"Could not pull the subtree for challenge '{ challenge_path } '. "
417
424
"Please check git error messages above." ,
@@ -420,25 +427,26 @@ def pull(self, challenge: str = None, quiet=False) -> int:
420
427
failed_pulls .append (challenge_instance )
421
428
continue
422
429
423
- log .debug (f"call(['git', 'mergetool'], cwd='{ config .project_path / challenge_path } ')" )
424
- git_mergetool = subprocess .call (["git" , "mergetool" ], cwd = config .project_path / challenge_path )
430
+ if not use_subrepo :
431
+ log .debug (f"call(['git', 'mergetool'], cwd='{ config .project_path / challenge_path } ')" )
432
+ git_mergetool = subprocess .call (["git" , "mergetool" ], cwd = config .project_path / challenge_path )
425
433
426
- log .debug (f"call(['git', 'commit', '--no-edit'], cwd='{ config .project_path / challenge_path } ')" )
427
- subprocess .call (["git" , "commit" , "--no-edit" ], cwd = config .project_path / challenge_path )
434
+ log .debug (f"call(['git', 'commit', '--no-edit'], cwd='{ config .project_path / challenge_path } ')" )
435
+ subprocess .call (["git" , "commit" , "--no-edit" ], cwd = config .project_path / challenge_path )
428
436
429
- log .debug (f"call(['git', 'clean', '-f'], cwd='{ config .project_path / challenge_path } ')" )
430
- git_clean = subprocess .call (["git" , "clean" , "-f" ], cwd = config .project_path / challenge_path )
437
+ log .debug (f"call(['git', 'clean', '-f'], cwd='{ config .project_path / challenge_path } ')" )
438
+ git_clean = subprocess .call (["git" , "clean" , "-f" ], cwd = config .project_path / challenge_path )
431
439
432
- # git commit is allowed to return a non-zero code
433
- # because it would also mean that there's nothing to commit
434
- if any (r != 0 for r in [git_mergetool , git_clean ]):
435
- click .secho (
436
- f"Could not commit the subtree for challenge '{ challenge_path } '. "
437
- "Please check git error messages above." ,
438
- fg = "red" ,
439
- )
440
- failed_pulls .append (challenge_instance )
441
- continue
440
+ # git commit is allowed to return a non-zero code
441
+ # because it would also mean that there's nothing to commit
442
+ if any (r != 0 for r in [git_mergetool , git_clean ]):
443
+ click .secho (
444
+ f"Could not commit the changes for challenge '{ challenge_path } '. "
445
+ "Please check git error messages above." ,
446
+ fg = "red" ,
447
+ )
448
+ failed_pulls .append (challenge_instance )
449
+ continue
442
450
443
451
if len (failed_pulls ) == 0 :
444
452
if not quiet :
@@ -460,6 +468,11 @@ def restore(self, challenge: str = None) -> int:
460
468
click .secho ("Could not find any added challenges to restore" , fg = "yellow" )
461
469
return 1
462
470
471
+ use_subrepo = config ["config" ].getboolean ("use_subrepo" , fallback = False )
472
+ if use_subrepo and not check_if_git_subrepo_is_installed ():
473
+ click .secho ("This project is configured to use git subrepo, but it's not installed." )
474
+ return 1
475
+
463
476
failed_restores = []
464
477
for challenge_key , challenge_source in config .challenges .items ():
465
478
if challenge is not None and challenge_key != challenge :
@@ -483,6 +496,19 @@ def restore(self, challenge: str = None) -> int:
483
496
failed_restores .append (challenge_key )
484
497
continue
485
498
499
+ # If we're using subrepo - the restore can be achieved by performing a force pull
500
+ if use_subrepo :
501
+ if self .pull (challenge , strategy = "force" ) != 0 :
502
+ click .secho (
503
+ f"Failed to restore challenge '{ challenge_key } ' via subrepo force pull. "
504
+ "Please check git error messages above." ,
505
+ fg = "red" ,
506
+ )
507
+ failed_restores .append (challenge_key )
508
+
509
+ continue
510
+
511
+ # Otherwise - default to restoring the repository via re-adding the subtree
486
512
# Check if target directory exits
487
513
if (config .project_path / challenge_key ).exists ():
488
514
click .secho (
0 commit comments