about summary refs log blame commit diff
path: root/tools/git-r.nix
blob: dbda330082a363e726ca78d0a6992161fddf1ec2 (plain) (tree)









































































































































                                                                                                 
# 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
    '';
  };
}