* [PATCH 1/3] Add git-sequencer shell prototype @ 2008-07-16 20:45 Stephan Beyer 2008-07-16 20:45 ` [PATCH 2/3] Add git-sequencer documentation Stephan Beyer 0 siblings, 1 reply; 8+ messages in thread From: Stephan Beyer @ 2008-07-16 20:45 UTC (permalink / raw) To: git; +Cc: Stephan Beyer, Daniel Barkalow, Christian Couder, Junio C Hamano git-sequencer is planned as a backend for user scripts that execute a sequence of git instructions and perhaps need manual intervention, for example git-rebase or git-am. Mentored-by: Christian Couder <chriscool@tuxfamily.org> Mentored-by: Daniel Barkalow <barkalow@iabervon.org> Signed-off-by: Stephan Beyer <s-beyer@gmx.net> --- .gitignore | 1 + Makefile | 1 + command-list.txt | 1 + git-sequencer.sh | 2064 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 2067 insertions(+), 0 deletions(-) create mode 100755 git-sequencer.sh diff --git a/.gitignore b/.gitignore index a213e8e..a617039 100644 --- a/.gitignore +++ b/.gitignore @@ -110,6 +110,7 @@ git-revert git-rm git-send-email git-send-pack +git-sequencer git-sh-setup git-shell git-shortlog diff --git a/Makefile b/Makefile index 9b52071..391a4ac 100644 --- a/Makefile +++ b/Makefile @@ -248,6 +248,7 @@ SCRIPT_SH += git-rebase--interactive.sh SCRIPT_SH += git-rebase.sh SCRIPT_SH += git-repack.sh SCRIPT_SH += git-request-pull.sh +SCRIPT_SH += git-sequencer.sh SCRIPT_SH += git-sh-setup.sh SCRIPT_SH += git-stash.sh SCRIPT_SH += git-submodule.sh diff --git a/command-list.txt b/command-list.txt index 3583a33..44bb5b0 100644 --- a/command-list.txt +++ b/command-list.txt @@ -101,6 +101,7 @@ git-rev-parse ancillaryinterrogators git-rm mainporcelain common git-send-email foreignscminterface git-send-pack synchingrepositories +git-sequencer plumbingmanipulators git-shell synchelpers git-shortlog mainporcelain git-show mainporcelain common diff --git a/git-sequencer.sh b/git-sequencer.sh new file mode 100755 index 0000000..6c406dc --- /dev/null +++ b/git-sequencer.sh @@ -0,0 +1,2064 @@ +#!/bin/sh +# A git sequencer prototype. + +SUBDIRECTORY_OK=Yes +OPTIONS_KEEPDASHDASH= +OPTIONS_SPEC="\ +git sequencer [options] [--] [<file>] +git sequencer (--continue | --abort | --skip | --edit | --status) +-- + Options to start a sequencing process +B,batch run in batch-mode +onto= checkout the given commit or branch first +no-advice suppress advice when pausing (conflicts, etc) +q,quiet suppress output +v,verbose be more verbose + Options to restart/change a sequencing process or show information +continue continue paused sequencer process +abort restore original branch and abort +skip skip current patch and continue +status show the status of the sequencing process +edit invoke editor to let user edit the remaining insns + Options to be used by user scripts +caller= provide information string: name|abort|cont|skip +" + +. git-sh-setup +require_work_tree + +git var GIT_COMMITTER_IDENT >/dev/null || + die 'You need to set your committer info first' + +SEQ_DIR="$GIT_DIR/sequencer" +TODO="$SEQ_DIR/todo" +DONE="$SEQ_DIR/done" +MSG="$SEQ_DIR/message" +PATCH="$SEQ_DIR/patch" +AUTHOR_SCRIPT="$SEQ_DIR/author-script" +ORIG_AUTHOR_SCRIPT="$SEQ_DIR/author-script.orig" +CALLER_SCRIPT="$SEQ_DIR/caller-script" +WHY_FILE="$SEQ_DIR/why" +MARK_PREFIX='refs/sequencer-marks' + +warn () { + echo "$*" >&2 +} + +cleanup () { + for ref in $(git for-each-ref --format='%(refname)' "$MARK_PREFIX") + do + git update-ref -d "$ref" "$ref" || return + done + rm -rf "$SEQ_DIR" +} + +print_advice () { + case "$ADVICE,$WHY" in + t,conflict) + echo " +After resolving the conflicts, mark the corrected paths with + + git add <paths> + +and run + + $(print_caller --continue) + +Note, that your working tree must match the index." + ;; + t,pause) + echo " +You can now edit files and add them to the index. +Once you are satisfied with your changes, run + + $(print_caller --continue) + +If you only want to change the commit message, run +git commit --amend before." + ;; + t,run) + echo " +Running failed: + $@ + +You can now fix the problem. When manual runs pass, +add the changes to the index and invoke + + $(print_caller --continue)" + ;; + t,todo) + echo " +Fix with git sequencer --edit or abort with $(print_caller --abort)." + ;; + esac +} + +# Die if there has been a conflict +die_to_continue () { + warn "$*" + test -z "$BATCHMODE" || + die_abort 'Aborting, because of batch mode.' + test -n "$WHY" || WHY=conflict + echo "$WHY" >"$WHY_FILE" + print_advice + exit 3 +} + +die_abort () { + restore + cleanup + die "$1" +} + +quit () { + cleanup + test -z "$*" || + echo "$*" + exit 0 +} + +perform () { + case "$VERBOSE" in + 0) + "$@" >/dev/null + ;; + 1) + output=$("$@" 2>&1 ) + status=$? + test $status -ne 0 && printf '%s\n' "$output" + return $status + ;; + 2) + "$@" + ;; + esac +} + +require_clean_work_tree () { + # test if working tree is dirty + git rev-parse --verify HEAD >/dev/null && + git update-index --ignore-submodules --refresh && + git diff-files --quiet --ignore-submodules && + git diff-index --cached --quiet HEAD --ignore-submodules -- || + die 'Working tree is dirty' +} + +ORIG_REFLOG_ACTION="$GIT_REFLOG_ACTION" + +comment_for_reflog () { + if test -z "$ORIG_REFLOG_ACTION" + then + GIT_REFLOG_ACTION='sequencer' + test -z "$CALLER" || + GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION ($CALLER)" + export GIT_REFLOG_ACTION + fi +} + +# Get commit message from commit $1 +commit_message () { + git cat-file commit "$1" | sed -e '1,/^$/d' +} + +LAST_COUNT= +mark_action_done () { + sed -e 1q <"$TODO" >>"$DONE" + sed -e 1d <"$TODO" >"$TODO.new" + mv -f "$TODO.new" "$TODO" + todo_ack "$TODO" + if test "$VERBOSE" -gt 0 + then + count=$(grep -c '^[^#]' <"$DONE") + total=$(expr "$count" + "$(grep -c '^[^#]' <"$TODO")") + if test "$LAST_COUNT" != "$count" + then + LAST_COUNT="$count" + test "$VERBOSE" -lt 1 || + printf 'Sequencing (%d/%d)\r' "$count" "$total" + test "$VERBOSE" -lt 2 || echo + fi + fi +} + +# Generate message, patch and author script files +make_patch () { + parent_sha1=$(git rev-parse --verify "$1^" 2>/dev/null || + echo '--root') + git diff-tree -p "$parent_sha1" "$1" >"$PATCH" + test -f "$MSG" || + commit_message "$1" >"$MSG" + test -f "$AUTHOR_SCRIPT" || + get_author_ident_from_commit "$1" >"$AUTHOR_SCRIPT" +} + +# Generate a patch and die with "conflict" status code +die_with_patch () { + make_patch "$1" + git rerere + die_to_continue "$2" +} + +restore () { + git rerere clear + + read HEADNAME <"$SEQ_DIR/head-name" + read HEAD <"$SEQ_DIR/head" + case $HEADNAME in + refs/*) + git symbolic-ref HEAD "$HEADNAME" + ;; + esac && + perform git reset --hard "$HEAD" +} + +has_action () { + grep '^[^#]' "$1" >/dev/null +} + +# Check if text file $1 contains a commit message +has_message () { + test -n "$(sed -n -e '/^Signed-off-by:/d;/^[^#]/p' <"$1")" +} + +# Count parents of commit $1 +count_parents() { + git cat-file commit "$1" | sed -n -e '1,/^$/p' | grep -c '^parent' +} + +# Evaluate the author script to get author information to +# apply it with "with_author git foo" then. +get_current_author () { + if test -f "$AUTHOR_SCRIPT" + then + . "$AUTHOR_SCRIPT" + else + . "$ORIG_AUTHOR_SCRIPT" + fi || die_abort 'Author script is damaged. This must not happen!' +} + +# Run command with author information +with_author () { + GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \ + GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \ + GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \ + "$@" +} + +# clean WHY_FILE and reset WHY +clean_why () { + rm -f "$WHY_FILE" + WHY= +} + +# Usage: pick_one (cherry-pick|revert) [-*|--edit] sha1 +pick_one () { + what="$1" + shift + + case "$what,$1" in + revert,*) + test "$1" != '--edit' && + what='revert --no-edit' + ;; + cherry-pick,-*) + ;; + cherry-pick,*) + # fast forward + if test "$(git rev-parse --verify "$1^" 2>/dev/null)" = \ + "$(git rev-parse --verify HEAD)" + then + perform git reset --hard "$1" + return + fi + ;; + esac + $use_perform git $what "$@" +} + +nth_string () { + case "$1" in + *1[0-9]|*[04-9]) + echo "$1th" + ;; + *1) + echo "$1st" + ;; + *2) + echo "$1nd" + ;; + *3) + echo "$1rd" + ;; + esac +} + +make_squash_message () { + if test -f "$squash_msg" + then + count=$(($(sed -n -e 's/^# This is [^0-9]*\([1-9][0-9]*\).*/\1/p' \ + <"$squash_msg" | sed -n -e '$p')+1)) + echo "# This is a combination of $count commits." + sed -e '1d' -e '2,/^./{ + /^$/d + }' <"$squash_msg" + else + count=2 + echo '# This is a combination of 2 commits.' + echo '# The first commit message is:' + echo + commit_message HEAD + fi + echo + echo "# This is the $(nth_string "$count") commit message:" + echo + commit_message "$1" +} + +make_squash_message_multiple () { + revlist=$(git rev-list --reverse "$sha1..HEAD") + count=$(echo "$revlist" | wc -l) + squash_i=0 + echo "# This is a combination of $count commits." + for cur_sha1 in $revlist + do + squash_i=$(($squash_i+1)) + if test -f "$squash_msg" + then + count=$(($count + $(sed -n -e \ + 's/^# This is [^0-9]*\([1-9][0-9]*\).*/\1/p' \ + <"$squash_msg" | sed -n -e '$p')+1)) + sed -e '1d' -e '2,/^./{ + /^$/d + }' <"$squash_msg" + fi + echo + echo "# This is the $(nth_string "$squash_i") commit message:" + echo + commit_message "$cur_sha1" + done +} + +peek_next_command () { + sed -n -e '/^#/d' -e '1s/ .*$//p' <"$TODO" +} + +# If $1 is a mark, make a ref from it; otherwise keep it. +# Note on marks: +# * :0 is allowed +# * :01 is the same as :1 +mark_to_ref () { + arg="$1" + ref=$(expr "x$arg" : 'x:0*\([0-9][0-9]*\)$') + test -n "$ref" && + arg="$MARK_PREFIX/$ref" + printf '%s\n' "$arg" +} + +mark_to_commit () { + git rev-parse --verify "$(mark_to_ref "$1")" +} + + +cannot_fallback () { + echo "$1" + die_to_continue 'Cannot fall back to three-way merge. Please hand-edit.' +} + + +fallback_3way () { + O_OBJECT=$(cd "$GIT_OBJECT_DIRECTORY" && pwd) + + rm -fr "$PATCH-merge-"* + mkdir "$PATCH-merge-tmp-dir" + + # First see if the patch records the index info that we can use. + git apply --build-fake-ancestor "$PATCH-merge-tmp-index" "$PATCH" && + GIT_INDEX_FILE="$PATCH-merge-tmp-index" \ + git write-tree >"$PATCH-merge-base+" || + cannot_fallback 'Repository lacks necessary blobs to fall back on 3-way merge.' + + echo 'Using index info to reconstruct a base tree...' + if GIT_INDEX_FILE="$PATCH-merge-tmp-index" \ + git apply --cached "$PATCH" + then + mv "$PATCH-merge-base+" "$PATCH-merge-base" + mv "$PATCH-merge-tmp-index" "$PATCH-merge-index" + else + cannot_fallback "Did you hand edit your patch? +It does not apply to blobs recorded in its index." + fi + + test -f "$PATCH-merge-index" && + his_tree=$(GIT_INDEX_FILE="$PATCH-merge-index" git write-tree) && + orig_tree=$(cat "$PATCH-merge-base") && + rm -fr "$PATCH-merge-"* || exit 1 + + echo 'Falling back to patching base and 3-way merge...' + + # This is not so wrong. Depending on which base we picked, + # orig_tree may be wildly different from ours, but his_tree + # has the same set of wildly different changes in parts the + # patch did not touch, so recursive ends up canceling them, + # saying that we reverted all those changes. + + eval GITHEAD_$his_tree='"$firstline"' + export GITHEAD_$his_tree + git merge-recursive "$orig_tree" -- HEAD "$his_tree" || { + git rerere + die 'Failed to merge in the changes.' + } +} + +# Run hook "$@" (with arguments) if executable +run_hook () { + test -z "$1" || return + hookname="$1" + hook="$GIT_DIR/hooks/$hookname" + shift + if test -x "$hook" + then + "$hook" "$@" || + die_to_continue "Hook $hookname failed." + fi +} + +# Add Signed-off-by: line if general option --signoff is given +dashdash_signoff () { + add_signoff= + if test -n "$SIGNOFF" + then + last_signed_off_by=$( + sed -n -e '/^Signed-off-by: /p' <"$MSG" | sed -n -e '$p' + ) + test "$last_signed_off_by" = "$SIGNOFF" || + add_signoff=$( + test '' = "$last_signed_off_by" && echo + echo "$SIGNOFF" + ) + fi + { + test -s "$MSG" && cat "$MSG" + test -n "$add_signoff" && echo "$add_signoff" + } >"$MSG.new" + mv "$MSG.new" "$MSG" +} + + +### --caller-related functions + +# Show string for caller invocation for --abort/--continue/--skip +print_caller_info () { + case "$1" in + --abort) + echo "$CALLER_ABRT" + ;; + --continue) + echo "$CALLER_CONT" + ;; + --skip) + echo "$CALLER_SKIP" + ;; + *) + warn 'Internal error: Unknown print_caller argument!' + ;; + esac +} + +# Print the program to invoke to (--)abort/continue/skip +print_caller() { + caller_info=$(print_caller_info "$1") + if test -n "$CALLERCOMPARE" -a -n "$caller_info" + then + test -n "$CALLER" && printf "$CALLER " + echo "$caller_info" + else + echo "git sequencer $1" + fi +} + +# Test if --caller was set correctly +# $1 must be abort/continue/skip +test_caller () { + caller_info=$(print_caller_info "--$1") + test -n "$CALLERCOMPARE" -a \ + "$CALLERCOMPARE" != "$CALLERSTRING" -a \ + -n "$caller_info" && + die "You must use '$CALLER $caller_info' to $1!" +} + +# Generate $CALLER_SCRIPT file from "git foo|--abort|--continue|--skip" +# string $1 +generate_caller_script () { + echo "$1" | sed -e 's/^\(.*\)|\(.*\)|\(.*\)|\(.*\)$/\ +CALLERCOMPARE="\0"\ +CALLER="\1"\ +CALLER_ABRT="\2"\ +CALLER_CONT="\3"\ +CALLER_SKIP="\4"/' >"$CALLER_SCRIPT" +} + +# Run caller script and set GIT_CHERRY_PICK_HELP +assure_caller () { + test -r "$CALLER_SCRIPT" && + . "$CALLER_SCRIPT" + + # we do not want cherry-pick to print *any* help + GIT_CHERRY_PICK_HELP="" + export GIT_CHERRY_PICK_HELP +} + + +### Helpers for check_* functions: + +# Print a warning on todo checking +todo_warn () { + printf 'Warning at line %d, %s: %s\n' "$line" "$command" "$*" +} + +# Raise an error on todo checking +todo_error () { + printf 'Error at line %d, %s: %s\n' "$line" "$command" "$*" + retval=1 +} + +# Test if $1 is a commit on todo checking +commit_check () { + test "$(git cat-file -t "$1" 2>/dev/null)" = commit || + todo_error "'$1' is not a commit." +} + +# A helper function to check if $1 is a an available mark during check_* +arg_is_mark_check () { + for cur_mark in $available_marks + do + test "$cur_mark" -eq "${1#:}" && return + done + todo_error "Mark $1 is not yet defined." +} + +# A helper function for check_* and mark usage: +# check if "$1" is a commit or a defined mark +arg_is_mark_or_commit_check () { + if expr "x$1" : 'x:[0-9][0-9]*$' >/dev/null + then + arg_is_mark_check "$1" + else + commit_check "$1" + fi +} + + +### Author script functions + +# Take "Name <e-mail>" in stdin and outputs author script +make_author_script_from_string () { + sed -e "s/'/'"'\\'"''/g" \ + -e 's/^\(.*\) <\(.*\)>.*$/GIT_AUTHOR_NAME='\''\1'\''\ +GIT_AUTHOR_EMAIL='\''\2'\''\ +GIT_AUTHOR_DATE=/' +} + + +### General option functions (and options spec) + +OPTIONS_GENERAL=' General options: +author= Override author +C,reuse-commit= Reuse message and authorship data from commit +F,file= Take commit message from given file +m,message= Specify commit message +M,reuse-message= Reuse message from commit +signoff Add signoff +e,edit Invoke editor to edit commit message' + +# Check if option is a general option +check_general_option () { + general_shift=1 + case "$1" in + --signoff) + return 0 + ;; + --author) + general_shift=2 + author_opt="t$author_opt" + expr "x$2" : 'x.* <.*>' >/dev/null || + todo_error "Author \"$2\" not in the correct format \"Name <e-mail>\"." + ;; + -m) + general_shift=2 + msg_opt="t$msg_opt" + ;; + -C) + general_shift=2 + msg_opt="t$msg_opt" + author_opt="t$author_opt" + commit_check "$2" + ;; + -M) + general_shift=2 + msg_opt="t$msg_opt" + commit_check "$2" + ;; + -F) + general_shift=2 + msg_opt="t$msg_opt" + test -r "$2" || + todo_error "Cannot read file '$2'." + ;; + -e) + test -z "$BATCHMODE" || + todo_error '--batch and --edit options do not make sense together.' + ;; + *) + return 1 + ;; + esac +} + +# Set a general option variable or return 1 +handle_general_option () { + general_shift=1 + case "$1" in + --signoff) + SIGNOFF=$(git var GIT_COMMITTER_IDENT | sed -e ' + s/>.*/>/ + s/^/Signed-off-by: /') + ;; + --author) + general_shift=2 + AUTHOR=t + echo "$2" | + make_author_script_from_string >"$AUTHOR_SCRIPT" + ;; + -m) + general_shift=2 + MESSAGE="$2" + ;; + -C) + general_shift=2 + AUTHOR=t + get_author_ident_from_commit "$2" >"$AUTHOR_SCRIPT" + MESSAGE=$(commit_message "$2") + ;; + -M) + general_shift=2 + MESSAGE=$(commit_message "$2") + ;; + -F) + general_shift=2 + MESSAGE=$(cat "$2") + ;; + -e) + EDIT=--edit + ;; + *) + return 1 + ;; + esac +} + + +### Functions for checking and realizing TODO instructions +# Note that options_*, check_* and insn_* function names are reserved. + +options_pause="\ +pause +-- +" + +# Check the "pause" instruction +check_pause () { + shift + test -z "$BATCHMODE" || + todo_error '"pause" instruction and --batch do not make sense together.' + test $# -eq 0 || + todo_error 'The pause instruction takes no arguments.' + return 0 +} + +# Realize the "pause" instruction +insn_pause () { + mark_action_done + make_patch HEAD + echo 'pause' >"$WHY_FILE" + WHY=pause print_advice + exit 2 +} + + +options_run="\ +run [--dir=<path>] [--] <cmd> <args>...:: +-- +dir= Change directory before running command +" + +# Check the "run" instruction +check_run () { + while test $# -gt 0 + do + case "$1" in + --dir) + test -e "$2" || + todo_error "Path $2 does not exist." + test -d "$2" || + todo_error "Path $2 is not a directory." + shift 2 + ;; + --) + shift + break + ;; + -*) + todo_error "Unknown option $1" + ;; + esac + done + # we don't check if cmd exists + return 0 +} + +# Realize the "run" instruction +insn_run () { + runpath=./ + while test $# -gt 0 + do + case "$1" in + --dir) + runpath="$2" + shift 2 + ;; + --) + shift + break + ;; + esac + done + + mark_action_done + + savepath="$PWD" + cd "$runpath" + "$@" + success="$?" + cd "$savepath" + + if test "$success" -ne 0 + then + test -z "$BATCHMODE" || + die_abort "Running $1 failed. Aborting because of batch mode." + make_patch HEAD + echo 'run' >"$WHY_FILE" + WHY=run print_advice "$@" + exit 3 + fi +} + + +options_patch="\ +patch [options] <file> +-- +3,3way Fall back to 3-way merge +k Pass to git-mailinfo (keep subject) +n Pass to git-mailinfo (no utf8) +$OPTIONS_GENERAL + Options passed to git-apply: +R,reverse Reverse changes +context= Ensure context of ... lines +p= Remove ... leading slashes +unidiff-zero Bypass unidiff checks +exclude= Do not apply changes to given files +no-add Ignore additions of patch +whitespace= Set whitespace error behavior +inaccurate-eof Support inaccurate EOFs +u no-op (backward compatibility) +binary no-op (backward compatibility) +" + +# Check the "file" instruction +check_patch () { + while test $# -gt 1 + do + case "$1" in + -3|-k|-n|-u|--binary|-R|--reverse|--unidiff-zero|--no-add|--inaccurate-eof) + : + ;; + -p|--whitespace|--exclude|--context) + shift + ;; + --) + shift + break + ;; + -*) + check_general_option "$@" || + todo_warn "Unknown option $1" + ;; + *) + todo_error "Wrong number of arguments. ($# given, 1 wanted)" + break + ;; + esac + shift $general_shift + done + + if test -f "$1" -a -r "$1" + then + grep -e '^diff' "$1" >/dev/null || + todo_error "File '$1' contains no patch." + else + todo_error "Cannot open file '$1'." + fi + return 0 +} + +# Realize the "patch" instruction +insn_patch () { + comment_for_reflog patch + + apply_opts= + mailinfo_opts= + threeway= + + # temporary files + infofile="$SEQ_DIR/patch-info" + msgfile="$SEQ_DIR/patch-msg" + + while test "$#" -gt 1 + do + case "$1" in + -3) + threeway=t + ;; + -k|-n) + mailinfo_opts="$mailinfo_opts $1" + ;; + -u|--binary) + : Do nothing. It is there due to b/c only. + ;; + -R|--reverse|--unidiff-zero|--no-add|--inaccurate-eof) + apply_opts="$apply_opts $1" + ;; + -p) + shift + apply_opts="$apply_opts -p$1" + ;; + --whitespace|--exclude) + apply_opts="$apply_opts $1=$2" + shift + ;; + --context) + shift + apply_opts="$apply_opts -C$1" + ;; + --) + shift + break + ;; + *) + handle_general_option "$@" + ;; + esac + shift $general_shift + done + + filename="$1" + + mark_action_done + + git mailinfo $mailinfo_opts "$msgfile" "$PATCH" \ + <"$filename" >"$infofile" || + die_abort 'Could not read or parse mail' + + # if author not set by option, read author information of patch + if test -z "$AUTHOR" + then + cp "$ORIG_AUTHOR_SCRIPT" "$AUTHOR_SCRIPT" + sed -e "s/'/'"'\\'"''/g" -n -e ' + s/^Author: \(.*\)$/GIT_AUTHOR_NAME='\''\1'\''/p; + s/^Email: \(.*\)$/GIT_AUTHOR_EMAIL='\''\1'\''/p; + s/^Date: \(.*\)$/GIT_AUTHOR_DATE='\''\1'\''/p + ' <"$infofile" >>"$AUTHOR_SCRIPT" + # If sed's result is empty, we keep the original + # author script by appending. + fi + + # Ignore every mail that's not containing a patch + test -s "$PATCH" || { + warn 'Does not contain patch!' + return 0 + } + + edit_msg= + if grep -e '^Subject:' "$infofile" >/dev/null + then + # add subject to commit message + sed -n -e '/^Subject:/ s/Subject: //p' <"$infofile" + echo + echo + cat "$msgfile" + else + cat "$msgfile" + edit_msg=t + fi | git stripspace >"$MSG" + rm -f "$infofile" "$msgfile" + + firstline=$(sed -e '1q' <"$MSG") + + get_current_author + + test -n "$MESSAGE" && printf '%s\n' "$MESSAGE" >"$MSG" + test -z "$firstline" && firstline=$(sed -e '1q' <"$MSG") + + dashdash_signoff + + with_author run_hook applypatch-msg "$MSG" + failed= + git apply $apply_opts --index "$PATCH" || failed=t + + if test -n "$failed" -a -n "$threeway" && (with_author fallback_3way) + then + # Applying the patch to an earlier tree and merging the + # result may have produced the same tree as ours. + git diff-index --quiet --cached HEAD -- && { + echo 'No changes -- Patch already applied.' + return 0 + # XXX: do we want that? + } + # clear apply_status -- we have successfully merged. + failed= + fi + + if test -n "$failed" + then + die_to_continue 'Patch failed.' + # XXX: We actually needed a git-apply flag that creates + # conflict markers and sets the DIFF_STATUS_UNMERGED flag. + fi + + with_author run_hook pre-applypatch + + test -n "$EDIT" && edit_msg=t + if ! has_message "$MSG" || test -n "$edit_msg" + then + echo " +# Please enter the commit message for the applied patch. +# (Comment lines starting with '#' will not be included)" >>"$MSG" + + git_editor "$MSG" || + die_with_patch 'Editor returned error.' + has_message "$MSG" || + die_with_patch 'No commit message given.' + fi + + tree=$(git write-tree) && + parent=$(git rev-parse --verify HEAD) && + commit=$(with_author git commit-tree "$tree" -p "$parent" <"$MSG") && + git update-ref -m "$GIT_REFLOG_ACTION: $firstline" HEAD "$commit" "$parent" || + die_to_continue 'Could not commit tree.' + + test -x "$GIT_DIR/hooks/post-applypatch" && + with_author "$GIT_DIR/hooks/post-applypatch" + + return 0 +} + + +options_pick="\ +pick [options] <commit> +-- +R,reverse Revert introduced changes +mainline= Specify parent number to use for merge commits +$OPTIONS_GENERAL +" + +# Check the "pick" instruction +check_pick () { + mainline= + while test $# -gt 1 + do + case "$1" in + -R) + ;; + --mainline) + shift + mainline="$1" + test "$mainline" -gt 0 || { + todo_error '--mainline needs an integer beginning from 1.' + mainline= + } + ;; + --) + shift + break + ;; + -*) + check_general_option "$@" || + todo_warn "Unknown option $1" + ;; + *) + todo_error "Wrong number of arguments. ($# given, 1 wanted)" + break + ;; + esac + shift $general_shift + done + + if test -n "$mainline" + then + parents=$(count_parents "$1") + test "$parents" -lt "$mainline" && + todo_error "Commit has only $parents (less than $mainline) parents." + test "$parents" -eq 1 && + todo_warn 'Commit is not a merge at all.' + fi + + commit_check "$1" + + return 0 +} + +# Realize the "pick" instruction +insn_pick () { + op=cherry-pick + mainline= + while test $# -gt 1 + do + case "$1" in + -R) + op=revert + ;; + --mainline) + mainline="$1 $2" + shift + ;; + --) + shift + break + ;; + -*) + handle_general_option "$@" + ;; + esac + shift $general_shift + done + + sha1=$(git rev-parse --verify "$1") + + comment_for_reflog pick + + mark_action_done + + edit_msg="$EDIT" + + # Don't edit on pick, but later, if author or message given. + test -n "$AUTHOR" -o -n "$MESSAGE" && edit_msg= + + # Be kind to users and ignore --mainline=1 on non-merge commits + test -n "$mainline" -a 2 -gt $(count_parents "$sha1") && mainline= + + use_perform= + test -n "$edit_msg" || + use_perform=perform + + pick_one "$op" $edit_msg $mainline $sha1 || + die_with_patch $sha1 "Could not apply $sha1" + + test -n "$EDIT" || + use_perform=perform + + get_current_author + signoff= + test -n "$SIGNOFF" && signoff=-s + if test -n "$AUTHOR" -a -n "$MESSAGE" + then + # this is just because we only want to do ONE amending commit + $use_perform git commit --amend $EDIT $signoff --no-verify \ + --author "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>" \ + --message="$MESSAGE" + elif test -n "$AUTHOR" + then + # correct author if AUTHOR is set + $use_perform git commit --amend $EDIT --no-verify -C HEAD \ + --author "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>" + elif test -n "$MESSAGE" + then + # correct commit message if MESSAGE is set + $use_perform git commit --amend $EDIT $signoff --no-verify \ + -C HEAD --message="$MESSAGE" + elif test -n "$SIGNOFF" + then + # only add signoff + $use_perform git commit --amend $EDIT $signoff --no-verify \ + -C HEAD + fi + + return 0 +} + +options_edit="\ +edit <commit> +-- +" + +# Check the "edit" instruction +check_edit () { + shift + test -z "$BATCHMODE" || + todo_error '"edit" instruction and --batch do not make sense together.' + test $# -eq 1 || + todo_error "Wrong number of arguments. ($# given, 1 wanted)" + return 0 +} + +# Realize the "edit" instruction +insn_edit () { + shift + insn_pick "$1" + + # work around mark_action_done in insn_pause + echo '# pausing' >"$TODO.new" + cat "$TODO" >>"$TODO.new" + mv -f "$TODO.new" "$TODO" + insn_pause +} + + +options_squash="\ +squash <commit> +squash [options] --from <mark> +-- +from Squash all commits from <mark> +collect-signoffs Collect Signed-off-by: lines +include-merges Do not fail on merge commits +$OPTIONS_GENERAL +" + +# Check the "squash" instruction +check_squash () { + from= + collect= + merges= + while test $# -gt 1 + do + case "$1" in + --from) + from=t + ;; + --collect-signoffs) + collect=t + todo_warn 'Not yet implemented.' + ;; + --include-merges) + merges=t + ;; + --) + shift + break + ;; + *) + check_general_option "$@" || + todo_error "Unknown option $1" + ;; + esac + shift $general_shift + done + + # in --from mode? + if test -n "$from" + then + test -z "$merges" && + cat "$DONE" "$TODO" | + sed -n -e '/^[ \t]*mark[ \t]*:\{0,1\}'"${1#:}"'\($\|[^0-9]\)/,'"$line"'p' | + grep '^[ \t]*merge' >/dev/null && + todo_error "$1..HEAD contains a merge commit. You may try --include-merges." + + arg_is_mark_check "$1" + else + test -n "$merges" && + todo_error '--include-merges only makes sense with --from <mark>.' + test -n "$collect" && + todo_error '--collect-signoffs only makes sense with --from <mark>.' + + commit_check "$1" + fi + + return 0 +} + +# Realize the "squash" instruction +insn_squash () { + squash_msg="$MSG-squash" + from= + while test $# -gt 1 + do + case "$1" in + --from) + from=t + ;; + --collect-signoffs) + warn '--collect-signoffs is not implemented.' + # XXX + ;; + --include-merges) + : # This has to be done during check_squash + ;; + --) + shift + break + ;; + *) + handle_general_option "$@" + ;; + esac + shift $general_shift + done + + if test -n "$from" + then + sha1=$(mark_to_commit ":${1#:}") + else + sha1=$(git rev-parse --verify "$1") + fi + + comment_for_reflog squash + + # Hm, somehow I don't think --skip on a conflicting squash + # may be useful, but if someone wants to do it, it should + # do the obvious: skip what squash would do. + echo "$(git rev-parse HEAD)" >"$SEQ_DIR/skiphead" + + mark_action_done + + if test -n "$MESSAGE" + then + printf '%s\n' "$MESSAGE" >"$MSG" + else + if test -n "$from" + then + make_squash_message_multiple "$sha1" >"$MSG" + else + make_squash_message "$sha1" >"$MSG" + fi + fi + + case "$(peek_next_command)" in + squash) + edit_commit= + use_perform=perform + cp "$MSG" "$squash_msg" + ;; + *) + edit_commit=-e + use_perform= + rm -f "$squash_msg" || exit + ;; + esac + + test -n "$MESSAGE" && edit_commit= + test -n "$EDIT" && edit_commit=-e + + # is --author (or equivalent) set? + if test -n "$AUTHOR" + then + # evaluate author script + get_current_author + else + # if --author is not given, we get the authorship + # information from the commit before. + eval "$(get_author_ident_from_commit HEAD)" + # but we do not write an author script + fi + + # --from or not + failed= + if test -n "$from" + then + perform git reset --soft "$sha1" + else + perform git reset --soft HEAD^ + + pick_one cherry-pick -n "$sha1" || failed=t + fi + + dashdash_signoff + + if test -z "$failed" + then + # This is like --amend, but with a different message + with_author $use_perform git commit --no-verify \ + -F "$MSG" $edit_commit || failed=t + else + cp "$MSG" "$GIT_DIR/MERGE_MSG" + warn + warn "Could not apply $sha1..." + die_with_patch $sha1 "" + fi + + return 0 +} + + +options_mark="\ +mark <mark> +-- +" + +# Check the "mark" instruction +check_mark () { + shift + test $# -eq 1 || + todo_error "Wrong number of arguments. ($# given, 1 wanted)" + my_mark=$(expr "x${1#:}" : 'x0*\([0-9][0-9]*\)$') + test -n "$my_mark" || + todo_error "Mark $1 not an integer." + expr "x$available_marks " : " $my_mark " >/dev/null && + todo_error "Mark :$my_mark already defined. Choose another integer." + available_marks="$available_marks $my_mark" + + return 0 +} + +# Realize the "mark" instruction +insn_mark () { + shift + given="$1" + + mark_action_done + + mark=$(mark_to_ref ":${given#:}") + git update-ref "$mark" HEAD + return 0 +} + + +options_merge="\ +merge [options] <commit-ish> ... +-- +standard Generate default commit message +s,strategy= Use merge strategy ... +$OPTIONS_GENERAL +" + +# Check the "merge" instruction +check_merge () { + while test $# -gt 1 + do + case "$1" in + --standard) + msg_opt="t$msg_opt" + ;; + -s) + shift + ;; + --) + shift + break + ;; + -*) + check_general_option "$@" || + todo_error "Unknown option $1" + ;; + esac + shift $general_shift + done + + test $# -gt 0 || + todo_error 'What are my parents? Need new parents!' + + while test $# -gt 0 + do + arg_is_mark_or_commit_check "$1" + shift + done + return 0 +} + +# Realize the "merge" instruction +insn_merge () { + comment_for_reflog merge + + standard= + + while test $# -gt 1 + do + case "$1" in + --standard) + standard=t + ;; + -s) + shift + strategy="-s $1" + ;; + --) + shift + break + ;; + -*) + handle_general_option "$@" + ;; + esac + shift $general_shift + done + + new_parents= + for p in "$@" + do + new_parents="$new_parents $(mark_to_ref $p)" + done + new_parents="${new_parents# }" + + get_current_author + + if test -n "$standard" + then + for cur_parent in $new_parents + do + printf '%s\t\t%s' \ + "$(git rev-parse "$cur_parent")" "$cur_parent" + done | git fmt-merge-msg >"$MSG" + fi + + test -n "$MESSAGE" && + printf '%s\n' "$MESSAGE" >"$MSG" + + dashdash_signoff + if ! has_message "$MSG" || test -n "$EDIT" + then + echo " +# Please enter the merge commit message. +# (Comment lines starting with '#' will not be included)" >>"$MSG" + + git_editor "$MSG" || + die_with_patch 'Editor returned error.' + has_message "$MSG" || + die_with_patch 'No commit message given.' + fi + + mark_action_done + if ! with_author perform git merge $strategy -m junk $new_parents + then + git rerere + cp "$MSG" "$GIT_DIR/MERGE_MSG" + die_to_continue 'Error merging' + fi + with_author perform git commit --amend -F "$MSG" --no-verify + return 0 +} + + +options_reset="\ +reset <commit-ish> +-- +" + +# Check the "reset" instruction +check_reset () { + shift + test $# -eq 1 || + todo_error "Wrong number of arguments. ($# given, 1 wanted)" + arg_is_mark_or_commit_check "$1" + + return 0 +} + +# Realize the "reset" instruction +insn_reset () { + shift + comment_for_reflog reset + + mark_action_done + perform git reset --hard "$(mark_to_commit "$1")" +} + + +options_ref="\ +ref <ref> +-- +" + +# Check the "ref" instruction +check_ref () { + shift + test $# -eq 1 || + todo_error "Wrong number of arguments. ($# given, 1 wanted)" + return 0 +} + +# Realize the "ref" instruction +insn_ref () { + shift + comment_for_reflog ref + + mark_action_done + perform git update-ref "$1" HEAD +} + + +### Instruction main loop + +# Run check_* or insn_* with massaged options +# Usage: run_insn (check|do) <insn> <insn options> +run_insn () { + c_or_i="$1" + insn="$2" + shift + shift + eval "option_spec=\"\$options_$insn\"" + eval "$(printf "%s" "$option_spec" | + git rev-parse --parseopt -- "$@" || + echo return 1)" + case "$c_or_i" in + check) + check_$insn "$@" + ;; + do) + insn_$insn "$@" + ;; + esac +} + +# Execute the first line of the current TODO file +execute_next () { + read -r command rol <"$TODO" + test "$VERBOSE" -gt 1 && + printf 'Next line: %s\n' "$command $rol" + + case "$command" in + '#'*|'') + mark_action_done + ;; + *) + # reset general options + rm -f "$AUTHOR_SCRIPT" "$MSG" + echo 'HEAD' >"$SEQ_DIR/skiphead" + general_shift=1 + AUTHOR= + EDIT= + MESSAGE= + SIGNOFF= + # XXX: eval is evil! + eval "run_insn do $command $rol" || + die_to_continue 'An unexpected error occured.' + ;; + esac +} + +# Execute the rest of the TODO file and finish +execute_rest () { + while has_action "$TODO" + do + execute_next + done + + comment_for_reflog finish + if test -n "$ONTO" + then + git update-ref -m "$GIT_REFLOG_ACTION: $ONTO" "$ONTO" HEAD && + git symbolic-ref HEAD "$ONTO" + fi && + quit +} + +# We don't need to check a todo file if it has not changed +# since it has last been acknowledged to be sane. +todo_has_changed () { + test -f "$1.sum" || return 0 + test "$(git hash-object "$1")" != "$(cat "$1.sum")" +} + +# acknowledge todo file to be sane +todo_ack () { + git hash-object "$1" >"$1.sum" +} + +# Main loop to check instructions +todo_check () { + todo="$TODO" + test -n "$1" && todo="$1" + todo_has_changed "$todo" || return 0 + + test "$VERBOSE" -lt 1 || printf 'Checking...\r' + available_marks=' ' + for cur_mark in $(git for-each-ref --format='%(refname)' "$MARK_PREFIX") + do + available_marks="$available_marks ${cur_mark##*/}" + done + + retval=0 + line=1 + while read -r command rol + do + case "$command" in + '#'*|'') + ;; + *) + eval 'test -n "$options_'"$command"'"' || { + retval=1 + todo_error "Unknown $command instruction" + continue + } + + general_shift=1 + msg_opt= + author_opt= + eval "run_insn check $command $rol" || + todo_error "Unknown option used" + expr "$msg_opt" : 'ttt*' >/dev/null && + todo_error 'You can only provide one commit message option.' + expr "$author_opt" : 'ttt*' >/dev/null && + todo_error 'You can only provide one author option.' + ;; + esac + line=$(expr "$line" + 1) + done <"$todo" + test $retval -ne 0 || todo_ack "$todo" + return $retval +} + +prepare_editable_todo () { + echo '# ALREADY DONE:' + sed -e 's/^/# /' <"$DONE" + echo '# ' + echo "$markline" + cat "$TODO" +} + +# expand shortcuts in TODO file $1 +expand_shortcuts () { + sed -e ' + s/^[ \t]*p\>/pick/; + s/^[ \t]*e\>/edit/; + s/^[ \t]*s\>/squash/; + ' <"$1" >"$TODO.cut" + mv "$TODO.cut" "$1" +} + +get_saved_options () { + read VERBOSE <"$SEQ_DIR/verbose" + read ADVICE <"$SEQ_DIR/advice" + read ONTO <"$SEQ_DIR/onto" + test -f "$WHY_FILE" && + read WHY <"$WHY_FILE" + return 0 +} + +# Realize sequencer invocation +do_startup () { + test -d "$SEQ_DIR" && + die 'sequencer already started' + + require_clean_work_tree + + HEAD=$(git rev-parse --verify HEAD) || + die 'No HEAD?' + + mkdir "$SEQ_DIR" || + die "Could not create temporary $SEQ_DIR" + + # save options + echo "$VERBOSE" >"$SEQ_DIR/verbose" + echo "$ADVICE" >"$SEQ_DIR/advice" + test -n "$CALLERSTRING" && + generate_caller_script "$CALLERSTRING" + # generate empty DONE and "onto" file + : >"$DONE" + : >"$SEQ_DIR/onto" + + if test -n "$BATCHMODE" + then + GIT_CHERRY_PICK_HELP=' Aborting (batch mode)' + export GIT_CHERRY_PICK_HELP + else + assure_caller + fi + + comment_for_reflog start + + # save old head before checking out the given <branch> + git symbolic-ref HEAD >"$SEQ_DIR/head-name" 2>/dev/null || + echo 'detached HEAD' >"$SEQ_DIR/head-name" + echo $HEAD >"$SEQ_DIR/head" + # do it here so that die_abort can work ;) + + if test -n "$ONTO" + then + # if ONTO is a branch name, then keep it, otherwise + # we don't care anymore and erase ONTO. + if git-show-ref --quiet --verify -- "refs/heads/${ONTO##*/}" + then + ONTO="refs/heads/${ONTO##*/}" + echo "$ONTO" >"$SEQ_DIR/onto" + perform git checkout "$(git rev-parse "$ONTO")" || + die_abort "Could not checkout branch $ONTO" + else + perform git checkout "$ONTO" || + die_abort "Could not checkout commit $ONTO" + ONTO= + fi + fi + + (git var GIT_AUTHOR_IDENT || git var COMMITTER_IDENT) | + make_author_script_from_string >"$ORIG_AUTHOR_SCRIPT" + + # read from file or from stdin? + if test -z "$1" + then + : >"$TODO" || + die_abort "Could not generate TODO file $TODO" + while read -r line + do + printf '%s\n' "$line" >>"$TODO" || + die_abort "Could not append to TODO file $TODO" + done + else + cp "$1" "$TODO" || + die_abort "Could not find TODO file $1." + fi + expand_shortcuts "$TODO" + + has_action "$TODO" || die_abort 'Nothing to do' + todo_check || WHY=todo die_to_continue "TODO file contains errors." + + execute_rest + exit +} + +# Realize --continue. +do_continue () { + test -d "$SEQ_DIR" || die 'No sequencer running' + test_caller 'continue' + + expand_shortcuts "$TODO" + todo_check || WHY=todo die_to_continue "TODO file contains errors." + + comment_for_reflog continue + + # Sanity check + git rev-parse --verify HEAD >/dev/null || + die_to_continue 'Cannot read HEAD' + git update-index --ignore-submodules --refresh && + git diff-files --quiet --ignore-submodules || + die_to_continue 'Working tree is dirty. (Use git add or git stash first?)' + + get_saved_options + + # do we have anything to commit? (staged changes) + if ! git diff-index --cached --quiet --ignore-submodules HEAD -- + then + get_current_author + + # After "pause", we should amend (merge-safe!). + # On conflict, we do not have a commit to amend, so we + # should just commit. + case "$WHY" in + pause|run) + with_author git commit --amend --no-verify -F "$MSG" -e || + die_to_continue 'Could not commit staged changes.' + ;; + conflict) + with_author git commit --no-verify -F "$MSG" -e || + die_to_continue 'Could not commit staged changes.' + echo '# resolved CONFLICTS of the last instruction' >>"$DONE" + ;; + *) + die_to_continue 'There are staged changes. Do not know what to do with them.' + esac + fi + + require_clean_work_tree + clean_why + execute_rest + exit +} + +# Realize --abort. +do_abort () { + test -d "$SEQ_DIR" || die 'No sequencer running' + test_caller 'abort' + + comment_for_reflog abort + git rerere clear + restore + quit +} + +# Realize --skip. +do_skip () { + test -d "$SEQ_DIR" || die 'No sequencer running' + test_caller 'skip' + + expand_shortcuts "$TODO" + todo_check || WHY=todo die_to_continue "TODO file contains errors." + get_saved_options + + comment_for_reflog skip + git rerere clear + clean_why + + perform git reset --hard "$(cat "$SEQ_DIR/skiphead")" && + echo '# SKIPPED the last instruction' >>"$DONE" && + execute_rest + exit +} + +# Realize --edit. +do_edit () { + test -d "$SEQ_DIR" || die 'No sequencer running' + + markline='### BEGIN EDITING BELOW THIS LINE ###' + prepare_editable_todo >"$TODO.new" + if todo_has_changed "$TODO" + then + sane= + else + sane=t + todo_ack "$TODO.new" + fi + + # XXX: does not make sense + # when input does not come from a terminal + git_editor "$TODO.new" || + die 'Editor returned an error.' + + if test -t 0 -a -t 1 + then + sane=t + echo + # interactive: + until has_action "$TODO.new" && todo_check "$TODO.new" + do + has_action "$TODO.new" || echo 'TODO file empty.' + printf 'What to do with the file? [c]orrect/[e]dit again/[r]ewind/[s]ave/[?] ' + read reply + case "$reply" in + [cC]*) + git_editor "$TODO.new" + expand_shortcuts "$TODO.new" + ;; + [eE]*) + prepare_editable_todo >"$TODO.new" + git_editor "$TODO.new" + expand_shortcuts "$TODO.new" + ;; + [rRxXqQ]*) + rm -f "$TODO.new" "$TODO.new.sum" + exit 0 + ;; + [sS]*) + test "$WHY" != 'pause' || + echo 'todo' >"$WHY_FILE" + sane= + break + ;; + [?hH]*) + cat <<EOF + +Help: +s - save TODO file and exit +c - respawn editor to correct TODO file +e - drop changes and respawn editor on original TODO file +r - drop changes and exit as if nothing happened +? - print this help +EOF + ;; + esac + echo + done + else + # defaults: + has_action "$TODO.new" || quit "Nothing to do" + todo_check "$TODO.new" || + die 'TODO file contains errors. Aborting.' + fi + cp "$TODO" "$TODO.old" + if grep "^$markline" "$TODO.new" >/dev/null + then + sed -e "1,/^$markline/d" <"$TODO.new" >"$TODO" + else + cp "$TODO.new" "$TODO" + fi + rm -f "$TODO.new" "$TODO.new.sum" + test -z "$sane" || todo_ack "$TODO" + echo + echo 'TODO file contains:' + echo + cat "$TODO" + exit 0 +} + +# Realize --status. +do_status () { + test -d "$SEQ_DIR" || die 'No sequencer running.' + get_saved_options + + if has_action "$DONE" + then + echo 'Already done (or tried):' + sed -e 's/^/ /' <"$DONE" + echo + fi + case "$WHY" in + pause) + echo 'Intentionally paused.' + ;; + run) + echo 'Interrupted because running failed.' + ;; + conflict) + echo 'Interrupted by conflict at' + sed -n -e 's/^/ /;$p' <"$DONE" + ;; + todo) + echo 'Interrupted because of errors in the TODO file.' + ;; + *) + echo 'Current state is broken.' + esac + test "$VERBOSE" -gt 1 && echo 'Running verbosely.' + test "$VERBOSE" -lt 1 && echo 'Running quietly.' + test -n "$ONTO" && + echo "Sequencing on branch ${ONTO##*/}." + if has_action "$TODO" + then + echo + + echo 'Still to do:' + sed -e 's/^/ /' <"$TODO" + fi + if test "$WHY" = todo + then + echo + echo 'But there are errors. To edit, run:' + echo ' git sequencer --edit' + else + echo + echo 'To abort & restore, invoke:' + echo " $(print_caller --abort)" + echo 'To continue, invoke:' + echo " $(print_caller --continue)" + test "$WHY" = 'pause' -o "$WHY" = 'run' || { + echo 'To skip the current instruction, invoke:' + echo " $(print_caller --skip)" + } + fi + exit 0 +} + +is_standalone () { + test $# -eq 2 && + test "$2" = '--' && + test -z "$BATCHMODE" && + test "$ADVICE" = t && + test -z "$onto" && + test "$VERBOSE" -eq 1 +} + + +### Option handling and startup + +onto= +BATCHMODE= +CALLERSTRING= +VERBOSE=1 +ADVICE=t +CALLER= +CALLER_ABRT= +CALLER_CONT= +CALLER_SKIP= +while test $# -gt 0 +do + case "$1" in + --continue) + is_standalone "$@" || usage + assure_caller + do_continue + ;; + --abort) + is_standalone "$@" || usage + assure_caller + do_abort + ;; + --skip) + is_standalone "$@" || usage + assure_caller + do_skip + ;; + --status) + is_standalone "$@" || usage + assure_caller + do_status + ;; + --edit) + is_standalone "$@" || usage + do_edit + ;; + --caller) + ############################################################### + # This feature is for user scripts only. (Hence undocumented.) + # User scripts should pass an argument like: + # --caller="git foo|abrt|go|next" + # on every git sequencer call. (It is only ignored on + # --edit and --status.) + # So git sequencer knows that + # "git foo abrt" will abort, + # "git foo go" will continue and + # "git foo next" will skip the sequencing process. + # This is useful if your user script does some extra + # preparations or cleanup before/after calling + # git sequencer --caller="..." --abort|--continue|--skip + # + # Running git-sequencer without the same --caller string + # fails then, until the sequencing process has finished or + # aborted. + # + # Btw, --caller="my_tiny_script.sh|-a||" will be + # interpreted, that users must invoke "my_tiny_script.sh -a" + # to abort, but can invoke "git sequencer --continue" and + # "git sequencer --skip" to continue or skip. + # And it is also possible to provide three different scripts + # by --caller="|script 1|tool 2|util 3". + # + # If your user script does not need any special + # abort/continue/skip behavior, then just do NOT pass + # the --caller option. + ############################################################### + shift + expr "x$1" : 'x.*|.*|.*|.*$' >/dev/null || + die 'Wrong --caller format.' + CALLERSTRING="$1" + ;; + -B) + BATCHMODE=t + # XXX: we still have abort on editor invokations + ;; + --no-advice) + ADVICE=f + ;; + --onto) + shift + ONTO="$1" + test $(git cat-file -t "$ONTO") = 'commit' || + die "$ONTO is no commit or branch." + ;; + -q) + VERBOSE=0 + ADVICE=f + # XXX: If there were no editors, + # we could just do exec >/dev/null + ;; + -v) + VERBOSE=2 + # or increment it if we have several verbosity steps + ;; + --) + shift + break + ;; + *) + die "$1 currently not implemented." + ;; + esac + shift +done +test $# -gt 1 && usage + +do_startup "$1" -- 1.5.6.3.391.ge45b ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH 2/3] Add git-sequencer documentation 2008-07-16 20:45 [PATCH 1/3] Add git-sequencer shell prototype Stephan Beyer @ 2008-07-16 20:45 ` Stephan Beyer 2008-07-16 20:45 ` [PATCH 3/3] Add git-sequencer test suite (t3350) Stephan Beyer 0 siblings, 1 reply; 8+ messages in thread From: Stephan Beyer @ 2008-07-16 20:45 UTC (permalink / raw) To: git; +Cc: Stephan Beyer, Daniel Barkalow, Christian Couder, Junio C Hamano Mentored-by: Christian Couder <chriscool@tuxfamily.org> Mentored-by: Daniel Barkalow <barkalow@iabervon.org> Signed-off-by: Stephan Beyer <s-beyer@gmx.net> --- Documentation/git-sequencer.txt | 673 +++++++++++++++++++++++++++++++++++++++ 1 files changed, 673 insertions(+), 0 deletions(-) create mode 100644 Documentation/git-sequencer.txt diff --git a/Documentation/git-sequencer.txt b/Documentation/git-sequencer.txt new file mode 100644 index 0000000..8a701c4 --- /dev/null +++ b/Documentation/git-sequencer.txt @@ -0,0 +1,673 @@ +git-sequencer(1) +================ + +NAME +---- +git-sequencer - Execute a sequence of git instructions + +SYNOPSIS +-------- +[verse] +'git sequencer' [--batch] [--onto=<base>] + [--verbose | --no-advice | --quiet] + [--] [<file>] +'git sequencer' --continue | --skip | --abort | --edit | --status + + +DESCRIPTION +----------- +Executes a sequence of git instructions to HEAD or `<base>`. +The sequence is given by `<file>` or standard input. +Also see 'TODO FILE FORMAT' below. + +Before doing anything, the TODO file is checked for correct syntax +and sanity. + +In case of a conflict or request in the TODO file, 'git-sequencer' will +pause. On conflict you can use 'git-diff' to locate the markers (`<<<<<<<`) +and make edits to resolve the conflict. + +For each file you edit, you need to tell git the changes by doing + + git add <file> + +After resolving the conflict manually and updating the index with the +desired resolution, you can continue the sequencing process with + + git sequencer --continue + +Alternatively, you can undo the 'git-sequencer' progress with + + git sequencer --abort + +or skip the current instruction with + + git sequencer --skip + +or correct the TODO file with + + git sequencer --edit + +During pauses or when finished with the sequencing task, the current +HEAD will always be the result of the last processed instruction. + + +OPTIONS +------- +<file>:: + Filename of the TODO file. If omitted, standard input is used. + See 'TODO FILE FORMAT' below. + +-B:: +--batch:: + Run in batch mode. If unexpected user intervention is needed + (e.g. a conflict or the need to run an editor), 'git-sequencer' fails. ++ +Note that the sanity check fails, if you use this option +and an instruction like `edit` or `pause` is in the TODO file. + +--onto=<base>:: + Checkout given commit or branch before sequencing. + If you provide a branch, sequencer will make the provided + changes on the branch, i.e. the branch will be changed. + +--continue:: + Restart the sequencing process after having resolved a merge conflict. + +--abort:: + Restore the original branch and abort the sequence operation. + +--skip:: + Restart the sequencing process by skipping the current instruction. + +--status:: + Show the current status of 'git-sequencer' and what + operations can be done to change that status. + +--edit:: + Invoke editor to edit the unprocessed part of the TODO file. ++ +The file is syntax- and sanity-checked afterwards, so that you can +safely run `git sequencer --skip` or `--continue` after editing. +If you nonetheless noticed that you made a mistake, you can +overwrite `.git/sequencer/todo` with `.git/sequencer/todo.old` and +rerun `git sequencer --edit`. ++ +If the check fails you are prompted if you want to correct your +changes, edit again, cancel editing or really want to save. + +--no-advice:: + Suppress advice on intentional and unintentional pauses. + +-q:: +--quiet:: + Suppress output. Implies `--no-advice`. + (Not yet implemented.) + +-v:: +--verbose:: + Be more verbose. + + +NOTES +----- + +When sequencing, it is possible, that you are changing the history of +a branch in a way that can cause problems for anyone who already has +a copy of the branch in their repository and tries to pull updates from +you. You should understand the implications of using 'git-sequencer' on +a repository that you share. + +'git-sequencer' will usually be called by another git porcelain, like +linkgit:git-am[1] or linkgit:git-rebase[1]. + + +TODO FILE FORMAT +---------------- + +The TODO file contains basically one instruction per line. + +Blank lines will be ignored. +All characters after a `#` character will be ignored until the end of a line. + +The following instructions can be used: + + +edit <commit>:: + Pick a commit and pause the sequencer process to let you + make changes. ++ +This is a short form for `pick <commit> and `pause` on separate lines. + + +mark <mark>:: + Set a symbolic mark for the last commit. + `<mark>` is an unsigned integer starting at 1 and + prefixed with a colon, e.g. `:1`. ++ +The marks can help if you want to refer to commits that you +created during the sequencer process, e.g. if you want to +merge such a commit. ++ +The set marks are removed after the sequencer has completed. + + +merge [options] <commit-ish1> <commit-ish2> ... <commit-ishN>:: + Merge commits into HEAD. ++ +You can refer to a commit by a mark. ++ +If you do not provide a commit message (using `-F`, `-m`, `-C`, `-M`, +or `--standard`), an editor will be invoked. ++ +See the following list and 'GENERAL OPTIONS' for values of `options`: + + --standard;; + Generate a commit message like 'Merge ... into HEAD'. + See also linkgit:git-fmt-merge-msg[1]. + + -s <strategy>;; + --strategy=<strategy>;; + Use the given merge strategy. + See also linkgit:git-merge[1]. + + +pick [options] <commit>:: + Pick (see linkgit:git-cherry-pick[1]) a commit. + Sequencer will pause on conflicts. ++ +See the following list and 'GENERAL OPTIONS' for values of `options`: + + -R;; + --reverse;; + Revert the changes introduced by pick <commit>. + + --mainline=<n>;; + Allow you to pick merge commits by specifying the + parent number (beginning from 1) to let sequencer + replay the changes relative to the specified parent. + + +This option does not work together with `-R`. + + +patch [options] <file>:: + If file `<file>` is a pure (diff) patch, then apply the patch. + If no `--message` option is given, an editor will + be invoked to enter a commit message. ++ +If `<file>` is a linkgit:git-format-patch[1]-formatted patch, +then the patch will be commited. ++ +See the following list and 'GENERAL OPTIONS' for values of `options`: + + -3;; + --3way;; + When the patch does not apply cleanly, fall back on + 3-way merge, if the patch records the identity of blobs + it is supposed to apply to, and we have those blobs + available locally. + + -k;; + Pass `-k` flag to 'git-mailinfo' (see linkgit:git-mailinfo[1]). + + -n;; + Pass `-n` flag to 'git-mailinfo' (see linkgit:git-mailinfo[1]). + + --exclude=<path-pattern>;; + Do not apply changes to files matching the given path pattern. + This can be useful when importing patchsets, where you want to + exclude certain files or directories. + + -R;; + --reverse;; + Apply the patch in reverse. + + --no-add;; + When applying a patch, ignore additions made by the + patch. This can be used to extract the common part between + two files by first running 'diff' on them and applying + the result with this option, which would apply the + deletion part but not addition part. + + --whitespace=<action>;; + Specify behavior on whitespace errors. + See linkgit:git-apply[1] for a detailed description. + + --context=<n>;; + Ensure at least <n> lines of surrounding context match before + and after each change. When fewer lines of surrounding + context exist they all must match. By default no context is + ever ignored. + + --inaccurate-eof;; + Under certain circumstances, some versions of 'diff' do not + correctly detect a missing new-line at the end of the file. + As a result, patches created by such 'diff' programs do not + record incomplete lines correctly. + This option adds support for applying such patches by + working around this bug. + + -p<n>;; + Remove <n> leading slashes from traditional diff paths. + The default is 1. + + --unidiff-zero;; + By default, 'git-apply' expects that the patch being + applied is a unified diff with at least one line of context. + This provides good safety measures, but breaks down when + applying a diff generated with --unified=0. To bypass these + checks use this option. + + +pause:: + Pause the sequencer process to let you manually make changes. + For example, you can re-edit the done commit, split a commit, + fix bugs or typos, or make further commits on top of HEAD before + continuing. ++ +After you have finished your changes and added them to the index, +invoke `git sequencer --continue`. +If you only want to edit the last commit message with an editor, +run `git commit --amend` (see linkgit:git-commit[1]) before saying +`--continue`. + + +ref <ref>:: + Set ref `<ref>` to the current HEAD, see also + linkgit:git-update-ref[1]. + + +reset <commit-ish>:: + Go back (see linkgit:git-reset[1] `--hard`) to commit `<commit-ish>`. + `<commit-ish>` can also be given by a mark, if prefixed with a colon. + + +squash [options] <commit>:: + Add the changes introduced by `<commit>` to the last commit. ++ +See 'GENERAL OPTIONS' for values of `options`. + +squash [options] --from <mark>:: + Squash all commits from the given mark into one commit. + There must not be any `merge` instructions between the + `mark` instruction and this `squash --from` instruction. ++ +See the following list and 'GENERAL OPTIONS' for values of `options`: + + --collect-signoffs;; + Collect the Signed-off-by: lines of each commit and + add them to the squashed commit message. + (Not yet implemented.) + + --include-merges;; + Sanity check does not fail if you have merges + between HEAD and <mark>. + + +run [--dir=<path>] [--] <cmd> <args>...:: + Run command `<cmd>` with arguments `<args>`. + Pause (conflict-like) if exit status is non-zero. ++ +If `<path>` is set, sequencer will change directory to `<path>` +before running the command and change back after exit. + + +GENERAL OPTIONS +--------------- + +Besides some special options, the instructions +`patch`, `merge`, `pick`, `squash` take the following general options: + +--author=<author>:: + Override the author name and e-mail address used in the commit. + Use `A U Thor <author@example.com>` format. + +-C <commit-ish>:: +--reuse-commit=<commit-ish>:: + Reuse message and authorship data from specified commit. + +-M <commit-ish> +--reuse-message=<commit-ish>:: + Reuse message from specified commit. + Note, that only the commit message is reused + and not the authorship information. + +-F <file>:: +--file=<file>:: + Take the commit message from the given file. + +-m <msg>:: +--message=<msg>:: + Use the given `<msg>` as the commit message. + +--signoff:: + Add `Signed-off-by:` line to the commit message (if not yet there), + using the committer identity of yourself. + +-e:: +--edit:: + Regardless what commit message options are given, + invoke the editor to allow editing of the commit message. + + +RETURN VALUES +------------- + +'git-sequencer' returns: + +* `0`, if 'git-sequencer' successfully completed all the instructions + in the TODO file or successfully aborted after + `git sequencer --abort`, +* `2`, on user-requested pausing, e.g. + when using the `edit` instruction. +* `3`, on pauses that are not requested, e.g. + when there are conflicts to resolve + or errors in the TODO file. +* any other value on error, e.g. + running 'git-sequencer' on a bare repository. + + +EXAMPLES +-------- + +Here are some examples that shall ease the start with the TODO +file format. +Make sure you have understood the `pick` and perhaps the `patch` command. +Those will not be explained further. + +Manually editing and adding commits +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sequencer allows manual intervention in between. +This can be useful to + +* check if everything has gone right so far, + +* split commits, + +* edit changes and/or commit messages, or + +* add further manual commits on top of the current one. + +If you want to do one of this, either change `pick` to `edit`, or +add a `pause` line after the specific instruction. + +Note that if you only want to edit the commit message in an +editor, just use the `--edit` option of your `pick` or `patch` +instruction. + +`HEAD` refers to the last commit being done by sequencer. +So if you want to split a commit, repeat something like + +------------ +$ git reset HEAD^ # Reset index to HEAD^, but keep working tree + # HEAD is the last commit being done by sequencer +$ git add -p # Add changes interactively to the index, and/or +$ git add file1 # Add changes from file1 to the index +$ git commit # Commit staged changes +------------ + +until you have no changes to commit, and then run + +------------ +$ git sequencer --continue # Continue sequencer process +------------ + +Be aware that if there are still staged changes, +'git-sequencer' will add those changes to the last commit being done. + + +Squashing commits +~~~~~~~~~~~~~~~~~ + +Squashing commits means putting the changes of many commits into one. +If you have two commits `abcdef1` and `fa1afe1` and you want to squash them, +feed 'git-sequencer' with a TODO file like: + +------------ +pick abcdef1 +squash fa1afe1 +------------ + +Squash will concatenate the commit messages of `abcdef1` and `fa1afe1` and +invoke an editor so that you can edit them. +Perhaps you just want to reuse the commit message of `abcdef1` and +add a signoff. Then use: + +------------ +pick abcdef1 +squash -C abcdef1 --signoff fa1afe1 +------------ + +You can also squash more than two commits. +Basically you can do: + +------------ +pick A +squash B +squash C +squash D +squash --message "Make indentation consistent" --signoff E +------------ + +If somebody sent you a patch that you have not yet applied and you want +to apply it and squash it, or if you have a `pick <commit>` list generated +with something like + +------------ +$ git rev-list --no-merges --reverse A^..E | sed -e 's/^/pick /'` +------------ + +you can use the `mark` and `squash --from` instructions to +squash all commits between them into one: + +------------ +mark :0 +pick A +pick B +pick C +pick D +pick E +squash --message "Make indentation consistent" --signoff --from :0 +------------ + + +Branching and Merging +~~~~~~~~~~~~~~~~~~~~~ + +Merging branches can easily be done using the `merge` instruction. +For an example, it is more interesting to branch, pick some commits +and merge. Imagine you want to 'copy' this onto the current branch: + +------------ + ...--A1--A2--A3--A4--A5---MA-A6 refs/heads/old + | \ / + | C1--MC--C2 refs/heads/topic + \ / + B1--B2--B3 +------------ + +You want the copy to look exactly like this, except that you +are not on branch `old`, and you want to call the copy of `topic` +simply `topic2`. +Here is a way to achieve this: + +------------ +pick A1 +mark :0 # remember this to pick further commits on trunk A + +pick B1 # pick commits for trunk B +pick B2 +pick B3 +mark :1 # remember this for merge +# Why not just merge B3 later? +# Then you would merge the original B3 and not the copy. +# But in this example you want to merge the copy of B3. + +reset :0 # go back to the copy of A1 +pick A2 # go on picking the commits of trunk A +pick A3 +mark :2 # remember this to pick further commits on trunk A + +pick C1 # pick commits for trunk C +merge -C MC :1 # merge trunk B +pick C2 +ref refs/heads/topic2 # create branch for the C trunk + +reset :2 # go back to last commit of trunk A (copy of A3) +pick A4 # go on picking the commits of trunk A +pick A5 +merge --standard topic2 # merge trunk C +pick A6 +------------ + + +Proper handling of conflicts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First of all, you are encouraged to use linkgit:git-rerere[1]: + +------------ +$ git config rerere.enabled true # enable rerere +------------ + +Sequencer invokes 'git-rerere' automatically on conflict. + +If you experience conflicts, try + +------------ +$ git diff # Show conflicting code +$ git status # Show conflicting files +------------ + +Then fix these conflicts using your editor and run + +------------ +$ git add file1 file2 file3 # Add modified files to the index +$ git status # Make sure working tree is clean +$ git sequencer --continue # Continue sequencer process +------------ + +Now assume a conflict happens because you have unproperly edited +the TODO file. + +Imagine your initial TODO file was: + +------------ +pick A +pick C +pick D +------------ + +But you wanted to pick B before C, and now you have this conflict on +picking C. You may first have a look at: + +------------ +$ git sequencer --status +------------ + +This will show you, what has been done, in what step the conflict +happened and what is still to do, like this: + +------------ +Already done (or tried): + pick A + pick C + +Interrupted by conflict at + pick C + +Still to do: + pick D + +To abort & restore, invoke: + git sequencer --abort +To continue, invoke: + git sequencer --continue +To skip the current instruction, invoke: + git sequencer --skip +------------ + +A good way to solve that situation is running + +------------ +$ git sequencer --edit +------------ + +and change the file to: + +------------ +pick B +pick C +pick D +------------ + +Save the file, and invoke: + +------------ +$ git sequencer --skip +------------ + +Then the conflict-ridden `pick C` will be skipped and B is picked, +before C will again be picked. + + +Running tests +~~~~~~~~~~~~~ + +Imagine you have test programs within a `tests/` directory in your working +tree. But before running your test programs, you have to invoke `make` in +the root directory of the working tree to compile your project. + +If the commit policy of your project says that after every commit the +software must be able to compile and the test suite must pass, you +are required to check this after every pick. + +This example shows how 'git-sequencer' can assist you: + +------------ +pick A # Fix foo +run make +run --dir=tests ./test-foo +pick B # Extend bar +run make +run --dir tests -- ./test-bar --expensive-tests +pick C +run make +run --dir tests make tests +------------ + +Sequencer will be paused, when a run fails (i.e. on non-zero exit status). +Then it is your turn to fix the problem and make the tests pass. + +Note, that on `git sequencer --continue`, 'git-sequencer' will not +repeat the failed `run` instruction. + + +SEE ALSO +-------- + +linkgit:git-add[1], +linkgit:git-am[1], +linkgit:git-cherry-pick[1], +linkgit:git-commit[1], +linkgit:git-fmt-merge-msg[1], +linkgit:git-format-patch[1], +linkgit:git-rebase[1], +linkgit:git-rerere[1], +linkgit:git-reset[1], +linkgit:git-update-ref[1] + + +Authors +------- +Written by Stephan Beyer <s-beyer@gmx.net>. + + +Documentation +------------- +Documentation by Stephan Beyer and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the linkgit:git[1] suite -- 1.5.6.3.391.ge45b ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH 3/3] Add git-sequencer test suite (t3350) 2008-07-16 20:45 ` [PATCH 2/3] Add git-sequencer documentation Stephan Beyer @ 2008-07-16 20:45 ` Stephan Beyer 2008-07-16 20:45 ` Sequencer migration patches Stephan Beyer 0 siblings, 1 reply; 8+ messages in thread From: Stephan Beyer @ 2008-07-16 20:45 UTC (permalink / raw) To: git; +Cc: Stephan Beyer, Daniel Barkalow, Christian Couder, Junio C Hamano Mentored-by: Christian Couder <chriscool@tuxfamily.org> Mentored-by: Daniel Barkalow <barkalow@iabervon.org> Signed-off-by: Stephan Beyer <s-beyer@gmx.net> --- t/t3350-sequencer.sh | 838 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 838 insertions(+), 0 deletions(-) create mode 100755 t/t3350-sequencer.sh diff --git a/t/t3350-sequencer.sh b/t/t3350-sequencer.sh new file mode 100755 index 0000000..3cc7da8 --- /dev/null +++ b/t/t3350-sequencer.sh @@ -0,0 +1,838 @@ +#!/bin/sh +# +# Copyright (c) 2008 Stephan Beyer +# +# `setup' is based on t3404* by Johannes Schindelin. + +test_description='git sequencer + +These are basic usage tests for git sequencer. +' +. ./test-lib.sh + +# set up two branches like this: +# +# A - B - C - D - E +# \ +# F - G - H +# \ +# I +# +# where B, D and G touch increment value in file1. +# The others generate empty file[23456]. + +SEQDIR=".git/sequencer" +SEQMARK="refs/sequencer-marks" +MARKDIR=".git/$SEQMARK" + +test_expect_success 'setup' ' + : >file1 && + git add file1 && + test_tick && + git commit -m "generate empty file1" && + git tag A && + echo 1 >file1 && + test_tick && + git commit -m "write 1 into file1" file1 && + git tag B && + : >file2 && + git add file2 && + test_tick && + git commit -m "generate empty file2" && + git tag C && + echo 2 >file1 && + test_tick && + git commit -m "write 2 into file1" file1 && + git tag D && + : >file3 && + git add file3 && + test_tick && + git commit -m "generate empty file3" && + git tag E && + git checkout -b branch1 A && + : >file4 && + git add file4 && + test_tick && + git commit -m "generate empty file4" && + git tag F && + echo 3 >file1 && + test_tick && + git commit -m "write 3 into file1" file1 && + git tag G && + : >file5 && + git add file5 && + test_tick && + git commit -m "generate empty file5" && + git tag H && + git checkout -b branch2 F && + : >file6 && + git add file6 && + test_tick && + git commit -m "generate empty file6" && + git tag I && + git diff -p --raw C..D >patchD.raw && + git diff -p --raw A..F >patchF.raw && + git format-patch --stdout A..B >patchB && + git format-patch --stdout B..C >patchC && + git format-patch --stdout C..D >patchD && + git format-patch --stdout A..F >patchF && + git format-patch --stdout F..G >patchG +' + +orig_author="$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>" + +# Functions to verify exit status of sequencer. +# Do not just use "test_must_fail git sequencer ..."! +expect_fail () { + "$@" + test $? -eq 1 +} +expect_continue () { + "$@" + test $? -eq 2 +} +expect_conflict () { + "$@" + test $? -eq 3 +} + + +# Other test helpers: + +# Test if commit $1 has author $2 +expect_author () { + test "$2" = "$(git cat-file commit "$1" | + sed -n -e "s/^author \(.*\)> .*$/\1>/p")" +} + +# Test if commit $1 has commit message in file $2 +# Side effect: overwrites actual +expect_msg () { + git cat-file commit "$1" | sed -e "1,/^$/d" >actual && + test_cmp "$2" actual +} + +# Test that no marks are set. +no_marks_set () { + if test -e "$MARKDIR" + then + rmdir "$MARKDIR" + fi +} + +test_expect_success 'fail on empty TODO from stdin' ' + expect_fail git sequencer <file6 && + ! test -d "$SEQDIR" +' + +# Generate fake editor +# +# Simple and practical concept: +# We use only a small string identifier for "editor sessions". +# Each sessions knows what to do and perhaps defines +# which session to choose next. +echo "#!$SHELL_PATH" >fake-editor.sh +cat >>fake-editor.sh <<\EOF +test -f fake-editor-session || exit 1 +#test -t 1 || exit 1 +# This test could be useful, but as the test-lib is not always +# verbose, this will fail. +next=ok +read this <fake-editor-session +case "$this" in +commitmsg) + echo 'echo 2 >file1' + ;; +squashCE) + echo 'generate file2 and file3' + ;; +squashCI) + echo 'generate file2 and file6' + next=squashDCE + ;; +squashDCE) + echo 'generate file2 and file3 and write 2 into file1' + next=merge1 + ;; +merge1) + echo 'A typed merge message.' + ;; +merge2) + test "$(sed -n -e 1p "$1")" = 'test merge' && + echo 'cleanup merge' || + echo error + sed -e 1d "$1" + ;; +editXXXXXXXXX) + printf 'last edited' + ;; +edit*) + printf 'edited: ' + cat "$1" + next="${this}X" + ;; +nochange) + cat "$1" + ;; +ok|fail) + echo '-- THIS IS UNEXPECTED --' + next=fail + ;; +*) + echo 'I do not know.' + ;; +esac >"$1".tmp +mv "$1".tmp "$1" +echo $next >fake-editor-session +exit 0 +EOF +chmod a+x fake-editor.sh +test_set_editor "$(pwd)/fake-editor.sh" + +next_session () { + echo "$1" >fake-editor-session +} + +# check if fake-editor-session is ok. +# If "$1" is set to anything, it will set the +# next session to "ok", which is nice for +# test_expect_failure. +session_ok () { + test "ok" = $(cat fake-editor-session) + ret=$? + test -n "$1" && next_session ok + return $ret +} + + +cat >todotest1 <<EOF +pick C +squash E +ref refs/tags/CE +EOF + +test_expect_success '"pick", "squash", "ref" from stdin' ' + next_session squashCE && + git sequencer <todotest1 && + ! test -d "$SEQDIR" && + session_ok && + test -f file2 && + test -f file3 && + test $(git rev-parse CE) = $(git rev-parse HEAD) && + test $(git rev-parse I) = $(git rev-parse HEAD^) +' + +cat >todotest2 <<EOF +# This is a test + +reset I # go back to I + +EOF + +test_expect_success '"reset" from file with comments and blank lines' ' + git sequencer todotest2 && + session_ok && + test $(git rev-parse I) = $(git rev-parse HEAD) +' + +cat >todotest1 <<EOF +pick C +EOF + +test_expect_success '--onto <branch> keeps branch' ' + git checkout -b test-branch A && + git checkout master && + git sequencer --onto test-branch <todotest1 && + session_ok && + test "$(git symbolic-ref -q HEAD)" = "refs/heads/test-branch" && + test "$(git rev-parse test-branch^)" = "$(git rev-parse A)" +' + +test_expect_success '--onto commit (detached HEAD) works' ' + git sequencer --onto A <todotest1 && + session_ok && + test_must_fail git symbolic-ref -q HEAD && + test "$(git rev-parse HEAD)" = "$(git rev-parse test-branch)" +' + +echo 'pick -R C' >>todotest1 + +test_expect_success 'pick -R works' ' + git checkout A && + git sequencer todotest1 && + session_ok && + ! test -f file2 +' + +mkdir testdir +cat >testdir/script <<EOF +#!/bin/sh +test -s ../file1 +EOF +chmod 755 testdir/script +cat >todotest1 <<EOF +run -- test -s file1 # this will fail +pick B +run --dir testdir -- test -s ../file1 +pick D +run --dir=testdir ./script +EOF + +test_expect_success '"run" insn works' ' + git checkout A && + expect_conflict git sequencer todotest1 && + : >newfile && + git add newfile && + next_session nochange && + git sequencer --continue && + session_ok && + test -f newfile +' + +echo thisdoesnotexist >>todotest1 + +test_expect_success 'junk is conflict' ' + git checkout A && + expect_conflict git sequencer todotest1 && + test -d "$SEQDIR" && + git sequencer --abort && + ! test -d "$SEQDIR" && + session_ok && + test $(git rev-parse A) = $(git rev-parse HEAD) +' + +GIT_AUTHOR_NAME="Another 'ant' Thor" +GIT_AUTHOR_EMAIL="a.thor@example.com" +GIT_COMMITTER_NAME="Co M Miter" +GIT_COMMITTER_EMAIL="c.miter@example.com" +export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL +yet_another="Max Min <mm@example.com>" + +cat >todotest1 <<EOF +patch patchB # write 1 into file1 +patch -k patchC # generate file2 +patch patchD.raw # write 2 into file1 +EOF + +echo 'write 1 into file1' >expected1 +echo '[PATCH] generate empty file2' >expected2 +echo 'echo 2 >file1' >expected3 + +test_expect_success '"patch" insn works' ' + git checkout A && + next_session commitmsg && + git sequencer todotest1 && + ! test -d "$SEQDIR" && + session_ok && + test "$(git rev-parse HEAD~3)" = "$(git rev-parse A)" && + test "$(git show HEAD~2:file1)" = "1" && + test -z "$(git show HEAD^:file2)" && + test "$(git show HEAD:file1)" = "2" && + expect_author HEAD~2 "$orig_author" && + expect_author HEAD~1 "$orig_author" && + expect_author HEAD~0 "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>" && + expect_msg HEAD~2 expected1 && + expect_msg HEAD~1 expected2 && + expect_msg HEAD~0 expected3 +' + +cat >todotest1 <<EOF +pick B # write 1 into file1 +pause +pick C # generate file2 +EOF + +echo 'generate empty file2' >expected1 +echo 'write 1 into file1' >expected2 + +test_expect_success "pick ; pause insns and --continue works" ' + git checkout A && + expect_continue git sequencer todotest1 && + session_ok && + echo 5 >file1 && + git add file1 && + next_session nochange && + git sequencer --continue && + test "$(git show HEAD:file1)" = 5 && + test -z "$(git show HEAD:file2)" && + expect_msg HEAD expected1 && + expect_msg HEAD^ expected2 && + session_ok +' + +cat >todotest1 <<EOF +edit B # write 1 into file1 +pick C # generate file2 +EOF + +test_expect_success "edit insn and --continue works" ' + git checkout A && + expect_continue git sequencer todotest1 && + session_ok && + echo 5 >file1 && + git add file1 && + next_session nochange && + git sequencer --continue && + test "$(git show HEAD:file1)" = 5 && + test -z "$(git show HEAD:file2)" && + expect_msg HEAD expected1 && + expect_msg HEAD^ expected2 && + session_ok +' + +cat >todotest1 <<EOF +patch patchB # write 1 into file1 +pick H # generate file5 +mark :1 +patch patchC # generate file2 +squash I # generate file6 +patch patchD # write 2 into file1 +ref refs/tags/CID +mark :2 +reset :1 # reset to new H +patch patchD # write 2 into file1 +squash CE # generate file2 and file3 +ref refs/tags/DCE +merge :2 # merge :2 into HEAD +patch patchF # generate file4 +EOF + +test_expect_success 'all insns work without options' ' + git checkout A && + next_session squashCI && + no_marks_set && + git sequencer todotest1 && + no_marks_set && + test "$(git show HEAD:file1)" = "2" && + test -z "$(git show HEAD:file2)" && + test -z "$(git show HEAD:file3)" && + test -z "$(git show HEAD:file4)" && + test -z "$(git show HEAD:file5)" && + test -z "$(git show HEAD:file6)" && + echo "$(git rev-parse DCE)" >expected && + echo "$(git rev-parse CID)" >>expected && + git cat-file commit HEAD^ | sed -n -e "s/^parent //p" >actual && + test_cmp expected actual && + session_ok +' + +cat >todotest1 <<EOF +merge --standard DCE +EOF + +echo "Merge DCE into HEAD" >expected1 + +test_expect_success 'merge --standard works' ' + git checkout CID && + git sequencer todotest1 && + expect_msg HEAD expected1 && + session_ok +' + +cat >todotest1 <<EOF +merge --standard --message="foo" DCE +EOF + + +test_expect_success 'merge --standard --message="foo" is conflict' ' + git checkout CID && + expect_conflict git sequencer todotest1 && + git sequencer --abort && + session_ok +' + +for command in 'pick ' 'patch patch' 'squash ' 'merge --standard ' +do + cat >todotest1 <<EOF +patch patchB # 1 into file1 +${command}G # 3 into file1 +patch -3 patchF # empty file4 +EOF + + test_expect_success "conflict test: ${command%% *} and --abort" ' + git checkout A && + expect_conflict git sequencer todotest1 && + session_ok && + test -d "$SEQDIR" && + git sequencer --abort && + session_ok && + test $(git rev-parse HEAD) = $(git rev-parse A) + ' + + test_expect_success "conflict test: ${command%% *} and --continue" ' + git checkout A && + expect_conflict git sequencer todotest1 && + session_ok && + test -d "$SEQDIR" && + ## XXX: It would be perfect if we could remove the if + { if test "${command%% *}" != "patch" + then grep "^<<<<<<<" file1 ; fi } && + echo 3 >file1 && + git add file1 && + next_session nochange && + git sequencer --continue && + session_ok && + ! test -d "$SEQDIR" && + test "$(git show HEAD:file1)" = "3" && + test -f file4 + ' + + test_expect_success "conflict test: ${command%% *} and --skip" ' + git checkout A && + expect_conflict git sequencer todotest1 && + session_ok && + test -d "$SEQDIR" && + git sequencer --skip && + session_ok && + ! test -d "$SEQDIR" && + test "$(git show HEAD:file1)" = "1" && + test -f file4 + ' +done + +echo 'file5-gen' >commitmsg + +cat >todotest1 <<EOF +patch --signoff patchB +pause +pick --author="$yet_another" --file="commitmsg" --signoff H +mark :1 +patch --message="file2-gen" patchC +squash --signoff --author="$yet_another" I +pause +patch --message="echo 2 >file1" patchD +mark :2 +reset :1 +patch --author="$yet_another" patchD +squash --signoff --message="generate file[23]" CE +merge --signoff --message="test merge" --author="$yet_another" :2 +pause +ref refs/tags/a_merge +patch --message="Generate file4 and write 23 into it" patchF.raw +pause +pick I +EOF + +cat >expected1 <<EOF +write 1 into file + +Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> +EOF + +test_expect_success 'insns work with options and another author 1' ' + git checkout A && + no_marks_set && + + # patch --signoff patchB # write 1 into file1 + # pause + expect_continue git sequencer todotest1 && + test "Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" = \ + "$(git cat-file commit HEAD | grep "^Signed-off-by")" && + expect_author HEAD "$orig_author" && + test -d "$SEQDIR" && + session_ok +' + +cat >expected1 <<EOF +file5-gen + +Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> +EOF + +cat >expected2 <<EOF +file2-gen + +generate empty file6 + +Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> +EOF + +test_expect_success 'insns work with options and another author 2' ' + : >file7 && + git add file7 && + next_session nochange && + git commit --amend && + session_ok && + + next_session nochange && + expect_continue git sequencer --continue && + session_ok && + + # amended commit + test "Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" = \ + "$(git cat-file commit HEAD^^ | grep "^Signed-off-by")" && + expect_author HEAD^^ "$orig_author" && + test -z "$(git show HEAD:file7)" && + + # pick --author="$yet_another" --file="commitmsg" --signoff H + expect_author HEAD^ "$yet_another" && + expect_msg HEAD^ expected1 && + test -z "$(git show HEAD:file5)" && + + # mark :1 + test "$(git rev-parse "$SEQMARK/1")" = "$(git rev-parse HEAD^)" && + + # patch --message="file2-gen" patchC + # squash --signoff --author="$yet_another" I # generate file6 + # pause + test -z "$(git show HEAD:file2)" && + test -z "$(git show HEAD:file6)" && + git ls-files | grep "^file2" && + git ls-files | grep "^file6" && + expect_msg HEAD expected2 && + expect_author HEAD "$yet_another" +' + +echo 'echo 2 >file1' >expected1 + +cat >expected2 <<EOF +generate file[23] + +Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> +EOF + +cat >expected3 <<EOF +test merge + +Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> +EOF + +cat >expected4 <<EOF +file1 +file2 +file3 +file5 +file6 +file7 +EOF + +test_expect_success 'insns work with options and another author 3' ' + # do not change anything + expect_continue git sequencer --continue && + session_ok && + + # patch --message="echo 2 >file1" patchD + # mark :2 + commit="$(git rev-parse --verify "$SEQMARK/2")" && + expect_author "$commit" "$orig_author" && + expect_msg "$commit" expected1 && + + # reset :1 + # patch --author="$yet_another" patchD # write 2 into file1 + # squash --signoff --message="generate file[23]" CE + expect_author HEAD^ "$yet_another" && + expect_msg HEAD^ expected2 && + + # merge --signoff --message="test merge" --author="$yet_another" :2 + # pause + expect_author HEAD "$yet_another" && + expect_msg HEAD expected3 && + git ls-files >actual && + test_cmp expected4 actual +' + +cat >expected_merge <<EOF +cleanup merge + +Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> +EOF + +echo 'Generate file4 and write 23 into it' >expected2 + +test_expect_success 'insns work with options and another author 4' ' + git rm file5 file6 file7 && + next_session merge2 && + expect_continue git sequencer --continue && + session_ok && + + # ref refs/tags/a_merge + expect_author a_merge "$yet_another" && + expect_msg a_merge expected_merge && + + # patch --message="Generate file4 and write 23 into it" patchF.raw + # pause + git ls-files | grep "^file4" && + echo 23 >file4 && + git add file4 && + next_session nochange && + git sequencer --continue && + session_ok && + no_marks_set && + test "$(git show HEAD:file1)" = "2" && + test "$(git show HEAD:file4)" = "23" && + expect_msg HEAD^ expected2 && + expect_author HEAD^ "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>" && + + # pick I + test -z "$(git show HEAD:file6)" && + git ls-files | grep "^file6" && + session_ok +' + +# almost the same to test --quiet +cat >todotest1 <<EOF +patch patchB +pick H +mark :1 +patch patchC +squash --message="a squash" I +patch patchD +mark :2 +reset :1 +patch patchD +squash --message="another squash" CE +merge --message="test merge" :2 +pause +patch patchF +EOF + +test_expect_failure '--quiet works' ' + git checkout A && + expect_continue git sequencer --quiet todotest1 >actual && + session_ok && + ! test -s actual +' + +test_expect_failure '--quiet works on continue' ' + git sequencer --continue >>actual && + session_ok && + ! test -s actual +' + +echo 'merge --strategy=ours --reuse-commit=a_merge branch1 branch2 CE CID' >todotest1 + +test_expect_success 'merge multiple branches and --reuse-commit works' ' + git checkout -b merge-multiple master && + git sequencer todotest1 && + session_ok && + expect_msg HEAD expected_merge && + git rev-parse HEAD^ >expected && + git rev-parse branch1 >>expected && + git rev-parse branch2 >>expected && + git rev-parse CE >>expected && + git rev-parse CID >>expected && + git cat-file commit HEAD | sed -n -e "s/^parent //p" >actual && + test_cmp expected actual && + ! test -f file6 +' + +echo 'pick --mainline=5 merge-multiple' >todotest1 + +test_expect_success 'pick --mainline works' ' + git checkout -b mainline CID && + git sequencer todotest1 && + session_ok && + expect_msg HEAD expected_merge && + ! test -f file6 && + test -f file3 && + test -f file2 && + test "$(git show HEAD:file1)" = 2 +' + +cat >todotest1 <<EOF +pick C # file2 +mark :1 +patch patchB # write 1 into file1 +patch patchD # write 2 into file1 +pick I # file6 +squash --message="2 in file1 and file6 exists" --signoff --from :1 +EOF + +cat >expected1 <<EOF +2 in file1 and file6 exists + +Signed-off-by: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> +EOF + +test_expect_success 'squash --from works' ' + git checkout A && + git sequencer <todotest1 && + session_ok && + test "$(git rev-parse A)" = "$(git rev-parse HEAD~2)" && + test "$(git show HEAD:file1)" = "2" && + test -z "$(git show HEAD:file6)" && + expect_msg HEAD expected1 +' + +cat >todotest1 <<EOF +patch patchB # write 1 into file1 +pick H # generate file5 +mark :1 +patch patchC # generate file2 +squash --message="file5" I # generate file6 +patch patchD # write 2 into file1 +mark :2 +reset :1 # reset to new H +patch patchD # write 2 into file1 +squash --message="CE" CE # generate file2 and file3 +merge --standard :2 # merge :2 into HEAD +patch patchF # generate file4 +EOF +cp todotest1 todotest2 +cat todotest1 | sed -e 's/^\(patch\|pick\|squash\|merge\) /&--edit /' >todotest3 +echo 'squash --message="doesnt work either" --from :1' >>todotest1 +echo 'squash --include-merges --message="stupid" --from :1' >>todotest2 + +test_expect_success 'squash --from conflicts with merge in between' ' + git checkout A && + expect_conflict git sequencer todotest1 && + git sequencer --abort && + session_ok && + ! test -d "$SEQDIR" +' + +test_expect_success 'squash --include-merges --from succeeds with merge in between' ' + git checkout A && + git sequencer todotest2 && + session_ok && + test "$(git rev-parse HEAD~3)" = "$(git rev-parse A)" +' + +test_expect_success 'patch|pick|squash|merge --edit works' ' + git checkout A && + next_session editX && + git sequencer todotest3 && + session_ok +' + +cat >todotest1 <<EOF +patch patchB +pause +EOF + +test_expect_success 'batch mode fails on pause insn' ' + git checkout A && + expect_fail git sequencer --batch todotest1 && + session_ok && + ! test -d "$SEQDIR" +' + +cat >todotest1 <<EOF +patch patchB +pick G +EOF + +test_expect_success 'batch mode fails on conflict' ' + git checkout A && + expect_fail git sequencer --batch <todotest1 && + session_ok && + ! test -d "$SEQDIR" && + test -z "$(git show HEAD:file1)" +' + +cat >todotest1 <<EOF +patch patchB +pause +EOF + +test_expect_success '--caller works' ' + git checkout A && + expect_continue git sequencer \ + --caller="this works|abrt||skip" todotest1 && + expect_fail git sequencer --abort && + expect_fail git sequencer --skip && + git sequencer --continue && + session_ok +' + +test_done -- 1.5.6.3.391.ge45b ^ permalink raw reply related [flat|nested] 8+ messages in thread
* Sequencer migration patches 2008-07-16 20:45 ` [PATCH 3/3] Add git-sequencer test suite (t3350) Stephan Beyer @ 2008-07-16 20:45 ` Stephan Beyer 2008-07-16 20:45 ` [PATCH 1/2] Migrate git-am to use git-sequencer Stephan Beyer ` (2 more replies) 0 siblings, 3 replies; 8+ messages in thread From: Stephan Beyer @ 2008-07-16 20:45 UTC (permalink / raw) To: git Cc: Stephan Beyer, Daniel Barkalow, Christian Couder, Junio C Hamano, Johannes Schindelin Hi, the following patches migrate git-am and git-rebase--interactive to git-sequencer. But there are some issues I want to mention. I've compared performance of original rebase and am with the sequencer-based ones and the sequencer-based ones perform fairly bad. For example, applying 45 patches with git-am took 3 seconds using the original and 8 seconds using the sequencer-based one. Rebasing 100 commits takes 10.1 seconds instead of only 4.8 seconds on my test machine. I expect that the builtin-sequencer performs muuuch better. But as long as there is no builtin-sequencer these patches should perhaps not be applied. Well, I could offer to provide patches to put sequencer-based git-am and git-rebase-i scripts to contrib/examples/ (or contrib/sequencer-examples/ or something else). ;-) Regards, Stephan Stephan Beyer (2): Migrate git-am to use git-sequencer Introduce git am --abort Documentation/git-am.txt | 5 +- Documentation/git-rerere.txt | 2 +- contrib/completion/git-completion.bash | 2 +- git-am.sh | 617 +++++++++++--------------------- git-rebase.sh | 7 +- t/t4150-am.sh | 27 +- 6 files changed, 244 insertions(+), 416 deletions(-) Stephan Beyer (1): Migrate rebase-i to sequencer git-rebase--interactive.sh | 438 ++++++++++------------------------------- t/t3404-rebase-interactive.sh | 9 +- 2 files changed, 113 insertions(+), 334 deletions(-) ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH 1/2] Migrate git-am to use git-sequencer 2008-07-16 20:45 ` Sequencer migration patches Stephan Beyer @ 2008-07-16 20:45 ` Stephan Beyer 2008-07-16 20:45 ` [PATCH 2/2] Introduce git am --abort Stephan Beyer 2008-07-16 20:45 ` [PATCH] Migrate rebase-i to sequencer Stephan Beyer 2008-07-17 13:05 ` Sequencer migration patches Stephan Beyer 2 siblings, 1 reply; 8+ messages in thread From: Stephan Beyer @ 2008-07-16 20:45 UTC (permalink / raw) To: git; +Cc: Stephan Beyer, Daniel Barkalow, Christian Couder, Junio C Hamano In principle a migration to git-sequencer is straightforward: Put all the mail from the mbox or Maildir into .git/rebase and let the "patch" instruction of sequencer do the rest of the work. The git am --interactive part is a little more tricky. To get this working, "pause" instructions are put after every "patch" instruction and then be_interactive() swoops in, that allows the user to input his choice. Also a slight behavior change, that can be seen in the diff of the test cases, should be mentioned: If git-am has nothing to do, the user does not have to remove .git/rebase or run git-am --skip manually. It automatically aborts instead, which seems to be an improvement. Mentored-by: Christian Couder <chriscool@tuxfamily.org> Mentored-by: Daniel Barkalow <barkalow@iabervon.org> Signed-off-by: Stephan Beyer <s-beyer@gmx.net> --- git-am.sh | 612 ++++++++++++++++++++------------------------------------- git-rebase.sh | 1 + t/t4150-am.sh | 6 +- 3 files changed, 214 insertions(+), 405 deletions(-) diff --git a/git-am.sh b/git-am.sh index cc8787b..e36f22c 100755 --- a/git-am.sh +++ b/git-am.sh @@ -33,102 +33,143 @@ cd_to_toplevel git var GIT_COMMITTER_IDENT >/dev/null || die "You need to set your committer info first" -stop_here () { - echo "$1" >"$dotest/next" - exit 1 +cleanup () { + git gc --auto + rm -fr "$dotest" } -stop_here_user_resolve () { - if [ -n "$resolvemsg" ]; then - printf '%s\n' "$resolvemsg" - stop_here $1 - fi - cmdline=$(basename $0) - if test '' != "$interactive" - then - cmdline="$cmdline -i" - fi - if test '' != "$threeway" - then - cmdline="$cmdline -3" - fi - echo "When you have resolved this problem run \"$cmdline --resolved\"." - echo "If you would prefer to skip this patch, instead run \"$cmdline --skip\"." - - stop_here $1 +die_abort () { + cleanup + die "$1" } -go_next () { - rm -f "$dotest/$msgnum" "$dotest/msg" "$dotest/msg-clean" \ - "$dotest/patch" "$dotest/info" - echo "$next" >"$dotest/next" - this=$next -} +be_interactive () { + msg="$GIT_DIR/sequencer/message" + patch="$GIT_DIR/sequencer/patch" + author_script="$GIT_DIR/sequencer/author-script" + # we rely on sequencer here + + test -t 0 || + die "cannot be interactive without stdin connected to a terminal." + action=$(cat "$dotest/interactive") + while test "$action" = again + do + echo + echo "Commit Body is:" + echo "--------------------------" + cat "$msg" + echo "--------------------------" + if test -z "$1" + then + printf "Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all " + else + echo 'Patch does not apply cleanly!' + printf "Apply+fix? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all " + fi -cannot_fallback () { - echo "$1" - echo "Cannot fall back to three-way merge." - exit 1 + read reply + case "$reply" in + [yY]*) + return 0 + ;; + [nN]*) + if test -z "$1" + then + git reset -q --hard HEAD^ + else + git reset -q --hard HEAD + fi + return 1 + ;; + [eE]*) + git_editor "$msg" + git commit --amend --file="$msg" --no-verify >/dev/null + ;; + [vV]*) + LESS=-S ${PAGER:-less} "$patch" + ;; + [aA]*) + echo 'accept' >"$dotest/interactive" + return 0 + ;; + *) + : + ;; + esac + done + test "$action" = accept && + sed -n -e '1s/^/Applying &/p' <"$msg" + return 0 } -fall_back_3way () { - O_OBJECT=`cd "$GIT_OBJECT_DIRECTORY" && pwd` - - rm -fr "$dotest"/patch-merge-* - mkdir "$dotest/patch-merge-tmp-dir" - - # First see if the patch records the index info that we can use. - git apply --build-fake-ancestor "$dotest/patch-merge-tmp-index" \ - "$dotest/patch" && - GIT_INDEX_FILE="$dotest/patch-merge-tmp-index" \ - git write-tree >"$dotest/patch-merge-base+" || - cannot_fallback "Repository lacks necessary blobs to fall back on 3-way merge." - - echo Using index info to reconstruct a base tree... - if GIT_INDEX_FILE="$dotest/patch-merge-tmp-index" \ - git apply $binary --cached <"$dotest/patch" - then - mv "$dotest/patch-merge-base+" "$dotest/patch-merge-base" - mv "$dotest/patch-merge-tmp-index" "$dotest/patch-merge-index" - else - cannot_fallback "Did you hand edit your patch? -It does not apply to blobs recorded in its index." - fi - - test -f "$dotest/patch-merge-index" && - his_tree=$(GIT_INDEX_FILE="$dotest/patch-merge-index" git write-tree) && - orig_tree=$(cat "$dotest/patch-merge-base") && - rm -fr "$dotest"/patch-merge-* || exit 1 - - echo Falling back to patching base and 3-way merge... +print_continue_info () { + echo 'When you have resolved this problem run "git am --resolved".' + echo 'If you would prefer to skip this patch, instead run "git am --skip".' +} - # This is not so wrong. Depending on which base we picked, - # orig_tree may be wildly different from ours, but his_tree - # has the same set of wildly different changes in parts the - # patch did not touch, so recursive ends up canceling them, - # saying that we reverted all those changes. +run_sequencer () { + git sequencer $noadvice --caller='git am||--resolved|--skip' "$@" + case "$?" in + 0) + cleanup + exit 0 + ;; + 2|3) + ret=$? + print_continue_info + exit $(($ret-2)) + ;; + *) + die_abort 'git-sequencer died unexpected. Aborting.' + ;; + esac +} - eval GITHEAD_$his_tree='"$FIRSTLINE"' - export GITHEAD_$his_tree - git-merge-recursive $orig_tree -- HEAD $his_tree || { - git rerere - echo Failed to merge in the changes. - exit 1 - } - unset GITHEAD_$his_tree +run_sequencer_i () { + command="$1" + while true + do + output=$(git sequencer $noadvice \ + --caller='git am -i||--resolved|--skip' \ + $command 2>&1 >/dev/null) + noadvice= + case "$?" in + 0) + cleanup + exit 0 + ;; + 2) + be_interactive + ;; + 3) + be_interactive conflict + if test $? -eq 0 + then + printf '%s\n' "$output" 1>&2 + print_continue_info + exit 1 + fi + ;; + *) + die_abort "$output" + ;; + esac + command=--continue + done } prec=4 dotest="$GIT_DIR/rebase" +todofile="$dotest/todo" sign= utf8=t keep= skip= interactive= resolved= binary= rebasing= -resolvemsg= resume= -git_apply_opt= +resolvemsg= +opts= while test $# != 0 do case "$1" in -i|--interactive) - interactive=t ;; + interactive=_i ;; -b|--binary) binary=t ;; -3|--3way) @@ -153,9 +194,9 @@ do --resolvemsg) shift; resolvemsg=$1 ;; --whitespace) - git_apply_opt="$git_apply_opt $1=$2"; shift ;; + opts="$opts $1=$2"; shift ;; -C|-p) - git_apply_opt="$git_apply_opt $1$2"; shift ;; + opts="$opts $1$2"; shift ;; --) shift; break ;; *) @@ -164,348 +205,117 @@ do shift done -# If the dotest directory exists, but we have finished applying all the -# patches in them, clear it out. -if test -d "$dotest" && - last=$(cat "$dotest/last") && - next=$(cat "$dotest/next") && - test $# != 0 && - test "$next" -gt "$last" -then - rm -fr "$dotest" -fi - if test -d "$dotest" then - case "$#,$skip$resolved" in - 0,*t*) - # Explicit resume command and we do not have file, so - # we are happy. - : ;; - 0,) - # No file input but without resume parameters; catch - # user error to feed us a patch from standard input - # when there is already $dotest. This is somewhat - # unreliable -- stdin could be /dev/null for example - # and the caller did not intend to feed us a patch but - # wanted to continue unattended. - tty -s - ;; - *) - false - ;; - esac || - die "previous rebase directory $dotest still exists but mbox given." - resume=yes -else - # Make sure we are not given --skip nor --resolved - test ",$skip,$resolved," = ,,, || - die "Resolve operation not in progress, we are not resuming." + test "$#" != 0 && + die "previous rebase directory $dotest still exists but mbox given." + + test -f "$dotest/interactive" && + interactive=_i action=$(cat "$dotest/interactive") - # Start afresh. - mkdir -p "$dotest" || exit + # No file input but without resume parameters; catch + # user error to feed us a patch from standard input + # when there is already $dotest. This is somewhat + # unreliable -- stdin could be /dev/null for example + # and the caller did not intend to feed us a patch but + # wanted to continue unattended. + test -z "$resolved$skip" && tty -s - if test -n "$prefix" && test $# != 0 - then - first=t - for arg - do - test -n "$first" && { - set x - first= - } - case "$arg" in - /*) - set "$@" "$arg" ;; - *) - set "$@" "$prefix$arg" ;; - esac - done - shift - fi - git mailsplit -d"$prec" -o"$dotest" -b -- "$@" > "$dotest/last" || { - rm -fr "$dotest" - exit 1 - } + test -n "$resolved" && run_sequencer$interactive --continue + test -n "$skip" && run_sequencer$interactive --skip - # -b, -s, -u, -k and --whitespace flags are kept for the - # resuming session after a patch failure. - # -3 and -i can and must be given when resuming. - echo "$binary" >"$dotest/binary" - echo " $ws" >"$dotest/whitespace" - echo "$sign" >"$dotest/sign" - echo "$utf8" >"$dotest/utf8" - echo "$keep" >"$dotest/keep" - echo 1 >"$dotest/next" - if test -n "$rebasing" - then - : >"$dotest/rebasing" - else - : >"$dotest/applying" - git update-ref ORIG_HEAD HEAD - fi + die "$dotest still exists. Use git am --skip/--resolved." fi -case "$resolved" in -'') - files=$(git diff-index --cached --name-only HEAD --) || exit - if [ "$files" ]; then - echo "Dirty index: cannot apply patches (dirty: $files)" >&2 - exit 1 - fi -esac +# Make sure we are not given --skip nor --resolved +test -z "$resolved$skip" || + die 'git-am is not in progress. You cannot use --skip/--resolved then.' -if test "$(cat "$dotest/binary")" = t -then - binary=--allow-binary-replacement -fi -if test "$(cat "$dotest/utf8")" = t -then - utf8=-u -else - utf8=-n -fi -if test "$(cat "$dotest/keep")" = t -then - keep=-k -fi -ws=`cat "$dotest/whitespace"` -if test "$(cat "$dotest/sign")" = t +# sequencer running? +git sequencer --status >/dev/null 2>&1 && + die "Sequencer already started. Cannot run git-am." + +# Start afresh. +mkdir -p "$dotest" || + die "Could not create $dotest directory." + +if test -n "$prefix" && test $# != 0 then - SIGNOFF=`git-var GIT_COMMITTER_IDENT | sed -e ' - s/>.*/>/ - s/^/Signed-off-by: /' - ` -else - SIGNOFF= + first=t + for arg + do + test -n "$first" && { + set x + first= + } + case "$arg" in + /*) + set "$@" "$arg" ;; + *) + set "$@" "$prefix$arg" ;; + esac + done + shift fi +last=$(git mailsplit -d"$prec" -o"$dotest" -b -- "$@") || { + cleanup + exit 1 +} +this=1 -last=`cat "$dotest/last"` -this=`cat "$dotest/next"` -if test "$skip" = t -then - git rerere clear - this=`expr "$this" + 1` - resume= +files=$(git diff-index --cached --name-only HEAD --) || exit +if [ "$files" ]; then + echo "Dirty index: cannot apply patches (dirty: $files)" >&2 + exit 1 fi -if test "$this" -gt "$last" +test -n "$interactive" && echo 'again' >"$dotest/interactive" + +# converting our options to git-sequencer file insn options +test -n "$binary" && opts="$opts --binary" +test -n "$utf8" || opts="$opts -n" +test -n "$keep" && opts="$opts -k" +test -n "$sign" && opts="$opts --signoff" +test -n "$threeway" && opts="$opts -3" + +# these files are created for tab completion scripts +if test -n "$rebasing" then - echo Nothing to do. - rm -fr "$dotest" - exit + : >"$dotest/rebasing" +else + : >"$dotest/applying" + git update-ref ORIG_HEAD HEAD fi +# create todofile +: > "$todofile" || + die_abort "Cannot create $todofile" while test "$this" -le "$last" do - msgnum=`printf "%0${prec}d" $this` - next=`expr "$this" + 1` - test -f "$dotest/$msgnum" || { - resume= - go_next - continue - } - - # If we are not resuming, parse and extract the patch information - # into separate files: - # - info records the authorship and title - # - msg is the rest of commit log message - # - patch is the patch body. - # - # When we are resuming, these files are either already prepared - # by the user, or the user can tell us to do so by --resolved flag. - case "$resume" in - '') - git mailinfo $keep $utf8 "$dotest/msg" "$dotest/patch" \ - <"$dotest/$msgnum" >"$dotest/info" || - stop_here $this - - # skip pine's internal folder data - grep '^Author: Mail System Internal Data$' \ - <"$dotest"/info >/dev/null && - go_next && continue - - test -s "$dotest/patch" || { - echo "Patch is empty. Was it split wrong?" - stop_here $this - } - if test -f "$dotest/rebasing" && - commit=$(sed -e 's/^From \([0-9a-f]*\) .*/\1/' \ - -e q "$dotest/$msgnum") && - test "$(git cat-file -t "$commit")" = commit - then - git cat-file commit "$commit" | - sed -e '1,/^$/d' >"$dotest/msg-clean" - else - SUBJECT="$(sed -n '/^Subject/ s/Subject: //p' "$dotest/info")" - case "$keep_subject" in -k) SUBJECT="[PATCH] $SUBJECT" ;; esac - - (printf '%s\n\n' "$SUBJECT"; cat "$dotest/msg") | - git stripspace > "$dotest/msg-clean" - fi - ;; - esac - - GIT_AUTHOR_NAME="$(sed -n '/^Author/ s/Author: //p' "$dotest/info")" - GIT_AUTHOR_EMAIL="$(sed -n '/^Email/ s/Email: //p' "$dotest/info")" - GIT_AUTHOR_DATE="$(sed -n '/^Date/ s/Date: //p' "$dotest/info")" + msgnum=$(printf "%0${prec}d" $this) + this=$(($this+1)) - if test -z "$GIT_AUTHOR_EMAIL" - then - echo "Patch does not have a valid e-mail address." - stop_here $this - fi - - export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE - - case "$resume" in - '') - if test '' != "$SIGNOFF" - then - LAST_SIGNED_OFF_BY=` - sed -ne '/^Signed-off-by: /p' \ - "$dotest/msg-clean" | - sed -ne '$p' - ` - ADD_SIGNOFF=` - test "$LAST_SIGNED_OFF_BY" = "$SIGNOFF" || { - test '' = "$LAST_SIGNED_OFF_BY" && echo - echo "$SIGNOFF" - }` - else - ADD_SIGNOFF= - fi - { - if test -s "$dotest/msg-clean" - then - cat "$dotest/msg-clean" - fi - if test '' != "$ADD_SIGNOFF" - then - echo "$ADD_SIGNOFF" - fi - } >"$dotest/final-commit" - ;; - *) - case "$resolved$interactive" in - tt) - # This is used only for interactive view option. - git diff-index -p --cached HEAD -- >"$dotest/patch" - ;; - esac - esac - - resume= - if test "$interactive" = t - then - test -t 0 || - die "cannot be interactive without stdin connected to a terminal." - action=again - while test "$action" = again - do - echo "Commit Body is:" - echo "--------------------------" - cat "$dotest/final-commit" - echo "--------------------------" - printf "Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all " - read reply - case "$reply" in - [yY]*) action=yes ;; - [aA]*) action=yes interactive= ;; - [nN]*) action=skip ;; - [eE]*) git_editor "$dotest/final-commit" - action=again ;; - [vV]*) action=again - LESS=-S ${PAGER:-less} "$dotest/patch" ;; - *) action=again ;; - esac - done - else - action=yes - fi - FIRSTLINE=$(sed 1q "$dotest/final-commit") - - if test $action = skip - then - go_next + # This ignores every mail that does not contain a patch. + grep '^diff' "$dotest/$msgnum" >/dev/null || continue - fi - if test -x "$GIT_DIR"/hooks/applypatch-msg - then - "$GIT_DIR"/hooks/applypatch-msg "$dotest/final-commit" || - stop_here $this - fi - - printf 'Applying %s\n' "$FIRSTLINE" - - case "$resolved" in - '') - git apply $git_apply_opt $binary --index "$dotest/patch" - apply_status=$? - ;; - t) - # Resolved means the user did all the hard work, and - # we do not have to do any patch application. Just - # trust what the user has in the index file and the - # working tree. - resolved= - git diff-index --quiet --cached HEAD -- && { - echo "No changes - did you forget to use 'git add'?" - stop_here_user_resolve $this - } - unmerged=$(git ls-files -u) - if test -n "$unmerged" - then - echo "You still have unmerged paths in your index" - echo "did you forget to use 'git add'?" - stop_here_user_resolve $this - fi - apply_status=0 - git rerere - ;; - esac - - if test $apply_status = 1 && test "$threeway" = t - then - if (fall_back_3way) - then - # Applying the patch to an earlier tree and merging the - # result may have produced the same tree as ours. - git diff-index --quiet --cached HEAD -- && { - echo No changes -- Patch already applied. - go_next - continue - } - # clear apply_status -- we have successfully merged. - apply_status=0 - fi - fi - if test $apply_status != 0 - then - echo Patch failed at $msgnum. - stop_here_user_resolve $this - fi - - if test -x "$GIT_DIR"/hooks/pre-applypatch - then - "$GIT_DIR"/hooks/pre-applypatch || stop_here $this - fi - - tree=$(git write-tree) && - parent=$(git rev-parse --verify HEAD) && - commit=$(git commit-tree $tree -p $parent <"$dotest/final-commit") && - git update-ref -m "$GIT_REFLOG_ACTION: $FIRSTLINE" HEAD $commit $parent || - stop_here $this - - if test -x "$GIT_DIR"/hooks/post-applypatch - then - "$GIT_DIR"/hooks/post-applypatch - fi - - go_next + extra= + test -n "$rebasing" && + commit=$(sed -e 's/^From \([0-9a-f]*\) .*/\1/' \ + -e q "$dotest/$msgnum") && + test "$(git cat-file -t "$commit")" = commit && + extra=" -C $commit" + + subject=$(sed -n '1,/^Subject:/s/Subject: *\(\[.*\]\)\{0,1\} *//p' \ + <"$dotest/$msgnum") + test -n "$interactive" || + printf 'run -- printf '\''Applying "%%s"\\n'\'' '\''%s'\''\n' \ + "$(printf '%s\n' "$subject" | + sed "s/'/'\\\\''/g")" >>"$todofile" + printf 'patch%s%s "%s" # %s\n' "$opts" "$extra" "$dotest/$msgnum" \ + "$subject" >>"$todofile" + test -z "$interactive" || echo 'pause' >>"$todofile" done -git gc --auto - -rm -fr "$dotest" +noadvice=--no-advice +run_sequencer$interactive "$todofile" diff --git a/git-rebase.sh b/git-rebase.sh index 56cf6f0..231c486 100755 --- a/git-rebase.sh +++ b/git-rebase.sh @@ -220,6 +220,7 @@ do dotest="$GIT_DIR"/rebase move_to_original_branch fi + rm -rf "$GIT_DIR/sequencer" git reset --hard $(cat "$dotest/orig-head") rm -r "$dotest" exit diff --git a/t/t4150-am.sh b/t/t4150-am.sh index 5cbd5ef..e771806 100755 --- a/t/t4150-am.sh +++ b/t/t4150-am.sh @@ -212,14 +212,12 @@ test_expect_success 'am takes patches from a Pine mailbox' ' ' test_expect_success 'am fails on mail without patch' ' - ! git am <failmail && - rm -r .git/rebase/ + test_must_fail git am <failmail ' test_expect_success 'am fails on empty patch' ' echo "---" >>failmail && - ! git am <failmail && - git am --skip && + test_must_fail git am <failmail && ! test -d .git/rebase ' -- 1.5.6.3.391.ge45b ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH 2/2] Introduce git am --abort 2008-07-16 20:45 ` [PATCH 1/2] Migrate git-am to use git-sequencer Stephan Beyer @ 2008-07-16 20:45 ` Stephan Beyer 0 siblings, 0 replies; 8+ messages in thread From: Stephan Beyer @ 2008-07-16 20:45 UTC (permalink / raw) To: git; +Cc: Stephan Beyer, Daniel Barkalow, Christian Couder, Junio C Hamano Currently, aborting a git-am process during a conflict is done by resetting to the HEAD before applying the patches and removing the .git/rebase directory manually. This patch introduces an --abort option for git-am to make this as easy as in git-rebase. Mentored-by: Christian Couder <chriscool@tuxfamily.org> Mentored-by: Daniel Barkalow <barkalow@iabervon.org> Signed-off-by: Stephan Beyer <s-beyer@gmx.net> --- Documentation/git-am.txt | 5 ++++- Documentation/git-rerere.txt | 2 +- contrib/completion/git-completion.bash | 2 +- git-am.sh | 19 ++++++++++++------- git-rebase.sh | 8 +++----- t/t4150-am.sh | 21 +++++++++++++++++---- 6 files changed, 38 insertions(+), 19 deletions(-) diff --git a/Documentation/git-am.txt b/Documentation/git-am.txt index 2d7f162..df35cee 100644 --- a/Documentation/git-am.txt +++ b/Documentation/git-am.txt @@ -13,7 +13,7 @@ SYNOPSIS [--3way] [--interactive] [--binary] [--whitespace=<option>] [-C<n>] [-p<n>] [<mbox> | <Maildir>...] -'git am' (--skip | --resolved) +'git am' (--abort | --skip | --resolved) DESCRIPTION ----------- @@ -79,6 +79,9 @@ default. You could use `--no-utf8` to override this. --interactive:: Run interactively. +--abort:: + Abort applying and rewind applied patches. + --skip:: Skip the current patch. This is only meaningful when restarting an aborted patch. diff --git a/Documentation/git-rerere.txt b/Documentation/git-rerere.txt index 678bfd3..ad81dbc 100644 --- a/Documentation/git-rerere.txt +++ b/Documentation/git-rerere.txt @@ -37,7 +37,7 @@ its working state. 'clear':: This resets the metadata used by rerere if a merge resolution is to be -is aborted. Calling 'git-am --skip' or 'git-rebase [--skip|--abort]' +is aborted. Calling 'git-am [--skip|--abort]' or 'git-rebase [--skip|--abort]' will automatically invoke this command. 'diff':: diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 29f6cd4..d271ba0 100755 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -489,7 +489,7 @@ _git_am () { local cur="${COMP_WORDS[COMP_CWORD]}" dir="$(__gitdir)" if [ -d "$dir"/rebase ]; then - __gitcomp "--skip --resolved" + __gitcomp "--skip --resolved --abort" return fi case "$cur" in diff --git a/git-am.sh b/git-am.sh index e36f22c..7301314 100755 --- a/git-am.sh +++ b/git-am.sh @@ -8,6 +8,7 @@ OPTIONS_SPEC="\ git-am [options] [<mbox>|<Maildir>...] git-am [options] --resolved git-am [options] --skip +git-am [options] --abort -- d,dotest= (removed -- do not use) i,interactive run interactively @@ -22,6 +23,7 @@ p= pass it through git-apply resolvemsg= override error message when patch failure occurs r,resolved to be used after a patch failure skip skip the current patch +abort abort patching and reset done patches rebasing (internal use for git-rebase)" . git-sh-setup @@ -108,7 +110,7 @@ print_continue_info () { } run_sequencer () { - git sequencer $noadvice --caller='git am||--resolved|--skip' "$@" + git sequencer $noadvice --caller='git am|--abort|--resolved|--skip' "$@" case "$?" in 0) cleanup @@ -130,7 +132,7 @@ run_sequencer_i () { while true do output=$(git sequencer $noadvice \ - --caller='git am -i||--resolved|--skip' \ + --caller='git am -i|--abort|--resolved|--skip' \ $command 2>&1 >/dev/null) noadvice= case "$?" in @@ -170,6 +172,8 @@ do case "$1" in -i|--interactive) interactive=_i ;; + --abort) + abort=t ;; -b|--binary) binary=t ;; -3|--3way) @@ -219,17 +223,18 @@ then # unreliable -- stdin could be /dev/null for example # and the caller did not intend to feed us a patch but # wanted to continue unattended. - test -z "$resolved$skip" && tty -s + test -z "$abort$resolved$skip" && tty -s + test -n "$abort" && run_sequencer$interactive --abort test -n "$resolved" && run_sequencer$interactive --continue test -n "$skip" && run_sequencer$interactive --skip - die "$dotest still exists. Use git am --skip/--resolved." + die "$dotest still exists. Use git am --abort/--skip/--resolved." fi -# Make sure we are not given --skip nor --resolved -test -z "$resolved$skip" || - die 'git-am is not in progress. You cannot use --skip/--resolved then.' +# Make sure we are not given --skip nor --resolved nor --abort +test -z "$abort$resolved$skip" || + die 'git-am is not in progress. You cannot use --abort/--skip/--resolved then.' # sequencer running? git sequencer --status >/dev/null 2>&1 && diff --git a/git-rebase.sh b/git-rebase.sh index 231c486..a83933b 100755 --- a/git-rebase.sh +++ b/git-rebase.sh @@ -216,13 +216,11 @@ do if test -d "$dotest" then move_to_original_branch + git reset --hard $(cat "$dotest/orig-head") + rm -r "$dotest" else - dotest="$GIT_DIR"/rebase - move_to_original_branch + git am --abort fi - rm -rf "$GIT_DIR/sequencer" - git reset --hard $(cat "$dotest/orig-head") - rm -r "$dotest" exit ;; --onto) diff --git a/t/t4150-am.sh b/t/t4150-am.sh index e771806..152e2d9 100755 --- a/t/t4150-am.sh +++ b/t/t4150-am.sh @@ -83,6 +83,10 @@ test_expect_success setup ' git commit -m third && git format-patch --stdout first >patch2 && git checkout -b lorem && + echo new >another && + git add another && + test_tick && + git commit -m "added another file" && sed -n -e "11,\$p" msg >file && head -n 9 msg >>file && test_tick && @@ -181,8 +185,8 @@ test_expect_success 'am -3 falls back to 3-way merge' ' ' test_expect_success 'am pauses on conflict' ' - git checkout lorem2^^ && - ! git am lorem-move.patch && + git checkout lorem2~3 && + test_must_fail git am lorem-move.patch && test -d .git/rebase ' @@ -193,9 +197,18 @@ test_expect_success 'am --skip works' ' test goodbye = "$(cat another)" ' +test_expect_success 'am --abort works' ' + git checkout lorem2~3 && + test_must_fail git am lorem-move.patch && + test -d .git/rebase && + git am --abort && + test "$(git rev-parse HEAD)" = "$(git rev-parse lorem2~3)" && + ! test -f another +' + test_expect_success 'am --resolved works' ' - git checkout lorem2^^ && - ! git am lorem-move.patch && + git checkout lorem2~3 && + test_must_fail git am lorem-move.patch && test -d .git/rebase && echo resolved >>file && git add file && -- 1.5.6.3.391.g7ab7e ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH] Migrate rebase-i to sequencer 2008-07-16 20:45 ` Sequencer migration patches Stephan Beyer 2008-07-16 20:45 ` [PATCH 1/2] Migrate git-am to use git-sequencer Stephan Beyer @ 2008-07-16 20:45 ` Stephan Beyer 2008-07-17 13:05 ` Sequencer migration patches Stephan Beyer 2 siblings, 0 replies; 8+ messages in thread From: Stephan Beyer @ 2008-07-16 20:45 UTC (permalink / raw) To: git Cc: Stephan Beyer, Daniel Barkalow, Christian Couder, Junio C Hamano, Johannes Schindelin The migration of pure rebase-i to sequencer is simply done by generating the todo list, but with a comment marker (`#') before the description, and then feed it to git sequencer. For git-rebase-i -p (preserving merges) merges should be rewritten. For this, the sequencer instructions "mark", "merge" and "reset" are used. Mentored-by: Christian Couder <chriscool@tuxfamily.org> Mentored-by: Daniel Barkalow <barkalow@iabervon.org> Signed-off-by: Stephan Beyer <s-beyer@gmx.net> --- git-rebase--interactive.sh | 438 ++++++++++------------------------------- t/t3404-rebase-interactive.sh | 9 +- 2 files changed, 113 insertions(+), 334 deletions(-) diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index da79a24..2136e02 100755 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -42,11 +42,6 @@ STRATEGY= ONTO= VERBOSE= -GIT_CHERRY_PICK_HELP=" After resolving the conflicts, -mark the corrected paths with 'git add <paths>', and -run 'git rebase --continue'" -export GIT_CHERRY_PICK_HELP - warn () { echo "$*" >&2 } @@ -74,48 +69,6 @@ require_clean_work_tree () { 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 -} - -last_count= -mark_action_done () { - sed -e 1q < "$TODO" >> "$DONE" - sed -e 1d < "$TODO" >> "$TODO".new - mv -f "$TODO".new "$TODO" - count=$(grep -c '^[^#]' < "$DONE") - total=$(($count+$(grep -c '^[^#]' < "$TODO"))) - if test "$last_count" != "$count" - then - last_count=$count - printf "Rebasing (%d/%d)\r" $count $total - test -z "$VERBOSE" || echo - fi -} - -make_patch () { - parent_sha1=$(git rev-parse --verify "$1"^) || - die "Cannot get patch for $1^" - git diff-tree -p "$parent_sha1".."$1" > "$DOTEST"/patch - test -f "$DOTEST"/message || - git cat-file commit "$1" | sed "1,/^$/d" > "$DOTEST"/message - test -f "$DOTEST"/author-script || - get_author_ident_from_commit "$1" > "$DOTEST"/author-script -} - -die_with_patch () { - make_patch "$1" - git rerere - die "$2" -} - die_abort () { rm -rf "$DOTEST" die "$1" @@ -125,48 +78,20 @@ has_action () { grep '^[^#]' "$1" >/dev/null } -pick_one () { - no_ff= - case "$1" in -n) sha1=$2; no_ff=t ;; *) sha1=$1 ;; esac - output 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^) || - die "Could not get the parent of $sha1" - current_sha1=$(git rev-parse --verify HEAD) - if test "$no_ff$current_sha1" = "$parent_sha1"; then - output git reset --hard $sha1 - test "a$1" = a-n && output git reset --soft $current_sha1 - sha1=$(git rev-parse --short $sha1) - output warn Fast forward to $sha1 - else - output git cherry-pick "$@" - fi -} - -pick_one_preserving_merges () { - case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac +create_todo_preserving_merges () { + shortsha1=$sha1 sha1=$(git rev-parse $sha1) - if test -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= + first_parent= for p in $(git rev-list --parents -1 $sha1 | cut -d' ' -f2-) do + # check if we've already seen this parent if test -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 @@ -174,186 +99,49 @@ pick_one_preserving_merges () { new_parents="$new_parents $new_p" ;; esac + else + new_parents="$new_parents $p" fi + test -n "$first_parent" || first_parent=$p done - case $fast_forward in - t) - output warn "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 - output 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')" - # No point in merging the first parent, that's HEAD - new_parents=${new_parents# $first_parent} - if ! GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \ - GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \ - GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \ - output git merge $STRATEGY -m "$msg" \ - $new_parents - then - git rerere - printf "%s\n" "$msg" > "$GIT_DIR"/MERGE_MSG - die Error redoing merge $sha1 - fi - ;; - *) - output git cherry-pick "$@" || - die_with_patch $sha1 "Could not pick $sha1" - ;; - esac - ;; - esac -} + # We do not have parent, so ignore this commit + test t = $preserve && return -nth_string () { - case "$1" in - *1[0-9]|*[04-9]) echo "$1"th;; - *1) echo "$1"st;; - *2) echo "$1"nd;; - *3) echo "$1"rd;; - esac -} + # We always write a mark, because we do not know if there will + # be a "reset" or "merge" + # Filter the unneeded marks out afterwards. + echo "mark :$mark" + mark=$(($mark+1)) -make_squash_message () { - if test -f "$SQUASH_MSG"; then - COUNT=$(($(sed -n "s/^# This is [^0-9]*\([1-9][0-9]*\).*/\1/p" \ - < "$SQUASH_MSG" | sed -ne '$p')+1)) - echo "# This is a combination of $COUNT commits." - sed -e 1d -e '2,/^./{ - /^$/d - }' <"$SQUASH_MSG" - else - COUNT=2 - echo "# This is a combination of two commits." - echo "# The first commit's message is:" - echo - git cat-file commit HEAD | sed -e '1,/^$/d' - fi - echo - echo "# This is the $(nth_string $COUNT) commit message:" - echo - git cat-file commit $1 | sed -e '1,/^$/d' -} + new_first_parent=$(expr "$new_parents" : ' \([^ ]*\)') -peek_next_command () { - sed -n "1s/ .*$//p" < "$TODO" -} + # Reset if needed + test -z "$first_parent" -o "$first_parent" = $lastsha1 || + echo "reset $new_first_parent" -do_next () { - rm -f "$DOTEST"/message "$DOTEST"/author-script \ - "$DOTEST"/amend || exit - read command sha1 rest < "$TODO" - case "$command" in - '#'*|'') - mark_action_done - ;; - pick|p) - comment_for_reflog pick + echo ":$mark" > "$REWRITTEN"/$sha1 - mark_action_done - pick_one $sha1 || - die_with_patch $sha1 "Could not apply $sha1... $rest" - ;; - edit|e) - comment_for_reflog edit - - mark_action_done - pick_one $sha1 || - die_with_patch $sha1 "Could not apply $sha1... $rest" - make_patch $sha1 - : > "$DOTEST"/amend - warn - warn "You can amend the commit now, with" - warn - warn " git commit --amend" - warn - warn "Once you are satisfied with your changes, run" - warn - warn " git rebase --continue" - warn - exit 0 - ;; - squash|s) - comment_for_reflog squash - - has_action "$DONE" || - die "Cannot 'squash' without a previous commit" - - mark_action_done - make_squash_message $sha1 > "$MSG" - case "$(peek_next_command)" in - squash|s) - EDIT_COMMIT= - USE_OUTPUT=output - cp "$MSG" "$SQUASH_MSG" - ;; - *) - EDIT_COMMIT=-e - USE_OUTPUT= - rm -f "$SQUASH_MSG" || exit - ;; - esac - - failed=f - author_script=$(get_author_ident_from_commit HEAD) - output git reset --soft HEAD^ - pick_one -n $sha1 || failed=t - echo "$author_script" > "$DOTEST"/author-script - if test $failed = f - then - # This is like --amend, but with a different message - eval "$author_script" - GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \ - GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \ - GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \ - $USE_OUTPUT git commit --no-verify -F "$MSG" $EDIT_COMMIT || failed=t - fi - if test $failed = t - then - cp "$MSG" "$GIT_DIR"/MERGE_MSG - warn - warn "Could not apply $sha1... $rest" - die_with_patch $sha1 "" - fi + # Merge or pick + case "$new_parents" in + ' '*' '*) + new_parents=${new_parents# $new_first_parent} + printf 'merge%s -C %s%s\t%s\n' "$STRATEGY" \ + "$shortsha1" "$new_parents" "$rest" ;; *) - warn "Unknown command: $command $sha1 $rest" - die_with_patch $sha1 "Please fix this in the file $TODO." + printf 'pick %s\t%s\n' "$shortsha1" "$rest" ;; esac - test -s "$TODO" && return - comment_for_reflog finish && + lastsha1="$sha1" + return 0 +} + +update_refs_and_exit () { HEADNAME=$(cat "$DOTEST"/head-name) && OLDHEAD=$(cat "$DOTEST"/head) && SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) && - if test -d "$REWRITTEN" - then - test -f "$DOTEST"/current-commit && - current_commit=$(cat "$DOTEST"/current-commit) && - git rev-parse HEAD > "$REWRITTEN"/$current_commit - if test -f "$REWRITTEN"/$OLDHEAD - then - NEWHEAD=$(cat "$REWRITTEN"/$OLDHEAD) - else - NEWHEAD=$OLDHEAD - fi - else - NEWHEAD=$(git rev-parse HEAD) - fi && + NEWHEAD=$(git rev-parse HEAD) && case $HEADNAME in refs/*) message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" && @@ -371,13 +159,6 @@ do_next () { exit } -do_rest () { - while : - do - do_next - done -} - # check if no other options are set is_standalone () { test $# -eq 2 -a "$2" = '--' && @@ -393,80 +174,47 @@ get_saved_options () { test -f "$DOTEST"/verbose && VERBOSE=t } -while test $# != 0 -do - case "$1" in - --continue) - is_standalone "$@" || usage - get_saved_options - comment_for_reflog continue - - test -d "$DOTEST" || die "No interactive rebase running" - - # Sanity check - git rev-parse --verify HEAD >/dev/null || - die "Cannot read HEAD" - git update-index --ignore-submodules --refresh && - git diff-files --quiet --ignore-submodules || - die "Working tree is dirty" - - # do we have anything to commit? - if git diff-index --cached --quiet --ignore-submodules HEAD -- +run_sequencer () { + git sequencer --caller='git rebase -i|--abort|--continue|--skip' "$@" + case "$?" in + 0) + if test "$1" = --abort then - : Nothing to commit -- skip this + rm -rf "$DOTEST" + exit else - . "$DOTEST"/author-script || - die "Cannot find the author identity" - if test -f "$DOTEST"/amend - then - git reset --soft HEAD^ || - die "Cannot rewind the HEAD" - fi - export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE && - git commit --no-verify -F "$DOTEST"/message -e || - die "Could not commit staged changes." + update_refs_and_exit fi - - require_clean_work_tree - do_rest ;; - --abort) - is_standalone "$@" || usage - get_saved_options - comment_for_reflog abort - - git rerere clear - test -d "$DOTEST" || die "No interactive rebase running" - - HEADNAME=$(cat "$DOTEST"/head-name) - HEAD=$(cat "$DOTEST"/head) - case $HEADNAME in - refs/*) - git symbolic-ref HEAD $HEADNAME - ;; - esac && - output git reset --hard $HEAD && - rm -rf "$DOTEST" - exit + 2) + # pause + exit 0 ;; - --skip) - is_standalone "$@" || usage - get_saved_options - comment_for_reflog skip - - git rerere clear - test -d "$DOTEST" || die "No interactive rebase running" + 3) + # conflict + exit 1 + ;; + *) + die_abort 'git-sequencer died unexpected.' + ;; + esac +} - output git reset --hard && do_rest +while test $# != 0 +do + case "$1" in + --abort|--continue|--skip) + is_standalone "$@" || usage + run_sequencer "$1" ;; -s) case "$#,$1" in *,*=*) - STRATEGY="-s "$(expr "z$1" : 'z-[^=]*=\(.*\)') ;; + STRATEGY=" -s "$(expr "z$1" : 'z-[^=]*=\(.*\)') ;; 1,*) usage ;; *) - STRATEGY="-s $2" + STRATEGY=" -s $2" shift ;; esac ;; @@ -492,12 +240,12 @@ do test $# -eq 1 -o $# -eq 2 || usage test -d "$DOTEST" && die "Interactive rebase already started" + git sequencer --status >/dev/null 2>&1 && + die "Sequencer already started. Cannot run rebase." git var GIT_COMMITTER_IDENT >/dev/null || die "You need to set your committer info first" - comment_for_reflog start - require_clean_work_tree UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base" @@ -514,42 +262,72 @@ do HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?" mkdir "$DOTEST" || die "Could not create temporary $DOTEST" - : > "$DOTEST"/interactive || die "Could not mark as interactive" + : > "$DOTEST"/interactive || die_abort "Could not mark as interactive" git symbolic-ref HEAD > "$DOTEST"/head-name 2> /dev/null || echo "detached HEAD" > "$DOTEST"/head-name echo $HEAD > "$DOTEST"/head - echo $UPSTREAM > "$DOTEST"/upstream echo $ONTO > "$DOTEST"/onto test -z "$STRATEGY" || echo "$STRATEGY" > "$DOTEST"/strategy test t = "$VERBOSE" && : > "$DOTEST"/verbose if test t = "$PRESERVE_MERGES" then + lastsha1= # $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.) + # reachable on the way between $UPSTREAM and $HEAD. + # The filename is the SHA1 of the old value and the + # content is the SHA1 or :mark of the new one. mkdir "$REWRITTEN" && for c in $(git merge-base --all $HEAD $UPSTREAM) do + test -n "$lastsha1" || lastsha1=$c echo $ONTO > "$REWRITTEN"/$c || die "Could not init rewritten commits" done - MERGES_OPTION= - else - MERGES_OPTION=--no-merges + git rev-list --abbrev-commit --abbrev=7 \ + --pretty=format:"%m%h # %s" --topo-order \ + --reverse --cherry-pick $UPSTREAM...$HEAD | \ + sed -n -e "s/^>//p" > "$DOTEST"/commit-list + + mark=0 + while read -r sha1 rest + do + create_todo_preserving_merges + done < "$DOTEST"/commit-list > "$TODO" + + # We now have more "mark :..." lines than needed. + # Remove the unused. This is just a step to keep + # the list clean. + keep_marks=$(sed -e "/^mark :/d" <"$TODO" | + sed -n -e 's/^[^#]* :\([0-9][0-9]*\).*$/:\1:/p') + while read -r line + do + case "$line" in + 'mark :'*) + case "$keep_marks " in + *${line#mark }:*) + echo "$line" + ;; + esac + ;; + *) + printf '%s\n' "$line" + ;; + esac + done < "$TODO" > "$TODO".new + mv "$TODO".new "$TODO" + fi + if test -z "$PRESERVE_MERGES" + then + git rev-list --no-merges --abbrev-commit --abbrev=7 \ + --pretty=format:"%mpick %h # %s" \ + --reverse --cherry-pick $UPSTREAM...$HEAD | \ + sed -n -e "s/^>//p" > "$TODO" fi SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM) SHORTHEAD=$(git rev-parse --short $HEAD) SHORTONTO=$(git rev-parse --short $ONTO) - git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \ - --abbrev=7 --reverse --left-right --cherry-pick \ - $UPSTREAM...$HEAD | \ - sed -n "s/^>/pick /p" > "$TODO" cat >> "$TODO" << EOF # Rebase $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO @@ -575,7 +353,7 @@ EOF die_abort "Nothing to do" git update-ref ORIG_HEAD $HEAD - output git checkout $ONTO && do_rest + run_sequencer --onto "$ONTO" "$TODO" ;; esac shift diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index ffe3dd9..64a28ef 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -159,19 +159,20 @@ test_expect_success 'stop on conflicting pick' ' git tag new-branch1 && test_must_fail git rebase -i master && test "$(git rev-parse HEAD~3)" = "$(git rev-parse master)" && - test_cmp expect .git/rebase-merge/patch && + test_cmp expect .git/sequencer/patch && test_cmp expect2 file1 && test "$(git-diff --name-status | sed -n -e "/^U/s/^U[^a-z]*//p")" = file1 && - test 4 = $(grep -v "^#" < .git/rebase-merge/done | wc -l) && - test 0 = $(grep -c "^[^#]" < .git/rebase-merge/git-rebase-todo) + test 4 = $(grep -v "^#" < .git/sequencer/done | wc -l) && + test 0 = $(grep -c "^[^#]" < .git/sequencer/todo) && + test -d .git/rebase-merge ' test_expect_success 'abort' ' git rebase --abort && test $(git rev-parse new-branch1) = $(git rev-parse HEAD) && test "$(git symbolic-ref -q HEAD)" = "refs/heads/branch1" && - ! test -d .git/rebase-merge + ! test -d .git/sequencer ' test_expect_success 'retain authorship' ' -- 1.5.6.3.391.ge45b ^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: Sequencer migration patches 2008-07-16 20:45 ` Sequencer migration patches Stephan Beyer 2008-07-16 20:45 ` [PATCH 1/2] Migrate git-am to use git-sequencer Stephan Beyer 2008-07-16 20:45 ` [PATCH] Migrate rebase-i to sequencer Stephan Beyer @ 2008-07-17 13:05 ` Stephan Beyer 2 siblings, 0 replies; 8+ messages in thread From: Stephan Beyer @ 2008-07-17 13:05 UTC (permalink / raw) To: git; +Cc: Daniel Barkalow, Christian Couder, Junio C Hamano, Johannes Schindelin Hi, Stephan Beyer wrote: > Rebasing 100 commits takes 10.1 seconds instead of only 4.8 seconds > on my test machine. Btw, the builtin-sequencer needs 1.7 seconds to pick that 100 commits on my test machine. Regards, Stephan -- Stephan Beyer <s-beyer@gmx.net>, PGP 0x6EDDD207FCC5040F ^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2008-07-17 13:06 UTC | newest] Thread overview: 8+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2008-07-16 20:45 [PATCH 1/3] Add git-sequencer shell prototype Stephan Beyer 2008-07-16 20:45 ` [PATCH 2/3] Add git-sequencer documentation Stephan Beyer 2008-07-16 20:45 ` [PATCH 3/3] Add git-sequencer test suite (t3350) Stephan Beyer 2008-07-16 20:45 ` Sequencer migration patches Stephan Beyer 2008-07-16 20:45 ` [PATCH 1/2] Migrate git-am to use git-sequencer Stephan Beyer 2008-07-16 20:45 ` [PATCH 2/2] Introduce git am --abort Stephan Beyer 2008-07-16 20:45 ` [PATCH] Migrate rebase-i to sequencer Stephan Beyer 2008-07-17 13:05 ` Sequencer migration patches Stephan Beyer
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for NNTP newsgroup(s).