feat(tools/git-r): git subcommand to display r/numbers for commits

Sadly, this can't quite be an alias (which would be difficult to
automatically set up anyways), since we want to check if an r/number is
part of the (upstream) canon branch.

The test script for the subcommand doubles up as a soundness check for
our pipelines ref creation.

Change-Id: I840af6556e50187c69490668bd8a18dd7dc25a86
Reviewed-on: https://cl.tvl.fyi/c/depot/+/8844
Tested-by: BuildkiteCI
Autosubmit: sterni <sternenseemann@systemli.org>
Reviewed-by: flokli <flokli@flokli.de>
This commit is contained in:
sterni 2023-06-22 13:41:54 +02:00
parent aa2f1bbc69
commit a72e67c8af
3 changed files with 144 additions and 0 deletions

View file

@ -35,6 +35,11 @@ steps:
# #
# Revision numbers are defined as the number of commits in the # Revision numbers are defined as the number of commits in the
# lineage of HEAD, following only the first parent of merges. # lineage of HEAD, following only the first parent of merges.
#
# Note that git does not fetch these refs by default, instead
# you'll have to modify your git config using
# `git config --add remote.origin.fetch '+refs/r/*:refs/r/*'`.
# The refs are available after the next `git fetch`.
- label: ":git:" - label: ":git:"
branches: "refs/heads/canon" branches: "refs/heads/canon"
command: | command: |

View file

@ -7,6 +7,7 @@ depot.nix.lazy-deps {
age.attr = "third_party.nixpkgs.age"; age.attr = "third_party.nixpkgs.age";
depotfmt.attr = "tools.depotfmt"; depotfmt.attr = "tools.depotfmt";
fetch-depot-inbox.attr = "tools.fetch-depot-inbox"; fetch-depot-inbox.attr = "tools.fetch-depot-inbox";
git-r.attr = "tools.git-r";
gerrit-update.attr = "tools.gerrit-update"; gerrit-update.attr = "tools.gerrit-update";
gerrit.attr = "tools.gerrit-cli"; gerrit.attr = "tools.gerrit-cli";
hash-password.attr = "tools.hash-password"; hash-password.attr = "tools.hash-password";

138
tools/git-r.nix Normal file
View file

@ -0,0 +1,138 @@
# Git subcommand loaded into the depot direnv via //tools/depot-deps that can
# display the r/number for (a) given commit(s) in depot. The r/number is a
# monotonically increasing number assigned to each commit which correspond to
# refs/r/* as created by `//ops/pipelines/static-pipeline.yaml`. They can also
# be used as TVL shortlinks and are supported by //web/atward.
{ pkgs, lib, ... }:
pkgs.writeTextFile {
name = "git-r";
destination = "/bin/git-r";
executable = true;
text = ''
set -euo pipefail
PROG_NAME="$0"
CANON_BRANCH="canon"
CANON_REMOTE="$(git config "branch.$CANON_BRANCH.remote" || echo "origin")"
CANON_HEAD="$CANON_REMOTE/$CANON_BRANCH"
usage() {
cat <<EOF
Usage: git r [-h | --usage] [<git commit> ...]
Display the r/number for the given git commit(s). If none is given,
HEAD is used as a default. The r/number is a monotonically increasing
number assigned to each commit on the $CANON_BRANCH branch in depot
equivalent to the revcount ignoring merged in branches (using
git-rev-list(1) internally).
The r/numbers displayed by \`git r\` correspond to refs created by CI
in depot, so they can be used as monotonically increasing commit
identifiers that can be used instead of a commit hash. To have
\`refs/r/*\` available locally (which is not necessary for the operation
of \`git r\`), you may have to enable fetching them like this:
git config --add remote.origin.fetch '+refs/r/*:refs/r/*'
They are created the next time you run `git fetch origin`.
EOF
exit "''${1:-0}"
}
eprintf() {
printf "$@" 1>&2
}
revs=()
if [[ $# -le 0 ]]; then
revs+=("HEAD")
fi
for arg in "$@"; do
# No flags supported at the moment
case "$arg" in
# --help is mapped to `man git-r` by git(1)
# TODO(sterni): git-r man page
-h | --usage)
usage
;;
-*)
eprintf 'error: unknown flag %s\n' "$PROG_NAME" "$arg"
usage 100 1>&2
;;
*)
revs+=("$arg")
;;
esac
done
for rev in "''${revs[@]}"; do
# Make sure $rev is well formed
git rev-parse "$rev" -- > /dev/null
if git merge-base --is-ancestor "$rev" "$CANON_HEAD"; then
printf 'r/'
git rev-list --count --first-parent "$rev"
else
eprintf 'error: refusing to calculate r/number: %s is not an ancestor of %s\n' \
"$rev" "$CANON_HEAD" 1>&2
exit 100
fi
done
'';
# Test case, assumes that it is executed in a checkout of depot
meta.ci.extraSteps.matches-refs = {
needsOutput = true;
label = "Verify `git r` output matches refs/r/*";
command = pkgs.writeShellScript "git-r-matches-refs" ''
set -euo pipefail
export PATH="${lib.makeBinPath [ pkgs.git pkgs.findutils ]}"
revs=("origin/canon" "origin/canon~1" "93a746aaaa092ffc3e7eb37e1df30bfd3a28435f")
failed=false
# assert_eq DESCRIPTION EXPECTED GIVEN
assert_eq() {
desc="$1"
exp="$2"
given="$3"
if [[ "$exp" != "$given" ]]; then
failed=true
printf 'error: case "%s" failed\n\texp:\t%s\n\tgot:\t%s\n' "$desc" "$exp" "$given" 1>&2
fi
}
git fetch origin '+refs/r/*:refs/r/*'
for rev in "''${revs[@]}"; do
assert_eq \
"r/number ref for $rev points at that rev" \
"$(git rev-parse "$rev")" \
"$(git rev-parse "$(./result/bin/git-r "$rev")")"
done
for rev in "''${revs[@]}"; do
assert_eq \
"r/number for matches ref pointing at $rev" \
"$(git for-each-ref --points-at="$rev" --format="%(refname:short)" 'refs/r/*')" \
"$(./result/bin/git-r "$rev")"
done
assert_eq \
"Passing multiple revs to git r works as expected" \
"$(git rev-parse "''${revs[@]}")" \
"$(./result/bin/git-r "''${revs[@]}" | xargs git rev-parse)"
if $failed; then
exit 1
fi
'';
};
}