Skip to content

Commit 3f602d9

Browse files
authored
Merge pull request #287 from jakub-nt/CFE-4089
CFE-4089: Implemented updating modules added from a URL by branch
2 parents 22340e4 + 02338a1 commit 3f602d9

File tree

14 files changed

+150
-57
lines changed

14 files changed

+150
-57
lines changed

JSON.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ The modules inside `build`, `provides`, and `index` use these fields:
194194
- `commit` (string): Commit hash used when we download and snapshot the version of a module.
195195
Used in `index` and modules added from an index.
196196
Must be updated together with `version`.
197+
- `branch` (string): Branch name used when updating modules added by URL.
198+
Optional - if specified, the branch will be used during `cfbs update` instead of the default branch.
199+
Requires the `url` field to be specified.
200+
If the `branch` field is specified, the `commit` field must also be specified.
197201
- `subdirectory` (string): Used if the module is inside a subdirectory of a repo.
198202
See for example [the `cfbs.json` of our modules repo](https://github.com/cfengine/modules/blob/master/cfbs.json).
199203
Not used for local modules (policy files or folders) - the name is the path to the module in this case.

cfbs/cfbs_config.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -150,27 +150,32 @@ def _add_using_url(
150150
checksum=None,
151151
explicit_build_steps=None,
152152
):
153-
"""The `url` argument can optionally also have the form `<url>@<commit>`."""
153+
"""The `url` argument can optionally also have the form `<url>@<commit or branch>`."""
154154
url_commit = None
155+
url_branch = None
155156
if url.endswith(SUPPORTED_ARCHIVES):
156157
config_path, url_commit = fetch_archive(url, checksum)
157158
else:
158159
assert url.startswith(SUPPORTED_URI_SCHEMES)
159160

160-
commit = None
161+
reference = None
161162
if "@" in url and (url.rindex("@") > url.rindex(".")):
162-
# commit specified in the url
163-
url, commit = url.rsplit("@", 1)
163+
# commit or branch specified together with the URL
164+
url, reference = url.rsplit("@", 1)
164165
if "@" in url and (url.rindex("@") > url.rindex(".")):
165166
raise CFBSUserError(
166-
"Cannot specify more than one commit for one add URL"
167+
"Cannot specify more than one commit or branch for one add URL"
167168
)
168-
if not is_a_commit_hash(commit):
169-
raise CFBSExitError("'%s' is not a commit reference" % commit)
169+
config_path, url_commit = clone_url_repo(url, reference)
170170

171-
config_path, url_commit = clone_url_repo(url, commit)
171+
# a branch name and a commit hash are distinguished as follows:
172+
# if the reference matches the form of a commit hash, it is assumed to be a commit hash, otherwise it is assumed to be a branch name
173+
if not is_a_commit_hash(reference):
174+
url_branch = reference
172175

173-
remote_config = CFBSJson(path=config_path, url=url, url_commit=url_commit)
176+
remote_config = CFBSJson(
177+
path=config_path, url=url, url_commit=url_commit, url_branch=url_branch
178+
)
174179

175180
provides = remote_config.get_provides(added_by)
176181
add_all = True

cfbs/cfbs_json.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from cfbs.utils import CFBSValidationError, read_json, CFBSExitError
2121

2222

23-
def _construct_provided_module(name, data, url, commit, added_by):
23+
def _construct_provided_module(name, data, url, commit, branch, added_by):
2424
# At this point the @commit part should be removed from url so:
2525
# either url should not have an @,
2626
# or the @ should be for user@host.something
@@ -35,6 +35,8 @@ def _construct_provided_module(name, data, url, commit, added_by):
3535
module["description"] = data["description"]
3636
module["url"] = url
3737
module["commit"] = commit
38+
if branch is not None:
39+
module["branch"] = branch
3840
subdirectory = data.get("subdirectory")
3941
if subdirectory:
4042
module["subdirectory"] = subdirectory
@@ -60,12 +62,16 @@ def __init__(
6062
index_argument=None,
6163
data=None,
6264
url=None,
63-
url_commit=None,
65+
url_commit: Optional[str] = None,
66+
url_branch=None,
6467
):
6568
assert path
6669
self.path = path
70+
6771
self.url = url
6872
self.url_commit = url_commit
73+
self.url_branch = url_branch
74+
6975
if data:
7076
self._data = data
7177
else:
@@ -184,7 +190,7 @@ def get_provides(self, added_by: Optional[str]):
184190
)
185191
for k, v in self._data["provides"].items():
186192
module = _construct_provided_module(
187-
k, v, self.url, self.url_commit, added_by
193+
k, v, self.url, self.url_commit, self.url_branch, added_by
188194
)
189195
modules[k] = module
190196
return modules
@@ -194,7 +200,7 @@ def get_module_for_build(self, name, added_by="cfbs add"):
194200
if "provides" in self._data and name in self._data["provides"]:
195201
module = self._data["provides"][name]
196202
return _construct_provided_module(
197-
name, module, self.url, self.url_commit, added_by
203+
name, module, self.url, self.url_commit, self.url_branch, added_by
198204
)
199205
if name in self.index:
200206
return self.index.get_module_object(name, added_by)

cfbs/commands.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ def search_command(terms: List[str]):
113113
git_configure_and_initialize,
114114
is_git_repo,
115115
CFBSGitError,
116-
ls_remote,
117116
)
118117

119118
from cfbs.git_magic import commit_after_command, git_commit_maybe_prompt
@@ -281,16 +280,12 @@ def init_command(
281280
log.debug("--masterfiles=%s appears to be a version number" % masterfiles)
282281
to_add = ["masterfiles@%s" % masterfiles]
283282
elif masterfiles != "no":
284-
log.debug("--masterfiles=%s appears to be a branch" % masterfiles)
285-
branch = masterfiles
286-
remote = "https://github.com/cfengine/masterfiles"
287-
commit = ls_remote(remote, branch)
288-
if commit is None:
289-
raise CFBSExitError(
290-
"Failed to find branch or tag %s at remote %s" % (branch, remote)
291-
)
292-
log.debug("Current commit for masterfiles branch %s is %s" % (branch, commit))
293-
to_add = ["%s@%s" % (remote, commit), "masterfiles"]
283+
log.debug(
284+
"--masterfiles=%s appears to be a branch or a commit hash" % masterfiles
285+
)
286+
# add masterfiles from URL, instead of index by name
287+
MPF_REPO_URL = "https://github.com/cfengine/masterfiles"
288+
to_add = ["%s@%s" % (MPF_REPO_URL, masterfiles), "masterfiles"]
294289
if to_add:
295290
result = add_command(to_add, added_by="cfbs init")
296291
if result != 0:
@@ -623,9 +618,10 @@ def update_command(to_update):
623618
log.warning("Module '%s' not in build. Skipping its update." % update.name)
624619
continue
625620
if "url" in old_module:
626-
path, commit = clone_url_repo(old_module["url"])
621+
branch = old_module.get("branch")
622+
path, commit = clone_url_repo(old_module["url"], branch)
627623
remote_config = CFBSJson(
628-
path=path, url=old_module["url"], url_commit=commit
624+
path=path, url=old_module["url"], url_commit=commit, url_branch=branch
629625
)
630626

631627
module_name = old_module["name"]
@@ -716,7 +712,7 @@ def update_command(to_update):
716712
)
717713
raise CFBSValidationError(
718714
"The cfbs.json was invalid before update, "
719-
+ "but updating modules did not fix it - aborting update"
715+
+ "but updating modules did not fix it - aborting update "
720716
+ "(see validation error messages above)"
721717
)
722718
config.save()
@@ -958,6 +954,7 @@ def human_readable(key: str):
958954
"url",
959955
"repo",
960956
"version",
957+
"branch",
961958
"commit",
962959
"by",
963960
"status",

cfbs/git.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,16 @@ def git_check_tracked_changes(scope=["all"]):
249249
raise CFBSGitError(
250250
"Failed to run 'git status -s -u' to check for changes."
251251
) from cpe
252+
253+
254+
def treeish_exists(treeish, repo_path):
255+
command = [
256+
"git",
257+
"rev-parse",
258+
"--verify",
259+
"--end-of-options",
260+
treeish + r"^{object}",
261+
]
262+
result = run(command, cwd=repo_path, stdout=DEVNULL, stderr=DEVNULL, check=False)
263+
264+
return result.returncode == 0

cfbs/internal_file_management.py

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import shutil
1414
from typing import Optional
1515

16+
from cfbs.git import ls_remote, treeish_exists
1617
from cfbs.utils import (
1718
cfbs_dir,
1819
cp,
@@ -136,28 +137,19 @@ def _get_path_from_url(url):
136137
return path
137138

138139

139-
def _get_git_repo_commit_sha(repo_path):
140-
assert os.path.isdir(os.path.join(repo_path, ".git"))
141-
142-
with open(os.path.join(repo_path, ".git", "HEAD"), "r") as f:
143-
head_ref_info = f.read()
144-
145-
assert head_ref_info.startswith("ref: ")
146-
head_ref = head_ref_info[5:].strip()
147-
148-
with open(os.path.join(repo_path, ".git", head_ref)) as f:
149-
return f.read().strip()
150-
151-
152140
def _clone_and_checkout(url, path, treeish):
153141
# NOTE: If any of these shell (git) commands fail, we will exit
154142
if not os.path.exists(os.path.join(path, ".git")):
155143
sh("git clone --no-checkout %s %s" % (url, path))
144+
if not treeish_exists(treeish, path):
145+
raise CFBSExitError("%s not found in %s" % (treeish, url))
146+
156147
sh("git checkout " + treeish, directory=path)
157148

158149

159-
def clone_url_repo(repo_url: str, commit: Optional[str] = None):
160-
"""Clones a Git repository at `repo_url` URL, optionally checking out the `commit` commit.
150+
def clone_url_repo(repo_url: str, reference: Optional[str] = None):
151+
"""Clones a Git repository at `repo_url` URL, optionally checking out the `reference` commit or branch.
152+
If `reference` is `None`, the repository's default branch will be used for the checkout.
161153
162154
Returns path to the `cfbs.json` located in the cloned Git repository, and the Git commit hash.
163155
"""
@@ -170,20 +162,23 @@ def clone_url_repo(repo_url: str, commit: Optional[str] = None):
170162
repo_dir = os.path.join(downloads, repo_path)
171163
os.makedirs(repo_dir, exist_ok=True)
172164

173-
if commit is not None:
174-
commit_path = os.path.join(repo_dir, commit)
175-
_clone_and_checkout(repo_url, commit_path, commit)
165+
if reference is None:
166+
reference = "HEAD"
167+
168+
# always store versions of the repository in cfbs/downloads by commit hash
169+
# therefore for branches, first find the commit it points to
170+
if is_a_commit_hash(reference):
171+
commit = reference
176172
else:
177-
master_path = os.path.join(repo_dir, "master")
178-
sh("git clone %s %s" % (repo_url, master_path))
179-
commit = _get_git_repo_commit_sha(master_path)
180-
181-
commit_path = os.path.join(repo_dir, commit)
182-
if os.path.exists(commit_path):
183-
# Already cloned in the commit dir, just remove the 'master' clone
184-
sh("rm -rf %s" % master_path)
185-
else:
186-
sh("mv %s %s" % (master_path, commit_path))
173+
# `reference` is a branch
174+
commit = ls_remote(repo_url, reference)
175+
if commit is None:
176+
raise CFBSExitError(
177+
"Failed to find branch %s at %s" % (reference, repo_url)
178+
)
179+
180+
commit_path = os.path.join(repo_dir, commit)
181+
_clone_and_checkout(repo_url, commit_path, commit)
187182

188183
json_path = os.path.join(commit_path, "cfbs.json")
189184
if os.path.exists(json_path):

cfbs/module.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def attributes() -> tuple:
7070
return (
7171
"name",
7272
"version",
73+
"branch",
7374
"commit",
7475
"added_by",
7576
"steps",

cfbs/pretty.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"index",
2222
"version",
2323
"commit",
24+
"branch",
2425
"subdirectory",
2526
"dependencies",
2627
"added_by",

cfbs/utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import urllib.error
1313
from collections import OrderedDict
1414
from shutil import rmtree
15-
from typing import Iterable, List, Tuple, Union
15+
from typing import Iterable, List, Optional, Tuple, Union
1616

1717
from cfbs.pretty import pretty
1818

@@ -532,7 +532,9 @@ def fetch_url(url, target, checksum=None):
532532
) from e
533533

534534

535-
def is_a_commit_hash(commit):
535+
def is_a_commit_hash(commit: Optional[str]):
536+
if commit is None:
537+
return False
536538
return bool(SHA1_RE.match(commit) or SHA256_RE.match(commit))
537539

538540

cfbs/validate.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,17 @@ def _validate_module_commit(name, module):
502502
raise CFBSValidationError(name, '"commit" must be a commit reference')
503503

504504

505+
def _validate_module_branch(name, module):
506+
assert "branch" in module
507+
branch = module["branch"]
508+
if type(branch) is not str:
509+
raise CFBSValidationError(name, '"branch" must be of type string')
510+
if not module["url"]:
511+
raise CFBSValidationError(name, '"branch" key requires the "url" key')
512+
if not module["commit"]:
513+
raise CFBSValidationError(name, '"branch" key requires the "commit" key')
514+
515+
505516
def _validate_module_subdirectory(name, module):
506517
assert "subdirectory" in module
507518
if type(module["subdirectory"]) is not str:
@@ -741,6 +752,8 @@ def validate_single_module(context, name, module, config, local_check=False):
741752
_validate_module_version(name, module)
742753
if "commit" in module:
743754
_validate_module_commit(name, module)
755+
if "branch" in module:
756+
_validate_module_branch(name, module)
744757
if "subdirectory" in module:
745758
_validate_module_subdirectory(name, module)
746759
if "steps" in module:

0 commit comments

Comments
 (0)