Skip to content

Commit

Permalink
Abstract away SCM integration
Browse files Browse the repository at this point in the history
The goal is to support different backends, like sapling.
  • Loading branch information
ktf committed Oct 10, 2023
1 parent bf63682 commit 6bafdb2
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 46 deletions.
100 changes: 78 additions & 22 deletions alibuild_helpers/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from alibuild_helpers.sync import (NoRemoteSync, HttpRemoteSync, S3RemoteSync,
Boto3RemoteSync, RsyncRemoteSync)
import yaml
from alibuild_helpers.workarea import cleanup_git_log, logged_git, updateReferenceRepoSpec
from alibuild_helpers.workarea import cleanup_git_log, logged_scm, updateReferenceRepoSpec
from alibuild_helpers.log import logger_handler, LogFormatter, ProgressPrint
from datetime import datetime
from glob import glob
Expand Down Expand Up @@ -61,26 +61,25 @@ def update_git_repos(args, specs, buildOrder, develPkgs):
"""

def update_repo(package, git_prompt):
specs[package]["scm"] = Git()

Check warning on line 64 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L64

Added line #L64 was not covered by tests
updateReferenceRepoSpec(args.referenceSources, package, specs[package],
fetch=args.fetchRepos,
usePartialClone=not args.docker,
allowGitPrompt=git_prompt)

# Retrieve git heads
cmd = ["ls-remote", "--heads", "--tags"]
scm = specs[package]["scm"]
cmd = scm.prefecthCmd()

Check warning on line 72 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L71-L72

Added lines #L71 - L72 were not covered by tests
if package in develPkgs:
specs[package]["source"] = \
os.path.join(os.getcwd(), specs[package]["package"])
cmd.append(specs[package]["source"])
else:
cmd.append(specs[package].get("reference", specs[package]["source"]))

output = logged_git(package, args.referenceSources,
output = logged_scm(scm, package, args.referenceSources,

Check warning on line 80 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L80

Added line #L80 was not covered by tests
cmd, ".", prompt=git_prompt, logOutput=False)
specs[package]["git_refs"] = {
git_ref: git_hash for git_hash, sep, git_ref
in (line.partition("\t") for line in output.splitlines()) if sep
}
specs[package]["scm_refs"] = scm.parseRefs(output)

Check warning on line 82 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L82

Added line #L82 was not covered by tests

requires_auth = set()
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
Expand All @@ -102,7 +101,7 @@ def update_repo(package, git_prompt):
(futurePackage, exc))
else:
debug("%r package updated: %d refs found", futurePackage,
len(specs[futurePackage]["git_refs"]))
len(specs[futurePackage]["scm_refs"]))

# Now execute git commands for private packages one-by-one, so the user can
# type their username and password without multiple prompts interfering.
Expand All @@ -114,7 +113,7 @@ def update_repo(package, git_prompt):
specs[package]["source"])
update_repo(package, git_prompt=True)
debug("%r package updated: %d refs found", package,
len(specs[package]["git_refs"]))
len(specs[package]["scm_refs"]))


# Creates a directory in the store which contains symlinks to the package
Expand Down Expand Up @@ -191,7 +190,7 @@ def hash_data_for_key(key):
h_default(spec["commit_hash"])
try:
# If spec["commit_hash"] is a tag, get the actual git commit hash.
real_commit_hash = spec["git_refs"]["refs/tags/" + spec["commit_hash"]]
real_commit_hash = spec["scm_refs"]["refs/tags/" + spec["commit_hash"]]

Check warning on line 193 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L193

Added line #L193 was not covered by tests
except KeyError:
# If it's not a tag, assume it's an actual commit hash.
real_commit_hash = spec["commit_hash"]
Expand All @@ -202,7 +201,7 @@ def hash_data_for_key(key):
h_real_commit(real_commit_hash)
h_alternatives = [(spec.get("tag", "0"), spec["commit_hash"], h_default),
(spec.get("tag", "0"), real_commit_hash, h_real_commit)]
for ref, git_hash in spec.get("git_refs", {}).items():
for ref, git_hash in spec.get("scm_refs", {}).items():

Check warning on line 204 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L204

Added line #L204 was not covered by tests
if ref.startswith("refs/tags/") and git_hash == real_commit_hash:
tag_name = ref[len("refs/tags/"):]
debug("Tag %s also points to %s, storing alternative",
Expand Down Expand Up @@ -269,12 +268,14 @@ def h_all(data): # pylint: disable=function-redefined
list({h.hexdigest() for _, _, h, in h_alternatives} - {spec["local_revision_hash"]})


def hash_local_changes(directory):
def hash_local_changes(spec):
"""Produce a hash of all local changes in the given git repo.
If there are untracked files, this function returns a unique hash to force a
rebuild, and logs a warning, as we cannot detect changes to those files.
"""
directory = spec["source"]
scm = spec["scm"]

Check warning on line 278 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L277-L278

Added lines #L277 - L278 were not covered by tests
untrackedFilesDirectories = []
class UntrackedChangesError(Exception):
"""Signal that we cannot detect code changes due to untracked files."""
Expand All @@ -283,10 +284,10 @@ def hash_output(msg, args):
lines = msg % args
# `git status --porcelain` indicates untracked files using "??".
# Lines from `git diff` never start with "??".
if any(line.startswith("?? ") for line in lines.split("\n")):
if any(scm.checkUntracked(line) for line in lines.split("\n")):

Check warning on line 287 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L287

Added line #L287 was not covered by tests
raise UntrackedChangesError()
h(lines)
cmd = "cd %s && git diff -r HEAD && git status --porcelain" % directory
cmd = scm.diffCmd(directory)

Check warning on line 290 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L290

Added line #L290 was not covered by tests
try:
err = execute(cmd, hash_output)
debug("Command %s returned %d", cmd, err)
Expand Down Expand Up @@ -318,6 +319,55 @@ def better_tarball(spec, old, new):
hashes = spec["local_hashes" if old_is_local else "remote_hashes"]
return old if hashes.index(old_hash) < hashes.index(new_hash) else new

class SCM(object):
def whereAmI(self, directory):
raise NotImplementedError
def branchOrRef(self, directory):
raise NotImplementedError
def lsRemote(self, remote):
raise NotImplementedError
def prefecthCmd(self):
raise NotImplementedError
def parseRefs(self, output):
raise NotImplementedError
def exec(self, *args, **kwargs):
raise NotImplementedError
def cloneCmd(self, spec, referenceRepo, usePartialClone):
raise NotImplementedError
def diffCmd(self, directory):
raise NotImplementedError
def checkUntracked(self, line):
raise NotImplementedError

class Git(SCM):
name = "Git"
def whereAmI(self, directory):
return git(("rev-parse", "HEAD"), directory)

Check warning on line 345 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L345

Added line #L345 was not covered by tests
def branchOrRef(self, directory):
out = git(("rev-parse", "--abbrev-ref", "HEAD"), directory=directory)
if out == "HEAD":
out = git(("rev-parse", "HEAD"), directory)[:10]
return out

Check warning on line 350 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L347-L350

Added lines #L347 - L350 were not covered by tests
def exec(self, *args, **kwargs):
return git(*args, **kwargs)

Check warning on line 352 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L352

Added line #L352 was not covered by tests
def parseRefs(self, output):
return {

Check warning on line 354 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L354

Added line #L354 was not covered by tests
git_ref: git_hash for git_hash, sep, git_ref
in (line.partition("\t") for line in output.splitlines()) if sep
}
def prefecthCmd(self):
return ["ls-remote", "--heads", "--tags"]

Check warning on line 359 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L359

Added line #L359 was not covered by tests
def cloneCmd(self, source, referenceRepo, usePartialClone):
cmd = ["clone", "--bare", source, referenceRepo]
if usePartialClone:
cmd.extend(clone_speedup_options())

Check warning on line 363 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L361-L363

Added lines #L361 - L363 were not covered by tests
def fetchCmd(self, source):
return ["fetch", "-f", "--tags", source, "+refs/heads/*:refs/heads/*"]

Check warning on line 365 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L365

Added line #L365 was not covered by tests
def diffCmd(self, directory):
return "cd %s && git diff -r HEAD && git status --porcelain" % directory

Check warning on line 367 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L367

Added line #L367 was not covered by tests
def checkUntracked(self, line):
return line.startswith("?? ")

Check warning on line 369 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L369

Added line #L369 was not covered by tests


def doBuild(args, parser):
if args.remoteStore.startswith("http"):
Expand Down Expand Up @@ -363,7 +413,15 @@ def doBuild(args, parser):
if not exists(specDir):
makedirs(specDir)

os.environ["ALIBUILD_ALIDIST_HASH"] = git(("rev-parse", "HEAD"), directory=args.configDir)
# if the alidist workdir contains a .git directory, we use Git as SCM
# otherwise we use Sapling
if exists("%s/.git" % args.configDir):
scm = Git()

Check warning on line 419 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L418-L419

Added lines #L418 - L419 were not covered by tests
else:
error("Cannot find .git directory in %s.", args.configDir)
return 1

Check warning on line 422 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L421-L422

Added lines #L421 - L422 were not covered by tests

os.environ["ALIBUILD_ALIDIST_HASH"] = scm.whereAmI(directory=args.configDir)

Check warning on line 424 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L424

Added line #L424 was not covered by tests

debug("Building for architecture %s", args.architecture)
debug("Number of parallel builds: %d", args.jobs)
Expand Down Expand Up @@ -504,23 +562,21 @@ def doBuild(args, parser):
# the commit_hash. If it's not a branch, it must be a tag or a raw commit
# hash, so we use it directly. Finally if the package is a development
# one, we use the name of the branch as commit_hash.
assert "git_refs" in spec
assert "scm_refs" in spec

Check warning on line 565 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L565

Added line #L565 was not covered by tests
try:
spec["commit_hash"] = spec["git_refs"]["refs/heads/" + spec["tag"]]
spec["commit_hash"] = spec["scm_refs"]["refs/heads/" + spec["tag"]]

Check warning on line 567 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L567

Added line #L567 was not covered by tests
except KeyError:
spec["commit_hash"] = spec["tag"]
# We are in development mode, we need to rebuild if the commit hash is
# different or if there are extra changes on top.
if spec["package"] in develPkgs:
# Devel package: we get the commit hash from the checked source, not from remote.
out = git(("rev-parse", "HEAD"), directory=spec["source"])
out = spec["scm"].whereAmI(directory=spec["source"])

Check warning on line 574 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L574

Added line #L574 was not covered by tests
spec["commit_hash"] = out.strip()
local_hash, untracked = hash_local_changes(spec["source"])
local_hash, untracked = hash_local_changes(spec)

Check warning on line 576 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L576

Added line #L576 was not covered by tests
untrackedFilesDirectories.extend(untracked)
spec["devel_hash"] = spec["commit_hash"] + local_hash
out = git(("rev-parse", "--abbrev-ref", "HEAD"), directory=spec["source"])
if out == "HEAD":
out = git(("rev-parse", "HEAD"), directory=spec["source"])[:10]
out = spec["scm"].branchOrRef(directory=spec["source"])

Check warning on line 579 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L579

Added line #L579 was not covered by tests
develPackageBranch = out.replace("/", "-")
spec["tag"] = args.develPrefix if "develPrefix" in args else develPackageBranch
spec["commit_hash"] = "0"
Expand Down
39 changes: 19 additions & 20 deletions alibuild_helpers/workarea.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,32 @@ def cleanup_git_log(referenceSources):
"Could not delete stale git log: %s" % exc)


def logged_git(package, referenceSources,
def logged_scm(scm, package, referenceSources,
command, directory, prompt, logOutput=True):
"""Run a git command, but produce an output file if it fails.
"""Run an SCM command, but produce an output file if it fails.
This is useful in CI, so that we can pick up git failures and show them in
This is useful in CI, so that we can pick up SCM failures and show them in
the final produced log. For this reason, the file we write in this function
must not contain any secrets. We only output the git command we ran, its exit
must not contain any secrets. We only output the SCM command we ran, its exit
code, and the package name, so this should be safe.
"""
# This might take a long time, so show the user what's going on.
info("Git %s for repository for %s...", command[0], package)
err, output = git(command, directory=directory, check=False, prompt=prompt)
info("%s %s for repository for %s...", scm.name, command[0], package)
err, output = scm.exec(command, directory=directory, check=False, prompt=prompt)

Check warning on line 43 in alibuild_helpers/workarea.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/workarea.py#L42-L43

Added lines #L42 - L43 were not covered by tests
if logOutput:
debug(output)
if err:
try:
with codecs.open(os.path.join(referenceSources, FETCH_LOG_NAME),
"a", encoding="utf-8", errors="replace") as logf:
logf.write("Git command for package %r failed.\n"
"Command: git %s\nIn directory: %s\nExit code: %d\n" %
(package, " ".join(command), directory, err))
logf.write("%s command for package %r failed.\n"

Check warning on line 50 in alibuild_helpers/workarea.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/workarea.py#L50

Added line #L50 was not covered by tests
"Command: %s %s\nIn directory: %s\nExit code: %d\n" %
(scm.name, package, scm.name.lower(), " ".join(command), directory, err))
except OSError as exc:
error("Could not write error log from git command:", exc_info=exc)
dieOnError(err, "Error during git %s for reference repo for %s." %
(command[0], package))
info("Done git %s for repository for %s", command[0], package)
error("Could not write error log from SCM command:", exc_info=exc)
dieOnError(err, "Error during %s %s for reference repo for %s." %

Check warning on line 55 in alibuild_helpers/workarea.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/workarea.py#L54-L55

Added lines #L54 - L55 were not covered by tests
(scm.name.lower(), command[0], package))
info("Done %s %s for repository for %s", scm.name.lower(), command[0], package)

Check warning on line 57 in alibuild_helpers/workarea.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/workarea.py#L57

Added line #L57 was not covered by tests
return output


Expand Down Expand Up @@ -97,6 +97,8 @@ def updateReferenceRepo(referenceSources, p, spec,
if "source" not in spec:
return

scm = spec["scm"]

Check warning on line 100 in alibuild_helpers/workarea.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/workarea.py#L100

Added line #L100 was not covered by tests

debug("Updating references.")
referenceRepo = os.path.join(os.path.abspath(referenceSources), p.lower())

Expand All @@ -114,14 +116,11 @@ def updateReferenceRepo(referenceSources, p, spec,
return None # no reference can be found and created (not fatal)

if not os.path.exists(referenceRepo):
cmd = ["clone", "--bare", spec["source"], referenceRepo]
if usePartialClone:
cmd.extend(clone_speedup_options())
logged_git(p, referenceSources, cmd, ".", allowGitPrompt)
cmd = scm.cloneCmd(spec["source"], referenceRepo, usePartialClone)
logged_scm(scm, p, referenceSources, cmd, ".", allowGitPrompt)

Check warning on line 120 in alibuild_helpers/workarea.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/workarea.py#L119-L120

Added lines #L119 - L120 were not covered by tests
elif fetch:
logged_git(p, referenceSources, (
"fetch", "-f", "--tags", spec["source"], "+refs/heads/*:refs/heads/*",
), referenceRepo, allowGitPrompt)
cmd = scm.fetchCmd(spec["source"], referenceRepo)
logged_scm(scm, p, referenceSources, cmd, referenceRepo, allowGitPrompt)

Check warning on line 123 in alibuild_helpers/workarea.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/workarea.py#L122-L123

Added lines #L122 - L123 were not covered by tests

return referenceRepo # reference is read-write

Expand Down
6 changes: 3 additions & 3 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,13 +323,13 @@ def setup_spec(script):
(root, TEST_ROOT_GIT_REFS),
(extra, TEST_EXTRA_GIT_REFS)):
spec.setdefault("requires", []).append(default["package"])
spec["git_refs"] = {ref: hash for hash, _, ref in (
spec["scm_refs"] = {ref: hash for hash, _, ref in (
line.partition("\t") for line in refs.splitlines()
)}
try:
spec["commit_hash"] = spec["git_refs"]["refs/tags/" + spec["tag"]]
spec["commit_hash"] = spec["scm_refs"]["refs/tags/" + spec["tag"]]
except KeyError:
spec["commit_hash"] = spec["git_refs"]["refs/heads/" + spec["tag"]]
spec["commit_hash"] = spec["scm_refs"]["refs/heads/" + spec["tag"]]
specs = {pkg["package"]: pkg for pkg in (default, zlib, root, extra)}

storeHashes("defaults-release", specs, isDevelPkg=False, considerRelocation=False)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_hashing.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_hashes_match_build_log(self):
self.assertEqual(spec["remote_revision_hash"], remote)
self.assertEqual(spec["local_revision_hash"], local)
# For logs produced by old hash implementations (which didn't
# consider spec["git_refs"]), alt_{remote,local} will only
# consider spec["scm_refs"]), alt_{remote,local} will only
# contain the primary hash anyway, so this works nicely.
self.assertEqual(spec["remote_hashes"], alt_remote.split(", "))
self.assertEqual(spec["local_hashes"], alt_local.split(", "))
Expand Down

0 comments on commit 6bafdb2

Please sign in to comment.