about summary refs log tree commit diff
path: root/tools/git-r.nix
blob: dbda330082a363e726ca78d0a6992161fddf1ec2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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
    '';
  };
}