Skip to content

Commit 5c2680b

Browse files
authored
Merge pull request #293 from victormlg/jakubPR
ENT-12974 Implemented absolute path Git repository module
2 parents 28886fe + 9055325 commit 5c2680b

File tree

10 files changed

+274
-18
lines changed

10 files changed

+274
-18
lines changed

JSON.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,10 @@ The modules inside `build`, `provides`, and `index` use these fields:
174174
For `provides` and `index` dictionaries, this name must be the key of each entry (not a field inside).
175175
For the `build` array, it must be inside each module object (with `name` as the key).
176176
Local modules (files and folders in same directory as `cfbs.json`), must start with `./`, and end with `/` if it's a directory.
177+
Absolute modules (a directory given by absolute path containing a Git repository) must start with `/` and end with `/`.
177178
Module names should not be longer than 64 characters.
178-
Module names (not including adfixes `./`, `/`, `.cf`, `.json` for local modules) should only contain lowercase ASCII alphanumeric characters possibly separated by dashes, and should start with a letter.
179-
Local module names can contain underscores instead of dashes.
179+
Module names (not including adfixes `./`, `/`, `.cf`, `.json` for local and absolute modules) should only contain lowercase ASCII alphanumeric characters possibly separated by dashes, and should start with a letter.
180+
Local and absolute module names can contain underscores instead of dashes.
180181
- `description` (string): Human readable description of what this module does.
181182
- `tags` (array of strings): Mostly used for information / finding modules on [build.cfengine.com](https://build.cfengine.com).
182183
Some common examples include `supported`, `experimental`, `security`, `library`, `promise-type`.

cfbs/cfbs_config.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@
3939
)
4040
from cfbs.pretty import pretty, CFBS_DEFAULT_SORTING_RULES
4141
from cfbs.cfbs_json import CFBSJson
42-
from cfbs.module import Module, is_module_added_manually, is_module_local
42+
from cfbs.module import (
43+
Module,
44+
is_module_added_manually,
45+
is_module_local,
46+
is_module_absolute,
47+
)
4348
from cfbs.prompts import prompt_user, prompt_user_yesno
4449
from cfbs.validate import validate_single_module
4550

@@ -337,8 +342,12 @@ def _handle_local_module(self, module, use_default_build_steps=True):
337342
name.startswith("./")
338343
and name.endswith((".cf", "/"))
339344
and "local" in module["tags"]
345+
) and not (
346+
name.startswith("/") and name.endswith("/") and "absolute" in module["tags"]
340347
):
341-
log.debug("Module '%s' does not appear to be a local module" % name)
348+
log.debug(
349+
"Module '%s' do not appear to be a local or absolute module" % name
350+
)
342351
return
343352

344353
if name.endswith(".cf"):
@@ -486,6 +495,12 @@ def add_command(
486495
"URI scheme not supported. The supported URI schemes are: "
487496
+ ", ".join(SUPPORTED_URI_SCHEMES)
488497
)
498+
for m in to_add:
499+
if is_module_absolute(m):
500+
if not os.path.exists(m):
501+
raise CFBSUserError("Absolute path module doesn't exist")
502+
if not os.path.isdir(m):
503+
raise CFBSUserError("Absolute path module is not a dir")
489504
self._add_modules(to_add, added_by, checksum, explicit_build_steps)
490505

491506
added = {m["name"] for m in self["build"]}.difference(before)

cfbs/commands.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def search_command(terms: List[str]):
104104
validate_single_module,
105105
)
106106
from cfbs.internal_file_management import (
107+
absolute_module_copy,
107108
clone_url_repo,
108109
SUPPORTED_URI_SCHEMES,
109110
fetch_archive,
@@ -116,11 +117,12 @@ def search_command(terms: List[str]):
116117
git_configure_and_initialize,
117118
is_git_repo,
118119
CFBSGitError,
120+
head_commit_hash,
119121
)
120122

121123
from cfbs.git_magic import commit_after_command, git_commit_maybe_prompt
122124
from cfbs.prompts import prompt_user, prompt_user_yesno
123-
from cfbs.module import Module, is_module_added_manually
125+
from cfbs.module import Module, is_module_absolute, is_module_added_manually
124126
from cfbs.masterfiles.generate_release_information import generate_release_information
125127

126128
_MODULES_URL = "https://archive.build.cfengine.com/modules"
@@ -634,6 +636,9 @@ def update_command(to_update):
634636
continue
635637

636638
new_module = provides[module_name]
639+
elif is_module_absolute(old_module["name"]):
640+
new_module = index.get_module_object(update.name)
641+
new_module["commit"] = head_commit_hash(old_module["name"])
637642
else:
638643

639644
if "version" not in old_module:
@@ -806,6 +811,10 @@ def _download_dependencies(config: CFBSConfig, redownload=False, ignore_versions
806811
local_module_copy(module, counter, max_length)
807812
counter += 1
808813
continue
814+
if name.startswith("/"):
815+
absolute_module_copy(module, counter, max_length)
816+
counter += 1
817+
continue
809818
if "commit" not in module:
810819
raise CFBSExitError("module %s must have a commit property" % name)
811820
commit = module["commit"]

cfbs/git.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import os
1212
import itertools
1313
import tempfile
14+
import shutil
1415
from subprocess import check_call, check_output, run, PIPE, DEVNULL, CalledProcessError
1516
from typing import Iterable, Union
1617

@@ -258,3 +259,26 @@ def treeish_exists(treeish, repo_path):
258259
result = run(command, cwd=repo_path, stdout=DEVNULL, stderr=DEVNULL, check=False)
259260

260261
return result.returncode == 0
262+
263+
264+
def head_commit_hash(repo_path):
265+
result = run(
266+
["git", "rev-parse", "HEAD"],
267+
cwd=repo_path,
268+
stdout=PIPE,
269+
stderr=DEVNULL,
270+
check=True,
271+
)
272+
273+
return result.stdout.decode("utf-8").strip()
274+
275+
276+
# Ensure reproducibility when copying git repositories
277+
# 1. hard reset to specific commit
278+
# 2. remove untracked files
279+
# 3. remove .git directory
280+
def git_clean_reset(repo_path, commit):
281+
run(["git", "reset", "--hard", commit], cwd=repo_path, check=True, stdout=DEVNULL)
282+
run(["git", "clean", "-fxd"], cwd=repo_path, check=True, stdout=DEVNULL)
283+
git_dir = os.path.join(repo_path, ".git")
284+
shutil.rmtree(git_dir, ignore_errors=True)

cfbs/index.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
from collections import OrderedDict
44
from typing import List, Optional, Union
55

6-
from cfbs.module import Module
6+
from cfbs.git import head_commit_hash, is_git_repo
7+
from cfbs.module import Module, is_module_absolute
78
from cfbs.utils import CFBSNetworkError, get_or_read_json, CFBSExitError, get_json
8-
from cfbs.internal_file_management import local_module_name
9+
from cfbs.internal_file_management import absolute_module_name, local_module_name
910

1011
_DEFAULT_INDEX = (
1112
"https://raw.githubusercontent.com/cfengine/build-index/master/cfbs.json"
@@ -48,6 +49,30 @@ def _local_module_data_subdir(
4849
"description": "Local subdirectory added using cfbs command line",
4950
"tags": ["local"],
5051
"steps": build_steps,
52+
# TODO: turn this into an argument, for when it's not "cfbs add" adding the module
53+
"added_by": "cfbs add",
54+
}
55+
56+
57+
def _absolute_module_data(module_name: str, version: Optional[str]):
58+
assert module_name.startswith("/")
59+
assert module_name.endswith("/")
60+
61+
if version is not None:
62+
commit_hash = version
63+
elif is_git_repo(module_name):
64+
commit_hash = head_commit_hash(module_name)
65+
else:
66+
commit_hash = ""
67+
68+
dst = os.path.join("services", "cfbs", module_name[1:])
69+
build_steps = ["directory ./ {}".format(dst)]
70+
return {
71+
"description": "Module added via absolute path to a Git repository directory",
72+
"tags": ["absolute"],
73+
"steps": build_steps,
74+
"commit": commit_hash,
75+
# TODO: turn this into an argument, for when it's not "cfbs add" adding the module
5176
"added_by": "cfbs add",
5277
}
5378

@@ -67,6 +92,14 @@ def _generate_local_module_object(
6792
return _local_module_data_json_file(module_name)
6893

6994

95+
def _generate_absolute_module_object(module_name: str, version: Optional[str]):
96+
assert module_name.startswith("/")
97+
assert module_name.endswith("/")
98+
assert os.path.isdir(module_name)
99+
100+
return _absolute_module_data(module_name, version)
101+
102+
70103
class Index:
71104
"""Class representing the cfbs.json containing the index of available modules"""
72105

@@ -171,7 +204,10 @@ def translate_alias(self, module: Module):
171204
module.name = data["alias"]
172205
else:
173206
if os.path.exists(module.name):
174-
module.name = local_module_name(module.name)
207+
if is_module_absolute(module.name):
208+
module.name = absolute_module_name(module.name)
209+
else:
210+
module.name = local_module_name(module.name)
175211

176212
def get_module_object(
177213
self,
@@ -187,6 +223,13 @@ def get_module_object(
187223

188224
if name.startswith("./"):
189225
object = _generate_local_module_object(name, explicit_build_steps)
226+
elif is_module_absolute(name):
227+
if not os.path.isdir(name):
228+
pass
229+
object = _generate_absolute_module_object(name, version)
230+
# currently, the argument of cfbs-add is split by `@` in the `Module` constructor
231+
# due to that, this hack is used to prevent creating the "version" field
232+
module = Module(name).to_dict()
190233
else:
191234
object = self[name]
192235
if version:

cfbs/internal_file_management.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@
2828
CFBSExitError,
2929
)
3030

31+
from cfbs.git import git_clean_reset
32+
3133
_SUPPORTED_TAR_TYPES = (".tar.gz", ".tgz")
3234
SUPPORTED_ARCHIVES = (".zip",) + _SUPPORTED_TAR_TYPES
3335
SUPPORTED_URI_SCHEMES = ("https://", "ssh://", "git://")
3436

3537

36-
def local_module_name(module_path):
38+
def local_module_name(module_path: str):
3739
assert os.path.exists(module_path)
3840
module = module_path
3941

@@ -64,6 +66,27 @@ def local_module_name(module_path):
6466
return module
6567

6668

69+
def absolute_module_name(module_path: str):
70+
assert os.path.exists(module_path)
71+
module = module_path
72+
assert module.startswith("/")
73+
74+
for illegal in ["//", "..", " ", "\n", "\t", " "]:
75+
if illegal in module:
76+
raise CFBSExitError("Module path cannot contain %s" % repr(illegal))
77+
78+
if not module.endswith("/"):
79+
module = module + "/"
80+
while "/./" in module:
81+
module = module.replace("/./", "/")
82+
83+
assert os.path.exists(module)
84+
if not os.path.isdir(module):
85+
raise CFBSExitError("'%s' must be a directory" % module)
86+
87+
return module
88+
89+
6790
def get_download_path(module) -> str:
6891
downloads = os.path.join(cfbs_dir(), "downloads")
6992

@@ -117,6 +140,23 @@ def local_module_copy(module, counter, max_length):
117140
)
118141

119142

143+
def absolute_module_copy(module, counter, max_length):
144+
assert "commit" in module
145+
name = module["name"]
146+
pretty_name = _prettify_name(name)
147+
target = "out/steps/%03d_%s_local/" % (counter, pretty_name)
148+
module["_directory"] = target
149+
module["_counter"] = counter
150+
151+
cp(name, target)
152+
git_clean_reset(target, module["commit"])
153+
154+
print(
155+
"%03d %s @ %s (Copied)"
156+
% (counter, pad_right(name, max_length), module["commit"][:7])
157+
)
158+
159+
120160
def _get_path_from_url(url):
121161
if not url.startswith(SUPPORTED_URI_SCHEMES):
122162
if "://" in url:

cfbs/module.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ def is_module_local(name: str):
1515
return name.startswith("./")
1616

1717

18+
def is_module_absolute(name: str):
19+
"""A module might contain `"absolute"` in its `"tags"` but this is not required.
20+
The source of truth for whether the module is absolute is whether it starts with `/`.
21+
"""
22+
return name.startswith("/")
23+
24+
1825
class Module:
1926
"""Class representing a module in cfbs.json"""
2027

0 commit comments

Comments
 (0)