elmarco
elmarco

Reputation: 32953

git rebase --continue and --stepback?

Is there a way to step back a commit during an interactive rebase?

Upvotes: 81

Views: 32866

Answers (5)

Lockszmith
Lockszmith

Reputation: 2551

The accepted answer by Moberg is a great find, and so are the other answers and comments on this page. All providing excellent steps and details of what goes behind the scenes.

Using the information gathred here, with a bit of trial and error, I created for myself a git-rebase--stepback script in my PATH, this allows me, on a no-modification-pending state, to run a single command to redo the last rebase step.

By default (without --confirm) output about the state would show. It will check whether there are any pending changes, and warn (preventing the git reset from losing any data unintentially)

Hope this helps out someone. And if anyone thinks this is a bad idea, I am more than willing to make changes or flat-out remove the code from here.

Invocation

# In case of pending changes, cleanup any modifications by:
> git stash
# or:
> git reset --hard HEAD

# Check what's going to happen
> git rebase--stepback
# Now that I'm satisfied with the results, add `--confirm` to perform
# the actions.
> git rebase--stepback --confirm

The code in the script

#! /usr/bin/env bash
# git rebase--stepback [--confirm] command

set -e

# Default is to just show what's going to happen - a dry run
# must pass --confirm to perform the action
arg_one="${1:-}"
DRY_RUN=1
if [[ -n "$arg_one" ]]; then
    if [[ "$arg_one" == "--confirm" ]]; then
        DRY_RUN=
    else
        printf "ERROR: Unknown arguments\n\t%s\n" "${*}"
        exit 2
    fi
fi

GIT_ROOT="$(git rev-parse --show-toplevel)"
GIT_WORKDIR="${GIT_ROOT}/.git"
REBASE_WORK_DIR=

is_in_a_rebase() {
    local r b _TMP_CURRENT_REBASE_STEP
    if [ -f "${GIT_WORKDIR}/rebase-merge/interactive" ]; then
        r="REBASE-i"
        b="$(cat "${GIT_WORKDIR}/rebase-merge/head-name")"
        REBASE_WORK_DIR="${GIT_WORKDIR}/rebase-merge"
    elif [ -d "${GIT_WORKDIR}/rebase-merge" ]; then
        r="REBASE-m"
        b="$(cat "${GIT_WORKDIR}/rebase-merge/head-name")"
        REBASE_WORK_DIR="${GIT_WORKDIR}/rebase-merge"
    else
        if [ -d "${GIT_WORKDIR}/rebase-apply" ]; then
            if [ -f "${GIT_WORKDIR}/rebase-apply/rebasing" ]; then
                r="REBASE"
            elif [ -f "${GIT_WORKDIR}/rebase-apply/applying" ]; then
                r="AM"
            else
                r="AM/REBASE"
            fi
            REBASE_WORK_DIR="${GIT_WORKDIR}/rebase-apply"
        fi
    fi
    printf 'REBASE_TYPE="%s"\n' "$r"
    printf 'REBASE_HEAD="%s"\n' "$b"
    printf 'REBASE_WORK_DIR="%s"\n' "$REBASE_WORK_DIR"
    CURRENT_REBASE_STEP_FULL="$(tail -1 "${REBASE_WORK_DIR:?No rebase directory found}/done")"
    printf 'CURRENT_REBASE_STEP_FULL="%s"\n' "$CURRENT_REBASE_STEP_FULL"
    printf 'CURRENT_REBASE_STEP_MODE="%s"\n' "$( awk '{ print $1; }' <<<"$CURRENT_REBASE_STEP_FULL" )"
    printf 'CURRENT_REBASE_STEP="%s"\n' "$( awk '{ print $2; }' <<<"$CURRENT_REBASE_STEP_FULL" )"
}

eval "$( is_in_a_rebase )"
printf '\n'

TODO_CONTENT="$(cat "${REBASE_WORK_DIR}/git-rebase-todo")"
if ! (grep -q "$CURRENT_REBASE_STEP" <<<"$TODO_CONTENT"); then
    # Only add step if it is not already in the rebase-todo
    printf 'Adding current STEP (%s) to rebase-todo:\n' "$CURRENT_REBASE_STEP"
    if [ -z "$DRY_RUN" ]; then
        [ -f "${REBASE_WORK_DIR}/git-rebase-todo.backup" ] \
        || cp "${REBASE_WORK_DIR}/git-rebase-todo" "${REBASE_WORK_DIR}/git-rebase-todo.backup"

        printf '%s\n' \
            "$CURRENT_REBASE_STEP_FULL" \
            "$TODO_CONTENT" \
            > "${REBASE_WORK_DIR}/git-rebase-todo"
    fi
else
    printf 'Current STEP (%s) found in rebase-todo\n' "$CURRENT_REBASE_STEP"
fi

printf '%s %s\n' \
    'REBASE_WORK_DIR:' "$REBASE_WORK_DIR" \
    'CURRENT_REBASE_STEP:' "$CURRENT_REBASE_STEP" \
    'CURRENT_REBASE_STEP_MODE:' "$CURRENT_REBASE_STEP_MODE" \
    'CURRENT_REBASE_STEP_FULL:' "$CURRENT_REBASE_STEP_FULL" \
    '' '' \
    'git-rebase-todo output' ''

cat "${REBASE_WORK_DIR}/git-rebase-todo" >&2

if (grep -q "$CURRENT_REBASE_STEP" "${REBASE_WORK_DIR}/git-rebase-todo"); then
    if [ -z "$(git status --porcelain)" ]; then
        printf "Performing a git reset --hard HEAD^1 so we can redo this step...\n"
        [ -z "$DRY_RUN" ] \
        && git reset --hard HEAD^1

        printf "Running git rebase --continue, which will retry the rebase step..."
        [ -z "$DRY_RUN" ] \
        && git rebase --continue
    else
        printf "WARNING: You have changes that have not been committed/stashed. can't continue"
        exit 1
    fi
fi

Upvotes: 3

Moberg
Moberg

Reputation: 5493

Yes there is. How to step back during an interactive rebase:

  1. Get the commit hash of your current HEAD, for example with git rev-parse HEAD
  2. run git rebase --edit-todo
  3. insert a pick with that hash to the top of that file pick <hash from step 1>
  4. run git reset --hard HEAD^ (this is a hard reset so if you have done anything you want to keep make sure to store it somewhere e.g. git stash before you run the command)

Now you are still in the rebase but one commit back and you are free to continue rebasing with

  1. git rebase --continue.

If you don't want the undone commit to be picked straight up without edits you can add edit <HASH> instead of pick <HASH> to the todo list (step 3).

Idea from: http://arigrant.com/blog/2014/5/4/git-rebase-stepping-forward-and-back

Addendum 3: If you are worried of missing a hash in the rebase, you can compare your todo file (.git/rebase-merge/git-rebase-todo) with the original one (.git/rebase-merge/git-rebase-todo.backup) as per comment from @Andrew Keeton .

Addendum 1 and 2: You can remember more hashes, reset to an even earlier point and add multiple picks to redo more than one commit. When you edit the todo list it also contains a large comment about the different commands available:

pick 8f8b9645c1c python: remove declare_namespace

# Rebase 803c25a8314..8f8b9645c1c onto 803c25a8314 (1 command)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.

Upvotes: 87

junvar
junvar

Reputation: 11574

Actually you can even if you're not doing interactive rebase.

As johnb003 mentioned in his comment, when rebasing, you're really making a series of new commits. By doing something such as git log --pretty=oneline --abbrev-commit, you can easily see all the commits you've already made through the rebase. Simply copy their hashes for easy reference later.

Then git rebase --abort, git rebase -i <base_branch>, copy the hashes you want to preserve, possibly change them to edit if you want to modify any of them, and continue

Upvotes: 9

sehe
sehe

Reputation: 392893

Nope, as Magnus said.

However,

  • git-rerere could come close to what you want in a way: if there were previous manual conflict resolutions that you didn't want to loose, you can enable rerere (prerecorded conflict resolutions) so that they will automatically be resolved in the same way on subsequent merges. Note that this means that you'll have to remember what part you want to resolve differently next time (presumably the goal of having a step-back in the first place?) because - well, rerere assumes you want to applies the same resolution again.

If you look at the implementation of rebase, you might be able to figure out alternative settings for GIT_WORK_TREE/GIT_DIR/GIT_INDEX; You could then perhaps use plumbing commands with a reflog for the rebase-in-progress branch?

  • this takes you deep into undocumented internals (beyond the plumbing)
  • you might just as well propose a patch to rebase that implements --step-back

Upvotes: 3

ralphtheninja
ralphtheninja

Reputation: 132988

No. You can --continue to continue rebasing (e.g. after you have solved some conflict), --abort (undo and whole rebase process) or --skip to skip current patch.

Upvotes: 0

Related Questions