2020 LintException ,
2121 RemoteChallengeNotFound ,
2222)
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
2424
2525log = logging .getLogger ("ctfcli.cli.challenges" )
2626
@@ -119,17 +119,24 @@ def templates(self) -> int:
119119
120120 return TemplatesCommand .list ()
121121
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 } )" )
124126 config = Config ()
125127
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
127129 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+
128135 # Get a relative path from project root to current directory
129136 project_path = config .project_path
130137 project_relative_cwd = Path .cwd ().relative_to (project_path )
131138
132- # Get a new directory that will add the git subtree
139+ # Get a new directory that will add the git subtree / git subrepo
133140 repository_basename = Path (repo ).stem
134141
135142 # 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:
148155
149156 # Add a new challenge to the config
150157 config ["challenges" ][str (challenge_key )] = repo
151- head_branch = get_git_repo_head_branch (repo )
152158
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 ]
170162
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 :
172175 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." ,
174177 fg = "red" ,
175178 )
176179 return 1
@@ -186,7 +189,7 @@ def add(self, repo: str, directory: str = None, yaml_path: str = None) -> int:
186189
187190 if any (r != 0 for r in [git_add , git_commit ]):
188191 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." ,
190193 fg = "red" ,
191194 )
192195 return 1
@@ -205,7 +208,7 @@ def add(self, repo: str, directory: str = None, yaml_path: str = None) -> int:
205208 return 1
206209
207210 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 } )" )
209212 config = Config ()
210213
211214 if challenge :
@@ -224,6 +227,11 @@ def push(self, challenge: str = None, no_auto_pull: bool = False, quiet=False) -
224227 else :
225228 context = click .progressbar (challenges , label = "Pushing challenges" )
226229
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+
227235 with context as context_challenges :
228236 for challenge_instance in context_challenges :
229237 click .echo ()
@@ -256,7 +264,6 @@ def push(self, challenge: str = None, no_auto_pull: bool = False, quiet=False) -
256264 continue
257265
258266 click .secho (f"Pushing '{ challenge_path } ' to '{ challenge_repo } '" , fg = "blue" )
259- head_branch = get_git_repo_head_branch (challenge_repo )
260267
261268 log .debug (
262269 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) -
287294
288295 if any (r != 0 for r in [git_add , git_commit ]):
289296 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." ,
291298 fg = "red" ,
292299 )
293300 failed_pushes .append (challenge_instance )
294301 continue
295302
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 ]
312308
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 :
314311 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." ,
316313 fg = "red" ,
317314 )
318315 failed_pushes .append (challenge_instance )
@@ -335,8 +332,8 @@ def push(self, challenge: str = None, no_auto_pull: bool = False, quiet=False) -
335332
336333 return 1
337334
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 } )" )
340337 config = Config ()
341338
342339 if challenge :
@@ -353,6 +350,11 @@ def pull(self, challenge: str = None, quiet=False) -> int:
353350 else :
354351 context = click .progressbar (challenges , label = "Pulling challenges" )
355352
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+
356358 failed_pulls = []
357359 with context as context_challenges :
358360 for challenge_instance in context_challenges :
@@ -386,18 +388,25 @@ def pull(self, challenge: str = None, quiet=False) -> int:
386388 continue
387389
388390 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- )
395391
396392 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 = [
401410 "git" ,
402411 "subtree" ,
403412 "pull" ,
@@ -406,12 +415,10 @@ def pull(self, challenge: str = None, quiet=False) -> int:
406415 challenge_repo ,
407416 head_branch ,
408417 "--squash" ,
409- ],
410- cwd = config .project_path ,
411- env = pull_env ,
412- )
418+ ]
413419
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 :
415422 click .secho (
416423 f"Could not pull the subtree for challenge '{ challenge_path } '. "
417424 "Please check git error messages above." ,
@@ -420,25 +427,26 @@ def pull(self, challenge: str = None, quiet=False) -> int:
420427 failed_pulls .append (challenge_instance )
421428 continue
422429
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 )
425433
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 )
428436
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 )
431439
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
442450
443451 if len (failed_pulls ) == 0 :
444452 if not quiet :
@@ -460,6 +468,11 @@ def restore(self, challenge: str = None) -> int:
460468 click .secho ("Could not find any added challenges to restore" , fg = "yellow" )
461469 return 1
462470
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+
463476 failed_restores = []
464477 for challenge_key , challenge_source in config .challenges .items ():
465478 if challenge is not None and challenge_key != challenge :
@@ -483,6 +496,19 @@ def restore(self, challenge: str = None) -> int:
483496 failed_restores .append (challenge_key )
484497 continue
485498
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
486512 # Check if target directory exits
487513 if (config .project_path / challenge_key ).exists ():
488514 click .secho (
0 commit comments