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 17, 2023
1 parent 522effc commit 1feb921
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 66 deletions.
53 changes: 30 additions & 23 deletions alibuild_helpers/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
from alibuild_helpers.utilities import Hasher
from alibuild_helpers.utilities import yamlDump
from alibuild_helpers.utilities import resolve_tag, resolve_version
from alibuild_helpers.git import git, clone_speedup_options
from alibuild_helpers.git import git, clone_speedup_options, Git
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.listRefsCmd()

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 @@ -363,7 +364,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()
else:
error("Cannot find .git directory in %s.", args.configDir)
return 1

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

debug("Building for architecture %s", args.architecture)
debug("Number of parallel builds: %d", args.jobs)
Expand Down Expand Up @@ -504,23 +513,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 516 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L516

Added line #L516 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 518 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L518

Added line #L518 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"].checkedOutCommitName(directory=spec["source"])

Check warning on line 525 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L525

Added line #L525 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 527 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L527

Added line #L527 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 530 in alibuild_helpers/build.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/build.py#L530

Added line #L530 was not covered by tests
develPackageBranch = out.replace("/", "-")
spec["tag"] = args.develPrefix if "develPrefix" in args else develPackageBranch
spec["commit_hash"] = "0"
Expand Down
30 changes: 30 additions & 0 deletions alibuild_helpers/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pipes import quote # Python 2.7
from alibuild_helpers.cmd import getstatusoutput
from alibuild_helpers.log import debug
from alibuild_helpers.scm import SCM

GIT_COMMAND_TIMEOUT_SEC = 120
"""How many seconds to let any git command execute before being terminated."""
Expand All @@ -16,6 +17,35 @@ def clone_speedup_options():
return ["--filter=blob:none"]
return []

class Git(SCM):
name = "Git"
def checkedOutCommitName(self, directory):
return git(("rev-parse", "HEAD"), directory)
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 28 in alibuild_helpers/git.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/git.py#L25-L28

Added lines #L25 - L28 were not covered by tests
def exec(self, *args, **kwargs):
return git(*args, **kwargs)
def parseRefs(self, output):
return {

Check warning on line 32 in alibuild_helpers/git.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/git.py#L32

Added line #L32 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 listRefsCmd(self):
return ["ls-remote", "--heads", "--tags"]

Check warning on line 37 in alibuild_helpers/git.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/git.py#L37

Added line #L37 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 41 in alibuild_helpers/git.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/git.py#L41

Added line #L41 was not covered by tests
return cmd
def fetchCmd(self, source):
return ["fetch", "-f", "--tags", source, "+refs/heads/*:refs/heads/*"]

Check warning on line 44 in alibuild_helpers/git.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/git.py#L44

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

Check warning on line 46 in alibuild_helpers/git.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/git.py#L46

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

Check warning on line 48 in alibuild_helpers/git.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/git.py#L48

Added line #L48 was not covered by tests

def git(args, directory=".", check=True, prompt=True):
debug("Executing git %s (in directory %s)", " ".join(args), directory)
Expand Down
3 changes: 2 additions & 1 deletion alibuild_helpers/init.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from alibuild_helpers.git import git
from alibuild_helpers.git import git, Git
from alibuild_helpers.utilities import getPackageList, parseDefaults, readDefaults, validateDefaults
from alibuild_helpers.log import debug, error, warning, banner, info
from alibuild_helpers.log import dieOnError
Expand Down Expand Up @@ -66,6 +66,7 @@ def doInit(args):

for p in pkgs:
spec = specs.get(p["name"])
spec["scm"] = Git()
dieOnError(spec is None, "cannot find recipe for package %s" % p["name"])
dest = join(args.develPrefix, spec["package"])
writeRepo = spec.get("write_repo", spec.get("source"))
Expand Down
19 changes: 19 additions & 0 deletions alibuild_helpers/scm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class SCM(object):
def checkedOutCommitName(self, directory):
raise NotImplementedError
def branchOrRef(self, directory):
raise NotImplementedError
def lsRemote(self, remote):
raise NotImplementedError
def listRefsCmd(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
41 changes: 20 additions & 21 deletions alibuild_helpers/workarea.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ordereddict import OrderedDict

from alibuild_helpers.log import dieOnError, debug, info, error
from alibuild_helpers.git import git, clone_speedup_options
from alibuild_helpers.git import clone_speedup_options

FETCH_LOG_NAME = "fetch-log.txt"

Expand All @@ -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)
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)

Check warning on line 54 in alibuild_helpers/workarea.py

View check run for this annotation

Codecov / codecov/patch

alibuild_helpers/workarea.py#L54

Added line #L54 was not covered by tests
dieOnError(err, "Error during %s %s for reference repo for %s." %
(scm.name.lower(), command[0], package))
info("Done %s %s for repository for %s", scm.name.lower(), command[0], package)
return output


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

scm = spec["scm"]

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)
elif fetch:
logged_git(p, referenceSources, (
"fetch", "-f", "--tags", spec["source"], "+refs/heads/*:refs/heads/*",
), referenceRepo, allowGitPrompt)
cmd = scm.fetchCmd(spec["source"])
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
Loading

0 comments on commit 1feb921

Please sign in to comment.