From 1b1dce4bae760248a1fc3e29548a72c446e77270 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 25 Jun 2007 01:11:14 +0100 Subject: Teach rebase an interactive mode Don't you just hate the fact sometimes, that git-rebase just applies the patches, without any possibility to edit them, or rearrange them? With "--interactive", git-rebase now lets you edit the list of patches, so that you can reorder, edit and delete patches. Such a list will typically look like this: pick deadbee The oneline of this commit pick fa1afe1 The oneline of the next commit ... By replacing the command "pick" with the command "edit", you can amend that patch and/or its commit message, and by replacing it with "squash" you can tell rebase to fold that patch into the patch before that. It is derived from the script sent to the list in Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- git-rebase--interactive.sh | 289 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100755 git-rebase--interactive.sh (limited to 'git-rebase--interactive.sh') diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh new file mode 100755 index 0000000000..ab3657250e --- /dev/null +++ b/git-rebase--interactive.sh @@ -0,0 +1,289 @@ +#!/bin/sh +# +# Copyright (c) 2006 Johannes E. Schindelin + +# SHORT DESCRIPTION +# +# This script makes it easy to fix up commits in the middle of a series, +# and rearrange commits. +# +# The original idea comes from Eric W. Biederman, in +# http://article.gmane.org/gmane.comp.version-control.git/22407 + +USAGE='(--continue | --abort | --skip | [--onto ] [])' + +. git-sh-setup +require_work_tree + +DOTEST="$GIT_DIR/.dotest-merge" +TODO="$DOTEST"/todo +DONE="$DOTEST"/done +STRATEGY= +VERBOSE= + +warn () { + echo "$*" >&2 +} + +require_clean_work_tree () { + # test if working tree is dirty + git rev-parse --verify HEAD > /dev/null && + git update-index --refresh && + git diff-files --quiet && + git diff-index --cached --quiet HEAD || + die "Working tree is dirty" +} + +ORIG_REFLOG_ACTION="$GIT_REFLOG_ACTION" + +comment_for_reflog () { + case "$ORIG_REFLOG_ACTION" in + ''|rebase*) + GIT_REFLOG_ACTION="rebase -i ($1)" + export GIT_REFLOG_ACTION + esac +} + +mark_action_done () { + sed -e 1q < "$TODO" >> "$DONE" + sed -e 1d < "$TODO" >> "$TODO".new + mv -f "$TODO".new "$TODO" +} + +make_patch () { + parent_sha1=$(git rev-parse --verify "$1"^ 2> /dev/null) + git diff "$parent_sha1".."$1" > "$DOTEST"/patch +} + +die_with_patch () { + make_patch "$1" + die "$2" +} + +pick_one () { + case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac + git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" + parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null) + current_sha1=$(git rev-parse --verify HEAD) + if [ $current_sha1 = $parent_sha1 ]; then + git reset --hard $sha1 + sha1=$(git rev-parse --short $sha1) + warn Fast forward to $sha1 + else + git cherry-pick $STRATEGY "$@" + fi +} + +do_next () { + read command sha1 rest < "$TODO" + case "$command" in + \#|'') + mark_action_done + continue + ;; + pick) + comment_for_reflog pick + + mark_action_done + pick_one $sha1 || + die_with_patch $sha1 "Could not apply $sha1... $rest" + ;; + edit) + comment_for_reflog edit + + mark_action_done + pick_one $sha1 || + die_with_patch $sha1 "Could not apply $sha1... $rest" + make_patch $sha1 + warn + warn "You can amend the commit now, with" + warn + warn " git commit --amend" + warn + exit 0 + ;; + squash) + comment_for_reflog squash + + test -z "$(grep -ve '^$' -e '^#' < $DONE)" && + die "Cannot 'squash' without a previous commit" + + mark_action_done + failed=f + pick_one -n $sha1 || failed=t + MSG="$DOTEST"/message + echo "# This is a combination of two commits." > "$MSG" + echo "# The first commit's message is:" >> "$MSG" + echo >> "$MSG" + git cat-file commit HEAD | sed -e '1,/^$/d' >> "$MSG" + echo >> "$MSG" + echo "# And this is the 2nd commit message:" >> "$MSG" + echo >> "$MSG" + git cat-file commit $sha1 | sed -e '1,/^$/d' >> "$MSG" + git reset --soft HEAD^ + author_script=$(get_author_ident_from_commit $sha1) + case $failed in + f) + # This is like --amend, but with a different message + eval "$author_script" + export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE + git commit -F "$MSG" -e + ;; + t) + cp "$MSG" "$GIT_DIR"/MERGE_MSG + warn + warn "Could not apply $sha1... $rest" + warn "After you fixed that, commit the result with" + warn + warn " $(echo $author_script | tr '\012' ' ') \\" + warn " git commit -F \"$GIT_DIR\"/MERGE_MSG -e" + die_with_patch $sha1 "" + esac + ;; + *) + warn "Unknown command: $command $sha1 $rest" + die_with_patch $sha1 "Please fix this in the file $TODO." + esac + test -s "$TODO" && return + + HEAD=$(git rev-parse HEAD) + HEADNAME=$(cat "$DOTEST"/head-name) + rm -rf "$DOTEST" && + warn "Successfully rebased and updated $HEADNAME." + + exit +} + +do_rest () { + while : + do + do_next + done + test -f "$DOTEST"/verbose && + git diff --stat $(cat "$DOTEST"/head)..HEAD + exit +} + +while case $# in 0) break ;; esac +do + case "$1" in + --continue) + comment_for_reflog continue + + test -d "$DOTEST" || die "No interactive rebase running" + + require_clean_work_tree + do_rest + ;; + --abort) + comment_for_reflog abort + + test -d "$DOTEST" || die "No interactive rebase running" + + HEADNAME=$(cat "$DOTEST"/head-name) + HEAD=$(cat "$DOTEST"/head) + git symbolic-ref HEAD $HEADNAME && + git reset --hard $HEAD && + rm -rf "$DOTEST" + exit + ;; + --skip) + comment_for_reflog skip + + test -d "$DOTEST" || die "No interactive rebase running" + + git reset --hard && do_rest + ;; + -s|--strategy) + shift + case "$#,$1" in + *,*=*) + STRATEGY="-s `expr "z$1" : 'z-[^=]*=\(.*\)'`" ;; + 1,*) + usage ;; + *) + STRATEGY="-s $2" + shift ;; + esac + ;; + --merge) + # we use merge anyway + ;; + -C*) + die "Interactive rebase uses merge, so $1 does not make sense" + ;; + -v) + VERBOSE=t + ;; + -i|--interactive) + # yeah, we know + ;; + ''|-h) + usage + ;; + *) + test -d "$DOTEST" && + die "Interactive rebase already started" + + git var GIT_COMMITTER_IDENT >/dev/null || + die "You need to set your committer info first" + + comment_for_reflog start + + ONTO= + case "$1" in + --onto) + ONTO=$(git rev-parse --verify "$2") || + die "Does not point to a valid commit: $2" + shift; shift + ;; + esac + + require_clean_work_tree + + if [ ! -z "$2"] + then + git show-ref --verify --quiet "refs/heads/$2" || + die "Invalid branchname: $2" + git checkout "$2" || + die "Could not checkout $2" + fi + + HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?" + UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base" + + test -z "$ONTO" && ONTO=$UPSTREAM + + mkdir "$DOTEST" || die "Could not create temporary $DOTEST" + : > "$DOTEST"/interactive || die "Could not mark as interactive" + git symbolic-ref HEAD > "$DOTEST"/head-name || + die "Could not get HEAD" + + echo $HEAD > "$DOTEST"/head + echo $UPSTREAM > "$DOTEST"/upstream + echo $ONTO > "$DOTEST"/onto + test t = "$VERBOSE" && : > "$DOTEST"/verbose + + cat > "$TODO" << EOF +# Rebasing $UPSTREAM..$HEAD onto $ONTO +# +# Commands: +# pick = use commit +# edit = use commit, but stop for amending +# squash = use commit, but meld into previous commit +EOF + git rev-list --no-merges --pretty=oneline --abbrev-commit \ + --abbrev=7 --reverse $UPSTREAM..$HEAD | \ + sed "s/^/pick /" >> "$TODO" + + test -z "$(grep -ve '^$' -e '^#' < $TODO)" && + die "Nothing to do" + + cp "$TODO" "$TODO".backup + ${VISUAL:-${EDITOR:-vi}} "$TODO" || + die "Could not execute editor" + + git reset --hard $ONTO && do_rest + esac + shift +done -- cgit v1.3 From c54b7817f4f6bf422ea532d81217c28f63c259c5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 25 Jun 2007 18:56:55 +0100 Subject: rebase -i: several cleanups Support "--verbose" in addition to "-v", show short names in the list comment, clean up if there is nothing to do, and add several "test_ticks" in the test script. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- git-rebase--interactive.sh | 19 +++++++++++++++---- t/t3404-rebase-interactive.sh | 2 ++ 2 files changed, 17 insertions(+), 4 deletions(-) (limited to 'git-rebase--interactive.sh') diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index ab3657250e..a81432c0a5 100755 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -60,6 +60,11 @@ die_with_patch () { die "$2" } +die_abort () { + rm -rf "$DOTEST" + die "$1" +} + pick_one () { case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" @@ -212,7 +217,7 @@ do -C*) die "Interactive rebase uses merge, so $1 does not make sense" ;; - -v) + -v|--verbose) VERBOSE=t ;; -i|--interactive) @@ -264,8 +269,11 @@ do echo $ONTO > "$DOTEST"/onto test t = "$VERBOSE" && : > "$DOTEST"/verbose + SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM) + SHORTHEAD=$(git rev-parse --short $HEAD) + SHORTONTO=$(git rev-parse --short $ONTO) cat > "$TODO" << EOF -# Rebasing $UPSTREAM..$HEAD onto $ONTO +# Rebasing $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO # # Commands: # pick = use commit @@ -277,13 +285,16 @@ EOF sed "s/^/pick /" >> "$TODO" test -z "$(grep -ve '^$' -e '^#' < $TODO)" && - die "Nothing to do" + die_abort "Nothing to do" cp "$TODO" "$TODO".backup ${VISUAL:-${EDITOR:-vi}} "$TODO" || die "Could not execute editor" - git reset --hard $ONTO && do_rest + test -z "$(grep -ve '^$' -e '^#' < $TODO)" && + die_abort "Nothing to do" + + git checkout $ONTO && do_rest esac shift done diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 48aa8ea814..19a3a8e813 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -140,6 +140,7 @@ test_expect_success 'abort' ' test_expect_success 'retain authorship' ' echo A > file7 && git add file7 && + test_tick && GIT_AUTHOR_NAME="Twerp Snog" git commit -m "different author" && git tag twerp && git rebase -i --onto master HEAD^ && @@ -149,6 +150,7 @@ test_expect_success 'retain authorship' ' test_expect_success 'squash' ' git reset --hard twerp && echo B > file7 && + test_tick && GIT_AUTHOR_NAME="Nitfol" git commit -m "nitfol" file7 && echo "******************************" && FAKE_LINES="1 squash 2" git rebase -i --onto master HEAD~2 && -- cgit v1.3 From 68a163c9b483ae352fcfee8c4505d113213daa73 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 25 Jun 2007 18:58:28 +0100 Subject: rebase -i: provide reasonable reflog for the rebased branch If your rebase succeeded, the HEAD's reflog will still show the whole mess, but "@{1}" now shows the state _before_ the rebase, so that you can reset (or compare) the original and the rebased revisions more easily. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- git-rebase--interactive.sh | 10 ++++++++-- t/t3404-rebase-interactive.sh | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) (limited to 'git-rebase--interactive.sh') diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index a81432c0a5..f0f2457975 100755 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -151,8 +151,14 @@ do_next () { esac test -s "$TODO" && return - HEAD=$(git rev-parse HEAD) - HEADNAME=$(cat "$DOTEST"/head-name) + comment_for_reflog finish && + HEADNAME=$(cat "$DOTEST"/head-name) && + OLDHEAD=$(cat "$DOTEST"/head) && + SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) && + NEWHEAD=$(git rev-parse HEAD) && + message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" && + git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD && + git symbolic-ref HEAD $HEADNAME && rm -rf "$DOTEST" && warn "Successfully rebased and updated $HEADNAME." diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 19a3a8e813..9f12bb9321 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -99,6 +99,10 @@ test_expect_success 'rebase on top of a non-conflicting commit' ' test $(git rev-parse I) = $(git rev-parse HEAD~2) ' +test_expect_success 'reflog for the branch shows state before rebase' ' + test $(git rev-parse branch1@{1}) = $(git rev-parse original-branch1) +' + test_expect_success 'exchange two commits' ' FAKE_LINES="2 1" git rebase -i HEAD~2 && test H = $(git cat-file commit HEAD^ | tail -n 1) && -- cgit v1.3 From f09c9b8c5ff9d8a15499b09ccd6c3e7b3c76af77 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 25 Jun 2007 18:59:43 +0100 Subject: Teach rebase -i about --preserve-merges The option "-p" (or long "--preserve-merges") makes it possible to rebase side branches including merges, without straightening the history. Example: X \ A---M---B / ---o---O---P---Q When the current HEAD is "B", "git rebase -i -p --onto Q O" will yield X \ ---o---O---P---Q---A'---M'---B' Note that this will - _not_ touch X [*1*], it does - _not_ work without the --interactive flag [*2*], it does - _not_ guess the type of the merge, but blindly uses recursive or whatever strategy you provided with "-s " for all merges it has to redo, and it does - _not_ make use of the original merge commit via git-rerere. *1*: only commits which reach a merge base between and HEAD are reapplied. The others are kept as-are. *2*: git-rebase without --interactive is inherently patch based (at least at the moment), and therefore merges cannot be preserved. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- Documentation/git-rebase.txt | 23 ++++++++- git-rebase--interactive.sh | 110 ++++++++++++++++++++++++++++++++++++++++-- t/t3404-rebase-interactive.sh | 22 +++++++++ 3 files changed, 151 insertions(+), 4 deletions(-) (limited to 'git-rebase--interactive.sh') diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index 2e3363a617..96907d4863 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] 'git-rebase' [-i | --interactive] [-v | --verbose] [--merge] [-C] - [--onto ] [] + [-p | --preserve-merges] [--onto ] [] 'git-rebase' --continue | --skip | --abort DESCRIPTION @@ -213,6 +213,10 @@ OPTIONS Make a list of the commits which are about to be rebased. Let the user edit that list before rebasing. +-p, \--preserve-merges:: + Instead of ignoring merges, try to recreate them. This option + only works in interactive mode. + include::merge-strategies.txt[] NOTES @@ -304,6 +308,23 @@ $ git rebase -i HEAD~5 And move the first patch to the end of the list. +You might want to preserve merges, if you have a history like this: + +------------------ + X + \ + A---M---B + / +---o---O---P---Q +------------------ + +Suppose you want to rebase the side branch starting at "A" to "Q". Make +sure that the current HEAD is "B", and call + +----------------------------- +$ git rebase -i -p --onto Q O +----------------------------- + Authors ------ Written by Junio C Hamano and diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index f0f2457975..0c2a9697c4 100755 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -10,7 +10,8 @@ # The original idea comes from Eric W. Biederman, in # http://article.gmane.org/gmane.comp.version-control.git/22407 -USAGE='(--continue | --abort | --skip | [--onto ] [])' +USAGE='(--continue | --abort | --skip | [--preserve-merges] [--verbose] + [--onto ] [])' . git-sh-setup require_work_tree @@ -18,6 +19,8 @@ require_work_tree DOTEST="$GIT_DIR/.dotest-merge" TODO="$DOTEST"/todo DONE="$DOTEST"/done +REWRITTEN="$DOTEST"/rewritten +PRESERVE_MERGES= STRATEGY= VERBOSE= @@ -68,6 +71,8 @@ die_abort () { pick_one () { case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" + test -d "$REWRITTEN" && + pick_one_preserving_merges "$@" && return parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null) current_sha1=$(git rev-parse --verify HEAD) if [ $current_sha1 = $parent_sha1 ]; then @@ -79,6 +84,75 @@ pick_one () { fi } +pick_one_preserving_merges () { + case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac + sha1=$(git rev-parse $sha1) + + if [ -f "$DOTEST"/current-commit ] + then + current_commit=$(cat "$DOTEST"/current-commit) && + git rev-parse HEAD > "$REWRITTEN"/$current_commit && + rm "$DOTEST"/current-commit || + die "Cannot write current commit's replacement sha1" + fi + + # rewrite parents; if none were rewritten, we can fast-forward. + fast_forward=t + preserve=t + new_parents= + for p in $(git rev-list --parents -1 $sha1 | cut -d\ -f2-) + do + if [ -f "$REWRITTEN"/$p ] + then + preserve=f + new_p=$(cat "$REWRITTEN"/$p) + test $p != $new_p && fast_forward=f + case "$new_parents" in + *$new_p*) + ;; # do nothing; that parent is already there + *) + new_parents="$new_parents $new_p" + esac + fi + done + case $fast_forward in + t) + echo "Fast forward to $sha1" + test $preserve=f && echo $sha1 > "$REWRITTEN"/$sha1 + ;; + f) + test "a$1" = a-n && die "Refusing to squash a merge: $sha1" + + first_parent=$(expr "$new_parents" : " \([^ ]*\)") + # detach HEAD to current parent + git checkout $first_parent 2> /dev/null || + die "Cannot move HEAD to $first_parent" + + echo $sha1 > "$DOTEST"/current-commit + case "$new_parents" in + \ *\ *) + # redo merge + author_script=$(get_author_ident_from_commit $sha1) + eval "$author_script" + msg="$(git cat-file commit $sha1 | \ + sed -e '1,/^$/d' -e "s/[\"\\]/\\\\&/g")" + # NEEDSWORK: give rerere a chance + if ! git merge $STRATEGY -m "$msg" $new_parents + then + echo "$msg" > "$GIT_DIR"/MERGE_MSG + warn Error redoing merge $sha1 + warn + warn After fixup, please use + die "$author_script git commit" + fi + ;; + *) + git cherry-pick $STRATEGY "$@" || + die_with_patch $sha1 "Could not pick $sha1" + esac + esac +} + do_next () { read command sha1 rest < "$TODO" case "$command" in @@ -155,7 +229,15 @@ do_next () { HEADNAME=$(cat "$DOTEST"/head-name) && OLDHEAD=$(cat "$DOTEST"/head) && SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) && - NEWHEAD=$(git rev-parse HEAD) && + if [ -d "$REWRITTEN" ] + then + test -f "$DOTEST"/current-commit && + current_commit=$(cat "$DOTEST"/current-commit) && + git rev-parse HEAD > "$REWRITTEN"/$current_commit + NEWHEAD=$(cat "$REWRITTEN"/$OLDHEAD) + else + NEWHEAD=$(git rev-parse HEAD) + fi && message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" && git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD && git symbolic-ref HEAD $HEADNAME && @@ -226,6 +308,9 @@ do -v|--verbose) VERBOSE=t ;; + -p|--preserve-merges) + PRESERVE_MERGES=t + ;; -i|--interactive) # yeah, we know ;; @@ -274,6 +359,25 @@ do echo $UPSTREAM > "$DOTEST"/upstream echo $ONTO > "$DOTEST"/onto test t = "$VERBOSE" && : > "$DOTEST"/verbose + if [ t = "$PRESERVE_MERGES" ] + then + # $REWRITTEN contains files for each commit that is + # reachable by at least one merge base of $HEAD and + # $UPSTREAM. They are not necessarily rewritten, but + # their children might be. + # This ensures that commits on merged, but otherwise + # unrelated side branches are left alone. (Think "X" + # in the man page's example.) + mkdir "$REWRITTEN" && + for c in $(git merge-base --all $HEAD $UPSTREAM) + do + echo $ONTO > "$REWRITTEN"/$c || + die "Could not init rewritten commits" + done + MERGES_OPTION= + else + MERGES_OPTION=--no-merges + fi SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM) SHORTHEAD=$(git rev-parse --short $HEAD) @@ -286,7 +390,7 @@ do # edit = use commit, but stop for amending # squash = use commit, but meld into previous commit EOF - git rev-list --no-merges --pretty=oneline --abbrev-commit \ + git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \ --abbrev=7 --reverse $UPSTREAM..$HEAD | \ sed "s/^/pick /" >> "$TODO" diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 9f12bb9321..883cf29595 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -166,4 +166,26 @@ test_expect_success 'retain authorship when squashing' ' git show HEAD | grep "^Author: Nitfol" ' +test_expect_success 'preserve merges with -p' ' + git checkout -b to-be-preserved master^ && + : > unrelated-file && + git add unrelated-file && + test_tick && + git commit -m "unrelated" && + git checkout -b to-be-rebased master && + echo B > file1 && + test_tick && + git commit -m J file1 && + test_tick && + git merge to-be-preserved && + echo C > file1 && + test_tick && + git commit -m K file1 && + git rebase -i -p --onto branch1 master && + test $(git rev-parse HEAD^^2) = $(git rev-parse to-be-preserved) && + test $(git rev-parse HEAD~3) = $(git rev-parse branch1) && + test $(git show HEAD:file1) = C && + test $(git show HEAD~2:file1) = B +' + test_done -- cgit v1.3