feat(sterni/git-only-push): isolate given commits and push to ref
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
This commit is contained in:
parent
7069de7857
commit
00f36f20e6
2 changed files with 136 additions and 0 deletions
12
users/sterni/git-only-push/default.nix
Normal file
12
users/sterni/git-only-push/default.nix
Normal file
|
@ -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"
|
||||||
|
''
|
124
users/sterni/git-only-push/git-only-push.sh
Executable file
124
users/sterni/git-only-push/git-only-push.sh
Executable file
|
@ -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
|
Loading…
Reference in a new issue