#!/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] [-f] [-x] [-b ] -r -t [--] ..." \ >&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:nxfh" 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 ;; x) cherry_pick_x=true ;; f) # TODO(sterni): support --force-with-lease push_f=true ;; h|?) usage # TODO(sterni): add man page # shellcheck disable=SC2016 [ "$opt" = "h" ] && printf ' \t-r \tRemote to push to. \t-t \tTarget ref to push to. \t-b \tOptional: Base revision to cherry-pick commits onto. Defaults to refs/remotes/origin/HEAD. \t-x\t\tUse `git cherry-pick -x` for creating cherry-picks. \t-f\-\tForce push to remote ref. \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 ${cherry_pick_x:+-x} "$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 ${push_f:+-f} "$remote" "HEAD:$to" fi