diff options
author | sterni <sternenseemann@systemli.org> | 2024-12-21T20·52+0100 |
---|---|---|
committer | clbot <clbot@tvl.fyi> | 2024-12-21T21·10+0000 |
commit | 00f36f20e69ea4d69f296ca98b0bc20a29625d1f (patch) | |
tree | c4dd47102ab64efb924e3dea75b998f906347986 | |
parent | 7069de785750e901e7ff8733922f4e039e333ea7 (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.nix | 12 | ||||
-rwxr-xr-x | users/sterni/git-only-push/git-only-push.sh | 124 |
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 |