about summary refs log tree commit diff
diff options
context:
space:
mode:
authorsterni <sternenseemann@systemli.org>2024-12-21T20·52+0100
committerclbot <clbot@tvl.fyi>2024-12-21T21·10+0000
commit00f36f20e69ea4d69f296ca98b0bc20a29625d1f (patch)
treec4dd47102ab64efb924e3dea75b998f906347986
parent7069de785750e901e7ff8733922f4e039e333ea7 (diff)
feat(sterni/git-only-push): isolate given commits and push to ref r/9013
Small git subcommand that enables you to push a subset of (independently
apply-able) commits from a local chain of commits to a remote ref, e.g.
for review. Useful for a workflow where you work on a chain of commits
and want to submit the ones that have been finished for review without
rebasing the chain.

Change-Id: I7717fe37867acdd826bc03a578104a0c3b2cbf71
Reviewed-on: https://cl.tvl.fyi/c/depot/+/12900
Reviewed-by: sterni <sternenseemann@systemli.org>
Autosubmit: sterni <sternenseemann@systemli.org>
Tested-by: BuildkiteCI
-rw-r--r--users/sterni/git-only-push/default.nix12
-rwxr-xr-xusers/sterni/git-only-push/git-only-push.sh124
2 files changed, 136 insertions, 0 deletions
diff --git a/users/sterni/git-only-push/default.nix b/users/sterni/git-only-push/default.nix
new file mode 100644
index 000000000000..9b89d24e0a4c
--- /dev/null
+++ b/users/sterni/git-only-push/default.nix
@@ -0,0 +1,12 @@
+{ pkgs, ... }:
+
+pkgs.runCommandNoCC "git-only-push"
+{
+  nativeBuildInputs = [ pkgs.buildPackages.shellcheck ];
+  buildInputs = [ pkgs.bash ];
+  src = ./git-only-push.sh;
+}
+  ''
+    shellcheck "$src"
+    install -Dm755 "$src" "$out/bin/git-only-push"
+  ''
diff --git a/users/sterni/git-only-push/git-only-push.sh b/users/sterni/git-only-push/git-only-push.sh
new file mode 100755
index 000000000000..f6b53e01e0ad
--- /dev/null
+++ b/users/sterni/git-only-push/git-only-push.sh
@@ -0,0 +1,124 @@
+#!/bin/sh
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: Copyright (c) 2024 by sterni
+#
+# WARNING: This script is not well tested and may find a way to eat your commits.
+#
+# git only-push lets you push a specific range or list of commits to a remote
+# ref based on a given revision (defaults to refs/remotes/origin/HEAD). This can
+# be useful to push a subset of commits (that are ready for review) from a local
+# commit chain to a PR branch (or gerrit style review ref).
+#
+# This is achieved by cherry-picking the relevant commits onto the base revision
+# in a temporary worktree. For this the commits need to apply independently of
+# prior commits not included in the selection, of course.
+#
+# git only-push is to be considered experimental. Its command line interface is
+# janky and may be revised.
+
+set -eu
+
+die() {
+  printf '%s: %s\n' "$(basename "$0")" "$2"
+  exit "$1"
+}
+
+usage() {
+  printf '%s\n' \
+    "git only-push [-n] [-b <rev>] -r <remote> -t <refspec> [--] <commit>..." \
+    >&2
+}
+
+base=refs/remotes/origin/HEAD
+dry=false
+
+# TODO(sterni): non-interactive mode, e.g. clean up also on cherry-pick failure
+while getopts "b:r:t:nh" opt; do
+  case $opt in
+    # TODO(sterni): it is probably too close to --branch?
+    b)
+      base="$OPTARG"
+      ;;
+    t)
+      to="$OPTARG"
+      ;;
+    r)
+      remote="$OPTARG"
+      ;;
+    n)
+      dry=true
+      ;;
+    h|?)
+      usage
+      # TODO(sterni): add man page
+      [ "$opt" = "h" ] && printf '
+\t-r <remote>\tRemote to push to.
+\t-t <refspec>\tTarget ref to push to.
+\t-b <rev>\tOptional: Base revision to cherry-pick commits onto. Defaults to refs/remotes/origin/HEAD.
+\t-n\t\tDry run.
+'
+      [ "$opt" = "h" ] && exit 0 || exit 100
+      ;;
+  esac
+done
+
+shift $((OPTIND - 1))
+
+if [ -z "${to:-}" ]; then
+  usage
+  die 100 "Missing -t flag"
+fi
+
+if [ -z "${remote:-}" ]; then
+  usage
+  die 100 "Missing -r flag"
+fi
+
+if [ "$#" -eq 0 ]; then
+  usage
+  die 100 "Missing commits"
+fi
+
+worktree=
+
+cleanup() {
+  cd "$repo"
+  test -n "$worktree" && test -e "$worktree" \
+    && git worktree remove "$worktree"
+}
+trap cleanup EXIT
+
+# Resolve ranges, get them into chronological order
+revs="$(git rev-list --no-walk "$@" | tac)"
+repo="$(git rev-parse --show-toplevel)"
+
+if $dry; then
+  printf 'Would create worktree and checkout %s\n' "$base" >&2
+else
+  worktree="$(mktemp -d)"
+  git worktree add "$worktree" "$base"
+
+  cd "$worktree"
+fi
+
+for rev in $revs; do
+  if $dry; then
+    printf 'Would cherry pick %s\n' "$rev" >&2
+  else
+    no_cherry_pick=false
+    git cherry-pick "$rev" || no_cherry_pick=true
+    if $no_cherry_pick; then
+      tmp="$worktree"
+      # Prevent cleanup from removing the worktree
+      worktree=""
+      die 101 "Could not cherry pick $rev. Please manually fixup worktree at $tmp"
+    fi
+  fi
+done
+
+if $dry; then
+  printf 'Would push resulting HEAD to %s on %s\n' "$to" "$remote" >&2
+else
+  git push "$remote" "HEAD:$to"
+  usage
+fi