Skip to content

Commit 891852e

Browse files
author
Miłosz Skaza
authored
Add bulk challenge pull and push, and auto pull after push (#149)
1 parent 26bcec8 commit 891852e

File tree

1 file changed

+216
-129
lines changed

1 file changed

+216
-129
lines changed

ctfcli/cli/challenges.py

+216-129
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import logging
23
import os
34
import subprocess
@@ -203,167 +204,253 @@ def add(self, repo: str, directory: str = None, yaml_path: str = None) -> int:
203204
click.secho(f"Could not process the challenge path: '{repo}'", fg="red")
204205
return 1
205206

206-
def push(self, challenge: str = None) -> int:
207+
def push(self, challenge: str = None, no_auto_pull: bool = False, quiet=False) -> int:
207208
log.debug(f"push: (challenge={challenge})")
208209
config = Config()
209210

210-
challenge_path = Path.cwd()
211211
if challenge:
212-
challenge_path = config.project_path / Path(challenge)
212+
challenge_instance = self._resolve_single_challenge(challenge)
213+
if not challenge_instance:
214+
return 1
213215

214-
# Get a relative path from project root to the challenge
215-
# As this is what git subtree push requires
216-
challenge_path = challenge_path.relative_to(config.project_path)
217-
challenge_repo = config.challenges.get(str(challenge_path), None)
216+
challenges = [challenge_instance]
217+
else:
218+
challenges = self._resolve_all_challenges()
218219

219-
# if we don't find the challenge by the directory,
220-
# check if it's saved with a direct path to challenge.yml
221-
if not challenge_repo:
222-
challenge_repo = config.challenges.get(str(challenge_path / "challenge.yml"), None)
220+
failed_pushes = []
223221

224-
if not challenge_repo:
225-
click.secho(
226-
f"Could not find added challenge '{challenge_path}' "
227-
"Please check that the challenge is added to .ctf/config and that your path matches",
228-
fg="red",
229-
)
230-
return 1
222+
if quiet or len(challenges) <= 1:
223+
context = contextlib.nullcontext(challenges)
224+
else:
225+
context = click.progressbar(challenges, label="Pushing challenges")
231226

232-
if not challenge_repo.endswith(".git"):
233-
click.secho(
234-
f"Cannot push challenge '{challenge_path}', as it's not a git-based challenge",
235-
fg="yellow",
236-
)
237-
return 1
227+
with context as context_challenges:
228+
for challenge_instance in context_challenges:
229+
click.echo()
238230

239-
head_branch = get_git_repo_head_branch(challenge_repo)
231+
# Get a relative path from project root to the challenge
232+
# As this is what git subtree push requires
233+
challenge_path = challenge_instance.challenge_directory.resolve().relative_to(config.project_path)
234+
challenge_repo = config.challenges.get(str(challenge_path), None)
240235

241-
log.debug(f"call(['git', 'add', '.'], cwd='{config.project_path / challenge_path}')")
242-
git_add = subprocess.call(["git", "add", "."], cwd=config.project_path / challenge_path)
236+
# if we don't find the challenge by the directory,
237+
# check if it's saved with a direct path to challenge.yml
238+
if not challenge_repo:
239+
challenge_repo = config.challenges.get(str(challenge_path / "challenge.yml"), None)
243240

244-
log.debug(
245-
f"call(['git', 'commit', '-m', 'Pushing changes to {challenge_path}'], "
246-
f"cwd='{config.project_path / challenge_path}')"
247-
)
248-
git_commit = subprocess.call(
249-
["git", "commit", "-m", f"Pushing changes to {challenge_path}"],
250-
cwd=config.project_path / challenge_path,
251-
)
241+
if not challenge_repo:
242+
click.secho(
243+
f"Could not find added challenge '{challenge_path}' "
244+
"Please check that the challenge is added to .ctf/config and that your path matches",
245+
fg="red",
246+
)
247+
failed_pushes.append(challenge_instance)
248+
continue
252249

253-
if any(r != 0 for r in [git_add, git_commit]):
254-
click.secho(
255-
"Could not commit the challenge changes. " "Please check git error messages above.",
256-
fg="red",
257-
)
258-
return 1
250+
if not challenge_repo.endswith(".git"):
251+
click.secho(
252+
f"Cannot push challenge '{challenge_path}', as it's not a git-based challenge",
253+
fg="yellow",
254+
)
255+
failed_pushes.append(challenge_instance)
256+
continue
259257

260-
log.debug(
261-
f"call(['git', 'subtree', 'push', '--prefix', '{challenge_path}', '{challenge_repo}', '{head_branch}'], "
262-
f"cwd='{config.project_path / challenge_path}')"
263-
)
264-
git_subtree_push = subprocess.call(
265-
[
266-
"git",
267-
"subtree",
268-
"push",
269-
"--prefix",
270-
challenge_path,
271-
challenge_repo,
272-
head_branch,
273-
],
274-
cwd=config.project_path,
275-
)
258+
click.secho(f"Pushing '{challenge_path}' to '{challenge_repo}'", fg="blue")
259+
head_branch = get_git_repo_head_branch(challenge_repo)
276260

277-
if git_subtree_push != 0:
278-
click.secho(
279-
"Could not push the challenge subtree. " "Please check git error messages above.",
280-
fg="red",
281-
)
282-
return 1
261+
log.debug(
262+
f"call(['git', 'status', '--porcelain'], cwd='{config.project_path / challenge_path}',"
263+
f" stdout=subprocess.PIPE, text=True)"
264+
)
265+
git_status = subprocess.run(
266+
["git", "status", "--porcelain"],
267+
cwd=config.project_path / challenge_path,
268+
stdout=subprocess.PIPE,
269+
text=True,
270+
)
283271

284-
return 0
272+
if git_status.stdout.strip() == "" and git_status.returncode == 0:
273+
click.secho(f"No changes to be pushed for {challenge_path}", fg="green")
274+
continue
275+
276+
log.debug(f"call(['git', 'add', '.'], cwd='{config.project_path / challenge_path}')")
277+
git_add = subprocess.call(["git", "add", "."], cwd=config.project_path / challenge_path)
278+
279+
log.debug(
280+
f"call(['git', 'commit', '-m', 'Pushing changes to {challenge_path}'], "
281+
f"cwd='{config.project_path / challenge_path}')"
282+
)
283+
git_commit = subprocess.call(
284+
["git", "commit", "-m", f"Pushing changes to {challenge_path}"],
285+
cwd=config.project_path / challenge_path,
286+
)
287+
288+
if any(r != 0 for r in [git_add, git_commit]):
289+
click.secho(
290+
"Could not commit the challenge changes. " "Please check git error messages above.",
291+
fg="red",
292+
)
293+
failed_pushes.append(challenge_instance)
294+
continue
295+
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+
)
312+
313+
if git_subtree_push != 0:
314+
click.secho(
315+
"Could not push the challenge subtree. " "Please check git error messages above.",
316+
fg="red",
317+
)
318+
failed_pushes.append(challenge_instance)
319+
continue
320+
321+
# if auto pull is not disabled
322+
if not no_auto_pull:
323+
self.pull(str(challenge_path), quiet=True)
285324

286-
def pull(self, challenge: str = None) -> int:
325+
if len(failed_pushes) == 0:
326+
if not quiet:
327+
click.secho("Success! All challenges pushed!", fg="green")
328+
329+
return 0
330+
331+
if not quiet:
332+
click.secho("Push failed for:", fg="red")
333+
for challenge in failed_pushes:
334+
click.echo(f" - {challenge}")
335+
336+
return 1
337+
338+
def pull(self, challenge: str = None, quiet=False) -> int:
287339
log.debug(f"pull: (challenge={challenge})")
288340
config = Config()
289341

290-
challenge_path = Path.cwd()
291342
if challenge:
292-
challenge_path = config.project_path / Path(challenge)
343+
challenge_instance = self._resolve_single_challenge(challenge)
344+
if not challenge_instance:
345+
return 1
293346

294-
# Get a relative path from project root to the challenge
295-
# As this is what git subtree push requires
296-
challenge_path = challenge_path.relative_to(config.project_path)
297-
challenge_repo = config.challenges.get(str(challenge_path), None)
347+
challenges = [challenge_instance]
348+
else:
349+
challenges = self._resolve_all_challenges()
298350

299-
# if we don't find the challenge by the directory,
300-
# check if it's saved with a direct path to challenge.yml
301-
if not challenge_repo:
302-
challenge_repo = config.challenges.get(str(challenge_path / "challenge.yml"), None)
351+
if quiet or len(challenges) <= 1:
352+
context = contextlib.nullcontext(challenges)
353+
else:
354+
context = click.progressbar(challenges, label="Pulling challenges")
303355

304-
if not challenge_repo:
305-
click.secho(
306-
f"Could not find added challenge '{challenge_path}' "
307-
"Please check that the challenge is added to .ctf/config and that your path matches",
308-
fg="red",
309-
)
310-
return 1
356+
failed_pulls = []
357+
with context as context_challenges:
358+
for challenge_instance in context_challenges:
359+
click.echo()
311360

312-
if not challenge_repo.endswith(".git"):
313-
click.secho(
314-
f"Cannot pull challenge '{challenge_path}', as it's not a git-based challenge",
315-
fg="yellow",
316-
)
317-
return 1
361+
# Get a relative path from project root to the challenge
362+
# As this is what git subtree push requires
363+
challenge_path = challenge_instance.challenge_directory.resolve().relative_to(config.project_path)
364+
challenge_repo = config.challenges.get(str(challenge_path), None)
318365

319-
click.secho(f"Pulling latest '{challenge_repo}' to '{challenge_path}'", fg="blue")
320-
head_branch = get_git_repo_head_branch(challenge_repo)
366+
# if we don't find the challenge by the directory,
367+
# check if it's saved with a direct path to challenge.yml
368+
if not challenge_repo:
369+
challenge_repo = config.challenges.get(str(challenge_path / "challenge.yml"), None)
321370

322-
log.debug(
323-
f"call(['git', 'subtree', 'pull', '--prefix', '{challenge_path}', "
324-
f"'{challenge_repo}', '{head_branch}', '--squash'], cwd='{config.project_path}')"
325-
)
326-
git_subtree_pull = subprocess.call(
327-
[
328-
"git",
329-
"subtree",
330-
"pull",
331-
"--prefix",
332-
challenge_path,
333-
challenge_repo,
334-
head_branch,
335-
"--squash",
336-
],
337-
cwd=config.project_path,
338-
)
371+
if not challenge_repo:
372+
click.secho(
373+
f"Could not find added challenge '{challenge_path}' "
374+
"Please check that the challenge is added to .ctf/config and that your path matches",
375+
fg="red",
376+
)
377+
failed_pulls.append(challenge_instance)
378+
continue
339379

340-
if git_subtree_pull != 0:
341-
click.secho(
342-
f"Could not pull the subtree for challenge '{challenge_path}'. "
343-
"Please check git error messages above.",
344-
fg="red",
345-
)
346-
return 1
380+
if not challenge_repo.endswith(".git"):
381+
click.secho(
382+
f"Cannot pull challenge '{challenge_path}', as it's not a git-based challenge",
383+
fg="yellow",
384+
)
385+
failed_pulls.append(challenge_instance)
386+
continue
347387

348-
log.debug(f"call(['git', 'mergetool'], cwd='{config.project_path / challenge_path}')")
349-
git_mergetool = subprocess.call(["git", "mergetool"], cwd=config.project_path / challenge_path)
388+
click.secho(f"Pulling latest '{challenge_repo}' to '{challenge_path}'", fg="blue")
389+
head_branch = get_git_repo_head_branch(challenge_repo)
350390

351-
log.debug(f"call(['git', 'clean', '-f'], cwd='{config.project_path / challenge_path}')")
352-
git_clean = subprocess.call(["git", "clean", "-f"], cwd=config.project_path / challenge_path)
391+
log.debug(
392+
f"call(['git', 'subtree', 'pull', '--prefix', '{challenge_path}', "
393+
f"'{challenge_repo}', '{head_branch}', '--squash'], cwd='{config.project_path}')"
394+
)
353395

354-
log.debug(f"call(['git', 'commit', '--no-edit'], cwd='{config.project_path / challenge_path}')")
355-
subprocess.call(["git", "commit", "--no-edit"], cwd=config.project_path / challenge_path)
396+
pull_env = os.environ.copy()
397+
pull_env["GIT_MERGE_AUTOEDIT"] = "no"
398+
399+
git_subtree_pull = subprocess.call(
400+
[
401+
"git",
402+
"subtree",
403+
"pull",
404+
"--prefix",
405+
challenge_path,
406+
challenge_repo,
407+
head_branch,
408+
"--squash",
409+
],
410+
cwd=config.project_path,
411+
env=pull_env,
412+
)
356413

357-
# git commit is allowed to return a non-zero code because it would also mean that there's nothing to commit
358-
if any(r != 0 for r in [git_mergetool, git_clean]):
359-
click.secho(
360-
f"Could not commit the subtree for challenge '{challenge_path}'. "
361-
"Please check git error messages above.",
362-
fg="red",
363-
)
364-
return 1
414+
if git_subtree_pull != 0:
415+
click.secho(
416+
f"Could not pull the subtree for challenge '{challenge_path}'. "
417+
"Please check git error messages above.",
418+
fg="red",
419+
)
420+
failed_pulls.append(challenge_instance)
421+
continue
365422

366-
return 0
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)
425+
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)
428+
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)
431+
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
442+
443+
if len(failed_pulls) == 0:
444+
if not quiet:
445+
click.secho("Success! All challenges pulled!", fg="green")
446+
return 0
447+
448+
if not quiet:
449+
click.secho("Pull failed for:", fg="red")
450+
for challenge in failed_pulls:
451+
click.echo(f" - {challenge}")
452+
453+
return 1
367454

368455
def restore(self, challenge: str = None) -> int:
369456
log.debug(f"restore: (challenge={challenge})")

0 commit comments

Comments
 (0)