about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--ops/pipelines/static-pipeline.yaml5
-rw-r--r--tools/depot-deps.nix1
-rw-r--r--tools/git-r.nix138
3 files changed, 144 insertions, 0 deletions
diff --git a/ops/pipelines/static-pipeline.yaml b/ops/pipelines/static-pipeline.yaml
index 46b41480ed58..af4f9d784e60 100644
--- a/ops/pipelines/static-pipeline.yaml
+++ b/ops/pipelines/static-pipeline.yaml
@@ -35,6 +35,11 @@ steps:
   #
   # Revision numbers are defined as the number of commits in the
   # 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:"
     branches: "refs/heads/canon"
     command: |
diff --git a/tools/depot-deps.nix b/tools/depot-deps.nix
index 60af9f1378c5..480b8c2f7c34 100644
--- a/tools/depot-deps.nix
+++ b/tools/depot-deps.nix
@@ -7,6 +7,7 @@ depot.nix.lazy-deps {
   age.attr = "third_party.nixpkgs.age";
   depotfmt.attr = "tools.depotfmt";
   fetch-depot-inbox.attr = "tools.fetch-depot-inbox";
+  git-r.attr = "tools.git-r";
   gerrit-update.attr = "tools.gerrit-update";
   gerrit.attr = "tools.gerrit-cli";
   hash-password.attr = "tools.hash-password";
diff --git a/tools/git-r.nix b/tools/git-r.nix
new file mode 100644
index 000000000000..dbda330082a3
--- /dev/null
+++ b/tools/git-r.nix
@@ -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
+    '';
+  };
+}