Skip to content

Commit 226036f

Browse files
authored
Refactor the ctf challenge add interface to create the challenge repo using git subtree (#65)
* Change git submodule challenge repos to use git subtree instead * Officially have ctf challenge commands to sync changes between the upstream repo and the local repo
1 parent e403b8b commit 226036f

File tree

2 files changed

+77
-18
lines changed

2 files changed

+77
-18
lines changed

ctfcli/cli/challenges.py

+64-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
import shutil
32
import subprocess
43
from pathlib import Path
54
from urllib.parse import urlparse
@@ -24,6 +23,7 @@
2423
from ctfcli.utils.deploy import DEPLOY_HANDLERS
2524
from ctfcli.utils.spec import CHALLENGE_SPEC_DOCS, blank_challenge_spec
2625
from ctfcli.utils.templates import get_template_dir
26+
from ctfcli.utils.git import get_git_repo_head_branch
2727

2828

2929
class Challenge(object):
@@ -58,7 +58,7 @@ def add(self, repo):
5858
# Get relative path from project root to current directory
5959
challenge_path = Path(os.path.relpath(os.getcwd(), get_project_path()))
6060

61-
# Get new directory that will exist after clone
61+
# Get new directory that will add the git subtree
6262
base_repo_path = Path(os.path.basename(repo).rsplit(".", maxsplit=1)[0])
6363

6464
# Join targets
@@ -67,11 +67,23 @@ def add(self, repo):
6767

6868
config["challenges"][str(challenge_path)] = repo
6969

70+
head_branch = get_git_repo_head_branch(repo)
71+
subprocess.call(
72+
[
73+
"git",
74+
"subtree",
75+
"add",
76+
"--prefix",
77+
challenge_path,
78+
repo,
79+
head_branch,
80+
"--squash",
81+
],
82+
cwd=get_project_path(),
83+
)
7084
with open(get_config_path(), "w+") as f:
7185
config.write(f)
7286

73-
subprocess.call(["git", "clone", "--depth", "1", repo])
74-
shutil.rmtree(str(base_repo_path / ".git"))
7587
elif Path(repo).exists():
7688
config["challenges"][repo] = repo
7789
with open(get_config_path(), "w+") as f:
@@ -89,9 +101,21 @@ def restore(self, challenge=None):
89101
if url.endswith(".git"):
90102
if challenge is not None and folder != challenge:
91103
continue
92-
click.echo(f"Cloning {url} to {folder}")
93-
subprocess.call(["git", "clone", "--depth", "1", url, folder])
94-
shutil.rmtree(str(Path(folder) / ".git"))
104+
click.echo(f"Adding git repo {url} to {folder} as subtree")
105+
head_branch = get_git_repo_head_branch(url)
106+
subprocess.call(
107+
[
108+
"git",
109+
"subtree",
110+
"add",
111+
"--prefix",
112+
folder,
113+
url,
114+
head_branch,
115+
"--squash",
116+
],
117+
cwd=get_project_path(),
118+
)
95119
else:
96120
click.echo(f"Skipping {url} - {folder}")
97121

@@ -178,22 +202,24 @@ def update(self, challenge=None):
178202
if challenge and challenge != folder:
179203
continue
180204
if url.endswith(".git"):
181-
click.echo(f"Cloning {url} to {folder}")
182-
subprocess.call(["git", "init"], cwd=folder)
183-
subprocess.call(["git", "remote", "add", "origin", url], cwd=folder)
184-
subprocess.call(["git", "add", "-A"], cwd=folder)
205+
click.echo(f"Pulling latest {url} to {folder}")
206+
head_branch = get_git_repo_head_branch(url)
185207
subprocess.call(
186-
["git", "commit", "-m", "Persist local changes (ctfcli)"],
187-
cwd=folder,
188-
)
189-
subprocess.call(
190-
["git", "pull", "--allow-unrelated-histories", "origin", "master"],
191-
cwd=folder,
208+
[
209+
"git",
210+
"subtree",
211+
"pull",
212+
"--prefix",
213+
folder,
214+
url,
215+
head_branch,
216+
"--squash",
217+
],
218+
cwd=get_project_path(),
192219
)
193220
subprocess.call(["git", "mergetool"], cwd=folder)
194221
subprocess.call(["git", "clean", "-f"], cwd=folder)
195222
subprocess.call(["git", "commit", "--no-edit"], cwd=folder)
196-
shutil.rmtree(str(Path(folder) / ".git"))
197223
else:
198224
click.echo(f"Skipping {url} - {folder}")
199225

@@ -299,3 +325,23 @@ def deploy(self, challenge, host=None):
299325
click.secho(
300326
f"An error occured during deployment", fg="red",
301327
)
328+
329+
def push(self, challenge=None):
330+
config = load_config()
331+
challenges = dict(config["challenges"])
332+
if challenge is None:
333+
# Get relative path from project root to current directory
334+
challenge_path = Path(os.path.relpath(os.getcwd(), get_project_path()))
335+
challenge = str(challenge_path)
336+
337+
try:
338+
url = challenges[challenge]
339+
head_branch = get_git_repo_head_branch(url)
340+
subprocess.call(
341+
["git", "subtree", "push", "--prefix", challenge, url, head_branch],
342+
cwd=get_project_path(),
343+
)
344+
except KeyError:
345+
click.echo(
346+
"Couldn't process that challenge path. Please check that the challenge is added to .ctf/config and that your path matches."
347+
)

ctfcli/utils/git.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import subprocess
2+
3+
4+
def get_git_repo_head_branch(repo):
5+
"""
6+
A helper method to get the reference of the HEAD branch of a git remote repo.
7+
https://stackoverflow.com/a/41925348
8+
"""
9+
out = subprocess.check_output(
10+
["git", "ls-remote", "--symref", repo, "HEAD"]
11+
).decode()
12+
head_branch = out.split()[1]
13+
return head_branch

0 commit comments

Comments
 (0)