rebase -i: Make option handling in pick_one more flexible
A message that is not in the archive (sent off-list or not yet received): [email protected]
RFC PATCH 1/7 rebase -i: Make option handling in pick_one more flexible
pick_one and pick_one_preserving_merges are wrappers around cherry-pick in rebase --interactive. They take the hash of a commit and build a cherry-pick command line that
- respects the user-supplied merge options
- disables complaints about empty commits
- tries to fast-forward the rebase head unless rebase is forced
- suppresses output unless the user requested higher verbosity
- rewrites merge commits to point to their rebased parents.
pick_one is used to implement not only pick but also squash, which amends the previous commit rather than creating a new one. When pick_one is called from squash, it receives a second argument -n. This tells pick_one to apply the changes to the index without committing them. Since the argument is almost directly passed to cherry-pick, we might want to do the same with other cherry-pick options. Currently, pick_one expects -n to be the first and only argument except for the commit hash.
Prepare pick_one for additional cherry-pick options by allowing -n to appear anywhere before the commit hash in the argument list. Loop over the argument list and pop each handled item until the commit hash is the only parameter left on the list. If an option is not supported, ignore it and issue a warning on the console. Construct a new arguments list extra_args of recognized options that shall be passed to cherry-pick on the command line.
--- git-rebase--interactive.sh | 61 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 16 deletions(-)
git-rebase--interactive.sh +45 -16
@@ -237,8 +237,26 @@ git_sequence_editor () { pick_one () { ff=--ff + extra_args= + while test $# -gt 0 + do + case "$1" in + -n) + ff= + extra_args="$extra_args -n" + ;; + -*) + warn "pick_one: ignored option -- $1" + ;; + *) + break + ;; + esac + shift + done + test $# -ne 1 && die "pick_one: wrong number of arguments" + sha1=$1 - case "$1" in -n) sha1=$2; ff= ;; *) sha1=$1 ;; esac case "$force_rebase" in '') ;; ?*) ff= ;; esac output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
@@ -248,24 +266,35 @@ pick_one () { fi test -d "$rewritten" && - pick_one_preserving_merges "$@" && return + pick_one_preserving_merges $extra_args $sha1 && return output eval git cherry-pick \ ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \ - "$strategy_args" $empty_args $ff "$@" + "$strategy_args" $empty_args $ff $extra_args $sha1 } pick_one_preserving_merges () { fast_forward=t - case "$1" in - -n) - fast_forward=f - sha1=$2 - ;; - *) - sha1=$1 - ;; - esac - sha1=$(git rev-parse $sha1) + no_commit= + extra_args= + while test $# -gt 0 + do + case "$1" in + -n) + fast_forward=f + extra_args="$extra_args -n" + no_commit=y + ;; + -*) + warn "pick_one_preserving_merges: ignored option -- $1" + ;; + *) + break + ;; + esac + shift + done + test $# -ne 1 && die "pick_one_preserving_merges: wrong number of arguments" + sha1=$(git rev-parse $1) if test -f "$state_dir"/current-commit then
@@ -335,7 +364,7 @@ pick_one_preserving_merges () { f) first_parent=$(expr "$new_parents" : ' \([^ ]*\)') - if [ "$1" != "-n" ] + if test -z "$no_commit" then # detach HEAD to current parent output git checkout $first_parent 2> /dev/null ||
@@ -344,7 +373,7 @@ pick_one_preserving_merges () { case "$new_parents" in ' '*' '*) - test "a$1" = a-n && die "Refusing to squash a merge: $sha1" + test -n "$no_commit" && die "Refusing to squash a merge: $sha1" # redo merge author_script_content=$(get_author_ident_from_commit $sha1)
@@ -365,7 +394,7 @@ pick_one_preserving_merges () { *) output eval git cherry-pick \ ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \ - "$strategy_args" "$@" || + "$strategy_args" $extra_args $sha1 || die_with_patch $sha1 "Could not pick $sha1" ;; esac
Signature · 1 line
2.0.0
Quoted 41 lines · Fabian Ruch
pick_oneandpick_one_preserving_mergesare wrappers aroundcherry-pickinrebase --interactive. They take the hash of a commit and build acherry-pickcommand line that- respects the user-supplied merge options
- disables complaints about empty commits
- tries to fast-forward the rebase head unless rebase is forced
- suppresses output unless the user requested higher verbosity
- rewrites merge commits to point to their rebased parents.
pick_oneis used to implement not onlypickbut alsosquash, which amends the previous commit rather than creating a new one. Whenpick_oneis called fromsquash, it receives a second argument-n. This tellspick_oneto apply the changes to the index without committing them. Since the argument is almost directly passed tocherry-pick, we might want to do the same with othercherry-pickoptions. Currently,pick_oneexpects-nto be the first and only argument except for the commit hash.Prepare
pick_onefor additionalcherry-pickoptions by allowing-nto appear anywhere before the commit hash in the argument list. Loop over the argument list and pop each handled item until the commit hash is the only parameter left on the list. If an option is not supported, ignore it and issue a warning on the console. Construct a new arguments listextra_argsof recognized options that shall be passed tocherry-pickon the command line.Signed-off-by Fabian Ruch <[email protected]>--- git-rebase--interactive.sh | 61 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 16 deletions(-)git-rebase--interactive.sh +1
-0@@ -237,8 +237,26 @@ git_sequence_editor () { pick_one () { ff=--ff + extra_args=
git-rebase--interactive.sh git_sequence_editor () { · line 250
Quoted 10 lines
git-rebase--interactive.sh +10
-0+ while test $# -gt 0 + do + case "$1" in + -n) + ff= + extra_args="$extra_args -n" + ;; + -*) + warn "pick_one: ignored option -- $1" + ;;
This is an internal interface, right? I.e., user input isn't being processed here? If so, then the presence of an unrecognized option is a bug and it is preferable to "die" here rather than "warn".
The same below and in at least one later commit.
Quoted 14 lines
git-rebase--interactive.sh +8
-1+ *) + break + ;; + esac + shift + done + test $# -ne 1 && die "pick_one: wrong number of arguments" + sha1=$1 - case "$1" in -n) sha1=$2; ff= ;; *) sha1=$1 ;; esac case "$force_rebase" in '') ;; ?*) ff= ;; esac output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
git-rebase--interactive.sh pick_one () { · line 273
Quoted 10 lines
git-rebase--interactive.sh +2
-2@@ -248,24 +266,35 @@ pick_one () { fi test -d "$rewritten" && - pick_one_preserving_merges "$@" && return + pick_one_preserving_merges $extra_args $sha1 && return output eval git cherry-pick \ ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \ - "$strategy_args" $empty_args $ff "$@" + "$strategy_args" $empty_args $ff $extra_args $sha1 }
It might be confusing that extra_args is used both in pick_one and in pick_one_preserving_merges. Since these are not local variables, the call to the latter changes the value of the variable in the former. I don't know if that could be a problem now (can pick_one_preserving_merges return with a nonzero exit code?) but even if not, it is a trap for future developers. I recommend giving the two variables different names.
Quoted 64 lines
git-rebase--interactive.sh +24
-13pick_one_preserving_merges () { fast_forward=t - case "$1" in - -n) - fast_forward=f - sha1=$2 - ;; - *) - sha1=$1 - ;; - esac - sha1=$(git rev-parse $sha1) + no_commit= + extra_args= + while test $# -gt 0 + do + case "$1" in + -n) + fast_forward=f + extra_args="$extra_args -n" + no_commit=y + ;; + -*) + warn "pick_one_preserving_merges: ignored option -- $1" + ;; + *) + break + ;; + esac + shift + done + test $# -ne 1 && die "pick_one_preserving_merges: wrong number of arguments" + sha1=$(git rev-parse $1) if test -f "$state_dir"/current-commit then@@ -335,7 +364,7 @@ pick_one_preserving_merges () { f) first_parent=$(expr "$new_parents" : ' \([^ ]*\)') - if [ "$1" != "-n" ] + if test -z "$no_commit" then # detach HEAD to current parent output git checkout $first_parent 2> /dev/null ||@@ -344,7 +373,7 @@ pick_one_preserving_merges () { case "$new_parents" in ' '*' '*) - test "a$1" = a-n && die "Refusing to squash a merge: $sha1" + test -n "$no_commit" && die "Refusing to squash a merge: $sha1" # redo merge author_script_content=$(get_author_ident_from_commit $sha1)@@ -365,7 +394,7 @@ pick_one_preserving_merges () { *) output eval git cherry-pick \ ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \ - "$strategy_args" "$@" || + "$strategy_args" $extra_args $sha1 || die_with_patch $sha1 "Could not pick $sha1" ;; esac
Signature · 3 lines
Michael Haggerty [email protected] http://softwareswirl.blogspot.com/
Hi Michael,
Quoted 57 lines · Michael Haggerty
Quoted 41 lines · Fabian Ruch
pick_oneandpick_one_preserving_mergesare wrappers aroundcherry-pickinrebase --interactive. They take the hash of a commit and build acherry-pickcommand line that- respects the user-supplied merge options
- disables complaints about empty commits
- tries to fast-forward the rebase head unless rebase is forced
- suppresses output unless the user requested higher verbosity
- rewrites merge commits to point to their rebased parents.
pick_oneis used to implement not onlypickbut alsosquash, which amends the previous commit rather than creating a new one. Whenpick_oneis called fromsquash, it receives a second argument-n. This tellspick_oneto apply the changes to the index without committing them. Since the argument is almost directly passed tocherry-pick, we might want to do the same with othercherry-pickoptions. Currently,pick_oneexpects-nto be the first and only argument except for the commit hash.Prepare
pick_onefor additionalcherry-pickoptions by allowing-nto appear anywhere before the commit hash in the argument list. Loop over the argument list and pop each handled item until the commit hash is the only parameter left on the list. If an option is not supported, ignore it and issue a warning on the console. Construct a new arguments listextra_argsof recognized options that shall be passed tocherry-pickon the command line.Signed-off-by Fabian Ruch <[email protected]>--- git-rebase--interactive.sh | 61 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 16 deletions(-)git-rebase--interactive.sh +1
-0@@ -237,8 +237,26 @@ git_sequence_editor () { pick_one () { ff=--ff + extra_args=git-rebase--interactive.sh git_sequence_editor () { · line 250
Quoted 10 lines
git-rebase--interactive.sh +10
-0+ while test $# -gt 0 + do + case "$1" in + -n) + ff= + extra_args="$extra_args -n" + ;; + -*) + warn "pick_one: ignored option -- $1" + ;;This is an internal interface, right? I.e., user input isn't being processed here? If so, then the presence of an unrecognized option is a bug and it is preferable to "die" here rather than "warn".
Quoted 1 line
The same below and in at least one later commit.
Ok.
Quoted 16 lines
Quoted 16 lines
git-rebase--interactive.sh +8
-1+ *) + break + ;; + esac + shift + done + test $# -ne 1 && die "pick_one: wrong number of arguments" + sha1=$1 - case "$1" in -n) sha1=$2; ff= ;; *) sha1=$1 ;; esac case "$force_rebase" in '') ;; ?*) ff= ;; esac output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"@@ -248,24 +266,35 @@ pick_one () { fi
Quoted 16 lines
git-rebase--interactive.sh pick_one () { · line 273
Quoted 8 lines
git-rebase--interactive.sh +2
-2test -d "$rewritten" && - pick_one_preserving_merges "$@" && return + pick_one_preserving_merges $extra_args $sha1 && return output eval git cherry-pick \ ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \ - "$strategy_args" $empty_args $ff "$@" + "$strategy_args" $empty_args $ff $extra_args $sha1 }It might be confusing that
extra_argsis used both inpick_oneand inpick_one_preserving_merges. Since these are not local variables, the call to the latter changes the value of the variable in the former. I don't know if that could be a problem now (canpick_one_preserving_mergesreturn with a nonzero exit code?) but even if not, it is a trap for future developers. I recommend giving the two variables different names.
Please correct me if I missed something. At the moment (786a89d), pick_one_preserving_merges will not and cannot return with a non-zero exit code because it doesn't explicitly return and all possibly failing last statements are guarded with a ... || die "...". Since this can only be established by carefully looking at the code, I will change the reuse of extra_args.
Now that we're at it, what I didn't understand when creating this patch was why the code doesn't explicitly say "one or the other" in the first place:
Quoted 8 lines
if test -d "$rewritten"
then
pick_one_preserving_merges"$@"
elseoutput eval git cherry-pick \ ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \ "$strategy_args" $empty_args $ff "$@"fi
At least that is how I interpreted it. After all, if pick_one_preserving_merges failed to recreate a merge commit, wouldn't it be a bug to record the changes in a single-parent commit?
Johannes, I cc'd you this email since you're the author of f09c9b8 - "Teach rebase -i about --preserve-merges". I hope you don't resent me digging out this seven-year-old patch.
Fabian
Quoted 64 lines
Quoted 64 lines
git-rebase--interactive.sh +24
-13pick_one_preserving_merges () { fast_forward=t - case "$1" in - -n) - fast_forward=f - sha1=$2 - ;; - *) - sha1=$1 - ;; - esac - sha1=$(git rev-parse $sha1) + no_commit= + extra_args= + while test $# -gt 0 + do + case "$1" in + -n) + fast_forward=f + extra_args="$extra_args -n" + no_commit=y + ;; + -*) + warn "pick_one_preserving_merges: ignored option -- $1" + ;; + *) + break + ;; + esac + shift + done + test $# -ne 1 && die "pick_one_preserving_merges: wrong number of arguments" + sha1=$(git rev-parse $1) if test -f "$state_dir"/current-commit then@@ -335,7 +364,7 @@ pick_one_preserving_merges () { f) first_parent=$(expr "$new_parents" : ' \([^ ]*\)') - if [ "$1" != "-n" ] + if test -z "$no_commit" then # detach HEAD to current parent output git checkout $first_parent 2> /dev/null ||@@ -344,7 +373,7 @@ pick_one_preserving_merges () { case "$new_parents" in ' '*' '*) - test "a$1" = a-n && die "Refusing to squash a merge: $sha1" + test -n "$no_commit" && die "Refusing to squash a merge: $sha1" # redo merge author_script_content=$(get_author_ident_from_commit $sha1)@@ -365,7 +394,7 @@ pick_one_preserving_merges () { *) output eval git cherry-pick \ ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \ - "$strategy_args" "$@" || + "$strategy_args" $extra_args $sha1 || die_with_patch $sha1 "Could not pick $sha1" ;; esac
Hi,
Quoted 63 lines · Fabian Ruch
Quoted 45 lines · Michael Haggerty
Quoted 44 lines · Fabian Ruch
pick_oneandpick_one_preserving_mergesare wrappers aroundcherry-pickinrebase --interactive. They take the hash of a commit and build acherry-pickcommand line that- respects the user-supplied merge options
- disables complaints about empty commits
- tries to fast-forward the rebase head unless rebase is forced
- suppresses output unless the user requested higher verbosity
- rewrites merge commits to point to their rebased parents.
pick_oneis used to implement not onlypickbut alsosquash, which amends the previous commit rather than creating a new one. Whenpick_oneis called fromsquash, it receives a second argument-n. This tellspick_oneto apply the changes to the index without committing them. Since the argument is almost directly passed tocherry-pick, we might want to do the same with othercherry-pickoptions. Currently,pick_oneexpects-nto be the first and only argument except for the commit hash.Prepare
pick_onefor additionalcherry-pickoptions by allowing-nto appear anywhere before the commit hash in the argument list. Loop over the argument list and pop each handled item until the commit hash is the only parameter left on the list. If an option is not supported, ignore it and issue a warning on the console. Construct a new arguments listextra_argsof recognized options that shall be passed tocherry-pickon the command line.[...]
git-rebase--interactive.sh +8
-1+ *) + break + ;; + esac + shift + done + test $# -ne 1 && die "pick_one: wrong number of arguments" + sha1=$1 - case "$1" in -n) sha1=$2; ff= ;; *) sha1=$1 ;; esac case "$force_rebase" in '') ;; ?*) ff= ;; esac output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"@@ -248,24 +266,35 @@ pick_one () { fiQuoted 16 lines
git-rebase--interactive.sh pick_one () { · line 273
Quoted 8 lines
git-rebase--interactive.sh +2
-2test -d "$rewritten" && - pick_one_preserving_merges "$@" && return + pick_one_preserving_merges $extra_args $sha1 && return output eval git cherry-pick \ ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \ - "$strategy_args" $empty_args $ff "$@" + "$strategy_args" $empty_args $ff $extra_args $sha1 }It might be confusing that
extra_argsis used both inpick_oneand inpick_one_preserving_merges. Since these are not local variables, the call to the latter changes the value of the variable in the former. I don't know if that could be a problem now (canpick_one_preserving_mergesreturn with a nonzero exit code?) but even if not, it is a trap for future developers. I recommend giving the two variables different names.
Quoted 6 lines
Please correct me if I missed something. At the moment (786a89d),
pick_one_preserving_mergeswill not and cannot return with a non-zero exit code because it doesn't explicitly return and all possibly failing last statements are guarded with a ... || die "...". Since this can only be established by carefully looking at the code, I will change the reuse ofextra_args.
I agree that this is a good change, but I would like to point out that it would be a good change even if it pick_one_preserving_merged could return with a non-zero exit code, even if it could be established easier that it cannot return with a non-zero exit code: it is plainly confusing. (My fault, of course.)
Quoted 13 lines
Now that we're at it, what I didn't understand when creating this patch was why the code doesn't explicitly say "one or the other" in the first place:
Quoted 8 lines
if test -d "$rewritten"
then
pick_one_preserving_merges"$@"
elseoutput eval git cherry-pick \ ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \ "$strategy_args" $empty_args $ff "$@"fi
Quoted 7 lines
At least that is how I interpreted it. After all, if
pick_one_preserving_mergesfailed to recreate a merge commit, wouldn't it be a bug to record the changes in a single-parent commit?Johannes, I cc'd you this email since you're the author of f09c9b8 - "Teach rebase -i about --preserve-merges". I hope you don't resent me digging out this seven-year-old patch.
I do not resent you. Nor do I resent that the seven-year-old change is coming to light again. But I have to admit that my recollection as to the rationale for the way the code was written is very faint indeed.
Also, my commit messages were substantially inferior back then, so I guess that the commit message of f09c9b8 does not quite help to jog my memory.
Looking at that revision of git-rebase--interactive.sh, however, I see three call sites of pick_one, and I would suspect that I wanted to maintain DRY code. Having said that, today I would replace
@@ -68,6 +71,8 @@ die_abort () {
pick_one () {
case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
+ test -d "$REWRITTEN" &&
+ pick_one_preserving_merges "$@" && return
parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null)
current_sha1=$(git rev-parse --verify HEAD)
if [ $current_sha1 = $parent_sha1 ]; thenby
@@ -68,6 +71,8 @@ die_abort () {
pick_one () {
case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
+ if test -d "$REWRITTEN"
+ then
+ pick_one_preserving_merges "$@"
+ return
+ fi
parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null)
current_sha1=$(git rev-parse --verify HEAD)
if [ $current_sha1 = $parent_sha1 ]; theninstead.
Hope this clarifies things!
Dscho
Quoted 18 lines · Michael Haggerty
git-rebase--interactive.sh git_sequence_editor () { · line 250
Quoted 13 lines
pick_one() {git-rebase--interactive.sh +11
-0ff=--ff + extra_args= + while test $# -gt 0 + do + case "$1" in + -n) + ff= + extra_args="$extra_args -n" + ;; + -*) + warn "pick_one: ignored option -- $1" + ;;This is an internal interface, right? I.e., user input isn't being processed here? If so, then the presence of an unrecognized option is a bug and it is preferable to "die" here rather than "warn".
Quoted 1 line
The same below and in at least one later commit.
And if this is purely an internal interface, then I really do not see the point of allowing -n to be anywhere other than the front. If we are planning to accept other random options to cherry-pick in later steps, but we are not yet doing so at this step, then I do not thin we want to have any loop like this before we actually start accepting and passing them to the underlying cherry-pick.
Furthermore, if the "-n" is currently used as an internal signal from the caller to pick_one() that it is executing the end-user supplied "squash" in the insn sheet, it may be a good idea to change that "-n" to something that is *NOT* a valid option to cherry-pick at this step, before we start accepting user-supplied options and relaying them to underlying cherry-pick.
One way to do so cleanly may be to _always_ add the type of pick as the first parameter to pick_one, i.e. either "pick" or "squash", and do:
pick_one () {
...
n_arg=
case "$1" in
pick) ;;
squash) n_arg=-n ;;
*) die "BUG: pick_one $1???" ;;
esac
shift
sha1=$1
...
output eval git cherry-pick $n_arg \
...
}Also I suspect that you would need to be careful *not* to allow "-n" to be given as part of the "random user-specified options" and pass that to cherry-pick in the later steps of your series [*1*], and for that you may need a loop that inspects the arguments like you had in this patch.
[Footnote]
*1* The existing callers of "pick_one -n" very well know and expect
that the step will only update the working tree and the index and it is the callers' responsibility to create a commit out of that state (either by amending or committing); similarly the existing callers of "pick_one" without "-n" very well know and expect that the step will make a commit unless there is a problem. I do not think you would consider it such a "problem to replay the change in the named commit" for the end user's insn sheet to pass a "-n".
Hi Junio,
Quoted 19 lines · Junio C Hamano
Quoted 18 lines · Michael Haggerty
git-rebase--interactive.sh git_sequence_editor () { · line 250
Quoted 13 lines
pick_one() {git-rebase--interactive.sh +11
-0ff=--ff + extra_args= + while test $# -gt 0 + do + case "$1" in + -n) + ff= + extra_args="$extra_args -n" + ;; + -*) + warn "pick_one: ignored option -- $1" + ;;This is an internal interface, right? I.e., user input isn't being processed here? If so, then the presence of an unrecognized option is a bug and it is preferable to "die" here rather than "warn".
Quoted 8 lines
Quoted 1 line
The same below and in at least one later commit.
And if this is purely an internal interface, then I really do not see the point of allowing -n to be anywhere other than the front. If we are planning to accept other random options to cherry-pick in later steps, but we are not yet doing so at this step, then I do not thin we want to have any loop like this before we actually start accepting and passing them to the underlying cherry-pick.
Ok, until we require pick_one to accept options apart from -n, this patch is postponed, for the presence of a single option is checked easiest without the loop. It might be the case that rewriting replayed commits in do_pick is the better alternative anyway and that it will never be required to relay user-specified options beyond do_pick.
Quoted 26 lines
Furthermore, if the "-n" is currently used as an internal signal from the caller to
pick_one()that it is executing the end-user supplied "squash" in the insn sheet, it may be a good idea to change that "-n" to something that is *NOT* a valid option to cherry-pick at this step, before we start accepting user-supplied options and relaying them to underlying cherry-pick.One way to do so cleanly may be to _always_ add the type of pick as the first parameter to
pick_one, i.e. either "pick" or "squash", and do:pick_one () { ... n_arg= case "$1" in pick) ;; squash) n_arg=-n ;; *) die "BUG: pick_one $1???" ;; esac shift sha1=$1 ... output eval git cherry-pick $n_arg \ ... }
Quoted 5 lines
Also I suspect that you would need to be careful *not* to allow "-n" to be given as part of the "random user-specified options" and pass that to cherry-pick in the later steps of your series [*1*], and for that you may need a loop that inspects the arguments like you had in this patch.
I really like the idea of being explicit about how pick_one shall replay the named commit and not using the cherry-pick option name for the squash case. However, pick_one will never receive random user-specified options. do_pick is the interface function which handles the pick arguments. If any user-specified options are relayed to pick_one and cherry-pick, they will be validated by do_pick first (using a loop like above).
Fabian
Quoted 11 lines
[Footnote]
*1* The existing callers of "
pick_one-n" very well know and expect
that the step will only update the working tree and the index and it is the callers' responsibility to create a commit out of that state (either by amending or committing); similarly the existing callers of "pick_one" without "-n" very well know and expect that the step will make a commit unless there is a problem. I do not think you would consider it such a "problem to replay the change in the named commit" for the end user's insn sheet to pass a "-n".
RFC PATCH 2/7 rebase -i: Teach do_pick the option --edit
In reply to a message not in the archiveThe to-do list command reword replays a commit like pick but lets the user also edit the commit's log message. If one thinks of pick entries as scheduled cherry-pick command lines, then reword becomes an alias for the command line cherry-pick --edit. The porcelain rebase--interactive defines a function do_pick for processing the pick entries on to-do lists. Teach do_pick to handle the option --edit and reimplement reword in terms of do_pick --edit. Refer to pick_one for the way options are parsed.
do_pick ought to act as a wrapper around cherry-pick. Unfortunately, it cannot just forward --edit to the cherry-pick command line. The assembled command line is executed within a command substitution for controlling the verbosity of rebase--interactive. Passing --edit would either hang the terminal or clutter the substituted command output with control sequences. Execute the reword code from do_next instead if the option --edit is specified. Adjust the fragment in two regards. Firstly, discard the additional message which is printed if an error occurs because
Aborting commit due to empty commit message. (Duplicate Signed-off-by lines.) Could not amend commit after successfully picking 1234567... Some change
is more readable than and contains (almost) the same information as
Aborting commit due to empty commit message. (Duplicate Signed-off-by lines.) Could not amend commit after successfully picking 1234567... Some change This is most likely due to an empty commit message, or the pre-commit hook failed. If the pre-commit hook failed, you may need to resolve the issue before you are able to reword the commit.
(It is true that a hook might not output any diagnosis but the same problem arises when using git-commit directly. git-rebase at least prints a generic message saying that it failed to commit.) Secondly, sneak in additional git-commit arguments:
- --allow-empty is missing: rebase--interactive suddenly fails if an empty commit is picked using reword instead of pick. The whether a commit is empty or not is not changed by an altered log message, so do as pick does. Add test.
- -n: Hide the fact that we are committing several times by not executing the pre-commit hook. Caveat: The altered log message is not verified because -n also skips the commit-msg hook. Either the log message verification must be included in the post-rewrite hook or git-commit must support more fine-grained control over which hooks are executed.
- -q: Hide the fact that we are committing several times by not printing the commit summary.
--- git-rebase--interactive.sh | 52 ++++++++++++++++++++++++++++++++++++------- t/t3404-rebase-interactive.sh | 8 +++++++ 2 files changed, 52 insertions(+), 8 deletions(-)
git-rebase--interactive.sh +44 -8
@@ -490,7 +490,42 @@ record_in_rewritten() { esac } +# Apply the changes introduced by the given commit to the current head. +# +# do_pick [--edit] <commit> <title> +# +# Wrapper around git-cherry-pick. +# +# <title> +# The commit message title of <commit>. Used for information +# purposes only. +# +# <commit> +# The commit to cherry-pick. +# +# -e, --edit +# After picking <commit>, open an editor and let the user edit the +# commit message. The editor contents becomes the commit message of +# the new head. do_pick () { + edit= + while test $# -gt 0 + do + case "$1" in + -e|--edit) + edit=y + ;; + -*) + warn "do_pick: ignored option -- $1" + ;; + *) + break + ;; + esac + shift + done + test $# -ne 2 && die "do_pick: wrong number of arguments" + if test "$(git rev-parse HEAD)" = "$squash_onto" then # Set the correct commit message and author info on the
@@ -512,6 +547,14 @@ do_pick () { pick_one $1 || die_with_patch $1 "Could not apply $1... $2" fi + + if test -n "$edit" + then + git commit --allow-empty --amend --no-post-rewrite -n -q ${gpg_sign_opt:+"$gpg_sign_opt"} || { + warn "Could not amend commit after successfully picking $1... $2" + exit_with_patch $1 1 + } + fi } do_next () {
@@ -532,14 +575,7 @@ do_next () { comment_for_reflog reword mark_action_done - do_pick $sha1 "$rest" - git commit --amend --no-post-rewrite ${gpg_sign_opt:+"$gpg_sign_opt"} || { - warn "Could not amend commit after successfully picking $sha1... $rest" - warn "This is most likely due to an empty commit message, or the pre-commit hook" - warn "failed. If the pre-commit hook failed, you may need to resolve the issue before" - warn "you are able to reword the commit." - exit_with_patch $sha1 1 - } + do_pick --edit $sha1 "$rest" record_in_rewritten $sha1 ;; edit|e)
t/t3404-rebase-interactive.sh +8 -0
@@ -75,6 +75,14 @@ test_expect_success 'rebase --keep-empty' ' test_line_count = 6 actual ' +test_expect_success 'rebase --keep-empty' ' + git checkout emptybranch && + set_fake_editor && + FAKE_LINES="1 reword 2" git rebase --keep-empty -i HEAD~2 && + git log --oneline >actual && + test_line_count = 6 actual +' + test_expect_success 'rebase -i with the exec command' ' git checkout master && (
Signature · 1 line
2.0.0
Re: [RFC PATCH 2/7] rebase -i: Teach do_pick the option --edit
Quoted 47 lines · Fabian Ruch
The to-do list command
rewordreplays a commit likepickbut lets the user also edit the commit's log message. If one thinks ofpickentries as scheduledcherry-pickcommand lines, thenrewordbecomes an alias for the command linecherry-pick --edit. The porcelainrebase--interactivedefines a functiondo_pickfor processing thepickentries on to-do lists. Teachdo_pickto handle the option--editand reimplementrewordin terms ofdo_pick --edit. Refer topick_onefor the way options are parsed.
do_pickought to act as a wrapper aroundcherry-pick. Unfortunately, it cannot just forward--editto thecherry-pickcommand line. The assembled command line is executed within a command substitution for controlling the verbosity ofrebase--interactive. Passing--editwould either hang the terminal or clutter the substituted command output with control sequences. Execute therewordcode fromdo_nextinstead if the option--editis specified. Adjust the fragment in two regards. Firstly, discard the additional message which is printed if an error occurs becauseAborting commit due to empty commit message. (Duplicate Signed-off-by lines.) Could not amend commit after successfully picking 1234567... Some change
is more readable than and contains (almost) the same information as
Aborting commit due to empty commit message. (Duplicate Signed-off-by lines.) Could not amend commit after successfully picking 1234567... Some change This is most likely due to an empty commit message, or the pre-commit hook failed. If the pre-commit hook failed, you may need to resolve the issue before you are able to reword the commit.
(It is true that a hook might not output any diagnosis but the same problem arises when using git-commit directly. git-rebase at least prints a generic message saying that it failed to commit.) Secondly, sneak in additional git-commit arguments:
-
--allow-emptyis missing:rebase--interactivesuddenly fails if an empty commit is picked usingrewordinstead ofpick. The whether a commit is empty or not is not changed by an altered log message, so do aspickdoes. Add test.-
-n: Hide the fact that we are committing several times by not executing the pre-commit hook. Caveat: The altered log message is not verified because-nalso skips the commit-msg hook. Either the log message verification must be included in the post-rewrite hook or git-commit must support more fine-grained control over which hooks are executed.
Quoted 2 lines
-
-q: Hide the fact that we are committing several times by not printing the commit summary.
It is preferable that each commit makes one logical change (though it must always be a self-contained change; i.e., the code should never be broken at the end of a commit). It would be clearer if you would split this commit into one refactoring commit (moving the handling of --edit to do_pick) plus one commit for each "git commit" option change and error message change. That way,
* Each commit (and log message) becomes simpler, making it easier to review.
* The changes can be discussed separately.
* If there is an error, "git bisect" can help determine which of the changes is at fault.
Quoted 16 lines
Signed-off-by Fabian Ruch <[email protected]>--- git-rebase--interactive.sh | 52 ++++++++++++++++++++++++++++++++++++------- t/t3404-rebase-interactive.sh | 8 +++++++ 2 files changed, 52 insertions(+), 8 deletions(-)git-rebase--interactive.sh +2
-0@@ -490,7 +490,42 @@ record_in_rewritten() { esac } +# Apply the changes introduced by the given commit to the current head. +#
git-rebase--interactive.sh record_in_rewritten() { · line 504
Quoted 10 lines
git-rebase--interactive.sh +10
-0+# do_pick [--edit] <commit> <title> +# +# Wrapper around git-cherry-pick. +# +# <title> +# The commit message title of <commit>. Used for information +# purposes only. +# +# <commit> +# The commit to cherry-pick.
Unless there is a reason to do otherwise, please order the documentation to match the order that the do_pick arguments appear.
Quoted 78 lines
git-rebase--interactive.sh +32
-8+# +# -e, --edit +# After picking <commit>, open an editor and let the user edit the +# commit message. The editor contents becomes the commit message of +# the new head. do_pick () { + edit= + while test $# -gt 0 + do + case "$1" in + -e|--edit) + edit=y + ;; + -*) + warn "do_pick: ignored option -- $1" + ;; + *) + break + ;; + esac + shift + done + test $# -ne 2 && die "do_pick: wrong number of arguments" + if test "$(git rev-parse HEAD)" = "$squash_onto" then # Set the correct commit message and author info on the@@ -512,6 +547,14 @@ do_pick () { pick_one $1 || die_with_patch $1 "Could not apply $1... $2" fi + + if test -n "$edit" + then + git commit --allow-empty --amend --no-post-rewrite -n -q ${gpg_sign_opt:+"$gpg_sign_opt"} || { + warn "Could not amend commit after successfully picking $1... $2" + exit_with_patch $1 1 + } + fi } do_next () {@@ -532,14 +575,7 @@ do_next () { comment_for_reflog reword mark_action_done - do_pick $sha1 "$rest" - git commit --amend --no-post-rewrite ${gpg_sign_opt:+"$gpg_sign_opt"} || { - warn "Could not amend commit after successfully picking $sha1... $rest" - warn "This is most likely due to an empty commit message, or the pre-commit hook" - warn "failed. If the pre-commit hook failed, you may need to resolve the issue before" - warn "you are able to reword the commit." - exit_with_patch $sha1 1 - } + do_pick --edit $sha1 "$rest" record_in_rewritten $sha1 ;; edit|e)t/t3404-rebase-interactive.sh +8
-0@@ -75,6 +75,14 @@ test_expect_success 'rebase --keep-empty' ' test_line_count = 6 actual ' +test_expect_success 'rebase --keep-empty' ' + git checkout emptybranch && + set_fake_editor && + FAKE_LINES="1 reword 2" git rebase --keep-empty -i HEAD~2 && + git log --oneline >actual && + test_line_count = 6 actual +' + test_expect_success 'rebase -i with the exec command' ' git checkout master && (
Signature · 3 lines
Michael Haggerty [email protected] http://softwareswirl.blogspot.com/
Re: [RFC PATCH 2/7] rebase -i: Teach do_pick the option --edit
Hi Michael,
Quoted 58 lines · Michael Haggerty
Quoted 47 lines · Fabian Ruch
The to-do list command
rewordreplays a commit likepickbut lets the user also edit the commit's log message. If one thinks ofpickentries as scheduledcherry-pickcommand lines, thenrewordbecomes an alias for the command linecherry-pick --edit. The porcelainrebase--interactivedefines a functiondo_pickfor processing thepickentries on to-do lists. Teachdo_pickto handle the option--editand reimplementrewordin terms ofdo_pick --edit. Refer topick_onefor the way options are parsed.
do_pickought to act as a wrapper aroundcherry-pick. Unfortunately, it cannot just forward--editto thecherry-pickcommand line. The assembled command line is executed within a command substitution for controlling the verbosity ofrebase--interactive. Passing--editwould either hang the terminal or clutter the substituted command output with control sequences. Execute therewordcode fromdo_nextinstead if the option--editis specified. Adjust the fragment in two regards. Firstly, discard the additional message which is printed if an error occurs becauseAborting commit due to empty commit message. (Duplicate Signed-off-by lines.) Could not amend commit after successfully picking 1234567... Some change
is more readable than and contains (almost) the same information as
Aborting commit due to empty commit message. (Duplicate Signed-off-by lines.) Could not amend commit after successfully picking 1234567... Some change This is most likely due to an empty commit message, or the pre-commit hook failed. If the pre-commit hook failed, you may need to resolve the issue before you are able to reword the commit.
(It is true that a hook might not output any diagnosis but the same problem arises when using git-commit directly. git-rebase at least prints a generic message saying that it failed to commit.) Secondly, sneak in additional git-commit arguments:
-
--allow-emptyis missing:rebase--interactivesuddenly fails if an empty commit is picked usingrewordinstead ofpick. The whether a commit is empty or not is not changed by an altered log message, so do aspickdoes. Add test.-
-n: Hide the fact that we are committing several times by not executing the pre-commit hook. Caveat: The altered log message is not verified because-nalso skips the commit-msg hook. Either the log message verification must be included in the post-rewrite hook or git-commit must support more fine-grained control over which hooks are executed.Quoted 2 lines
-
-q: Hide the fact that we are committing several times by not printing the commit summary.It is preferable that each commit makes one logical change (though it must always be a self-contained change; i.e., the code should never be broken at the end of a commit). It would be clearer if you would split this commit into one refactoring commit (moving the handling of --edit to
do_pick) plus one commit for each "git commit" option change and error message change. That way,
Quoted 5 lines
* Each commit (and log message) becomes simpler, making it easier to review.
* The changes can be discussed separately.
* If there is an error, "git bisect" can help determine which of the changes is at fault.
Hmph, I neglected that totally here. I'm sorry. If it's all right, I will reply with the five separate commits (refactoring, error message, --allow-empty, -n, -q) to this email. The whole patch series is still RFC and the combination of the five will be exactly this one, so that shouldn't confuse or burden anyone.
Quoted 14 lines
Quoted 14 lines
Signed-off-by Fabian Ruch <[email protected]>--- git-rebase--interactive.sh | 52 ++++++++++++++++++++++++++++++++++++------- t/t3404-rebase-interactive.sh | 8 +++++++ 2 files changed, 52 insertions(+), 8 deletions(-)git-rebase--interactive.sh +0
-0@@ -490,7 +490,42 @@ record_in_rewritten() { esac }
Quoted 15 lines
git-rebase--interactive.sh record_in_rewritten() { · line 504
Quoted 12 lines
git-rebase--interactive.sh +12
-0+# Apply the changes introduced by the given commit to the current head. +# +# do_pick [--edit] <commit> <title> +# +# Wrapper around git-cherry-pick. +# +# <title> +# The commit message title of <commit>. Used for information +# purposes only. +# +# <commit> +# The commit to cherry-pick.Unless there is a reason to do otherwise, please order the documentation to match the order that the
do_pickarguments appear.
Ok.
The reason was to document the non-option arguments first and I ended up documenting the arguments in reverse order to simply not abandon all order. Having a look at several man-pages of git commands (cherry-pick, commit, am, rebase), I wasn't able to extract a common pattern of order of argument documentation.
Fabian
Quoted 77 lines
Quoted 77 lines
git-rebase--interactive.sh +32
-8+# +# -e, --edit +# After picking <commit>, open an editor and let the user edit the +# commit message. The editor contents becomes the commit message of +# the new head. do_pick () { + edit= + while test $# -gt 0 + do + case "$1" in + -e|--edit) + edit=y + ;; + -*) + warn "do_pick: ignored option -- $1" + ;; + *) + break + ;; + esac + shift + done + test $# -ne 2 && die "do_pick: wrong number of arguments" + if test "$(git rev-parse HEAD)" = "$squash_onto" then # Set the correct commit message and author info on the@@ -512,6 +547,14 @@ do_pick () { pick_one $1 || die_with_patch $1 "Could not apply $1... $2" fi + + if test -n "$edit" + then + git commit --allow-empty --amend --no-post-rewrite -n -q ${gpg_sign_opt:+"$gpg_sign_opt"} || { + warn "Could not amend commit after successfully picking $1... $2" + exit_with_patch $1 1 + } + fi } do_next () {@@ -532,14 +575,7 @@ do_next () { comment_for_reflog reword mark_action_done - do_pick $sha1 "$rest" - git commit --amend --no-post-rewrite ${gpg_sign_opt:+"$gpg_sign_opt"} || { - warn "Could not amend commit after successfully picking $sha1... $rest" - warn "This is most likely due to an empty commit message, or the pre-commit hook" - warn "failed. If the pre-commit hook failed, you may need to resolve the issue before" - warn "you are able to reword the commit." - exit_with_patch $sha1 1 - } + do_pick --edit $sha1 "$rest" record_in_rewritten $sha1 ;; edit|e)t/t3404-rebase-interactive.sh +8
-0@@ -75,6 +75,14 @@ test_expect_success 'rebase --keep-empty' ' test_line_count = 6 actual ' +test_expect_success 'rebase --keep-empty' ' + git checkout emptybranch && + set_fake_editor && + FAKE_LINES="1 reword 2" git rebase --keep-empty -i HEAD~2 && + git log --oneline >actual && + test_line_count = 6 actual +' + test_expect_success 'rebase -i with the exec command' ' git checkout master && (
RFC PATCH 3/7 rebase -i: Stop on root commits with empty log messages
In reply to a message not in the archiveWhen rebase is executed with --root but no --onto is specified, rebase creates a sentinel commit which is replaced with the root commit in three steps. This combination of options results never in a fast-forward.
1. The sentinel commit is forced into the authorship of the root commit.
2. The changes introduced by the root commit are applied to the index but not committed. If this step fails for whatever reason, all commit information will be there and the user can safely run git-commit --amend after resolving the problems.
3. The new root commit is created by squashing the changes into the sentinel commit which already carries the authorship of the cherry-picked root commit.
The command line used to create the commit in the third step specifies effectless and erroneous options. Remove those.
- --allow-empty-message is erroneous: If the root's commit message is empty, the rebase shall fail like for any other commit that is on the to-do list and has an empty commit message.
Fix the bug that git-rebase does not fail when the initial commit has an empty log message but is replayed using --root is specified. Add test.
- -C is effectless: The commit being amended, which is the sentinel commit, already carries the authorship and log message of the cherry-picked root commit. The committer email and commit date fields are reset either way.
After all, if step two fails, rebase --continue won't include these flags in the git-commit command line either.
--- git-rebase--interactive.sh | 4 ++-- t/t3412-rebase-root.sh | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-)
git-rebase--interactive.sh +2 -2
@@ -539,8 +539,8 @@ do_pick () { git commit --allow-empty --allow-empty-message --amend \ --no-post-rewrite -n -q -C $1 && pick_one -n $1 && - git commit --allow-empty --allow-empty-message \ - --amend --no-post-rewrite -n -q -C $1 \ + git commit --allow-empty --amend \ + --no-post-rewrite -n -q \ ${gpg_sign_opt:+"$gpg_sign_opt"} || die_with_patch $1 "Could not apply $1... $2" else
t/t3412-rebase-root.sh +39 -0
@@ -278,4 +278,43 @@ test_expect_success 'rebase -i -p --root with conflict (second part)' ' test_cmp expect-conflict-p out ' +test_expect_success 'stop rebase --root on empty root log message' ' + # create a root commit with a non-empty tree so that rebase does + # not fail because of an empty commit, and an empty log message + echo root-commit >file && + git add file && + tree=$(git write-tree) && + root=$(git commit-tree $tree </dev/null) && + git checkout -b no-message-root-commit $root && + # do not ff because otherwise neither the patch nor the message + # are looked at and checked for emptiness + test_must_fail env EDITOR=true git rebase -i --force-rebase --root && + echo root-commit >file.expected && + test_cmp file file.expected && + git rebase --abort +' + +test_expect_success 'stop rebase --root on empty child log message' ' + # create a root commit with a non-empty tree and provide a log + # message so that rebase does not fail until the root commit is + # successfully replayed + echo root-commit >file && + git add file && + tree=$(git write-tree) && + root=$(git commit-tree $tree -m root-commit) && + git checkout -b no-message-child-commit $root && + # create a child commit with a non-empty patch so that rebase + # does not fail because of an empty commit, but an empty log + # message + echo child-commit >file && + git add file && + git commit --allow-empty-message --no-edit && + # do not ff because otherwise neither the patch nor the message + # are looked at and checked for emptiness + test_must_fail env EDITOR=true git rebase -i --force-rebase --root && + echo child-commit >file.expected && + test_cmp file file.expected && + git rebase --abort +' + test_done
Signature · 1 line
2.0.0
Re: [RFC PATCH 3/7] rebase -i: Stop on root commits with empty log messages
Quoted 68 lines · Fabian Ruch
When
rebaseis executed with--rootbut no--ontois specified,rebasecreates a sentinel commit which is replaced with the root commit in three steps. This combination of options results never in a fast-forward.1. The sentinel commit is forced into the authorship of the root commit.
2. The changes introduced by the root commit are applied to the index but not committed. If this step fails for whatever reason, all commit information will be there and the user can safely run
git-commit --amendafter resolving the problems.3. The new root commit is created by squashing the changes into the sentinel commit which already carries the authorship of the cherry-picked root commit.
The command line used to create the commit in the third step specifies effectless and erroneous options. Remove those.
-
--allow-empty-messageis erroneous: If the root's commit message is empty, the rebase shall fail like for any other commit that is on the to-do list and has an empty commit message.Fix the bug that git-rebase does not fail when the initial commit has an empty log message but is replayed using
--rootis specified. Add test.-
-Cis effectless: The commit being amended, which is the sentinel commit, already carries the authorship and log message of the cherry-picked root commit. The committer email and commit date fields are reset either way.After all, if step two fails,
rebase --continuewon't include these flags in the git-commit command line either.Signed-off-by Fabian Ruch <[email protected]>--- git-rebase--interactive.sh | 4 ++-- t/t3412-rebase-root.sh | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-)git-rebase--interactive.sh +2
-2@@ -539,8 +539,8 @@ do_pick () { git commit --allow-empty --allow-empty-message --amend \ --no-post-rewrite -n -q -C $1 && pick_one -n $1 && - git commit --allow-empty --allow-empty-message \ - --amend --no-post-rewrite -n -q -C $1 \ + git commit --allow-empty --amend \ + --no-post-rewrite -n -q \ ${gpg_sign_opt:+"$gpg_sign_opt"} || die_with_patch $1 "Could not apply $1... $2" elset/t3412-rebase-root.sh +3
-0@@ -278,4 +278,43 @@ test_expect_success 'rebase -i -p --root with conflict (second part)' ' test_cmp expect-conflict-p out ' +test_expect_success 'stop rebase --root on empty root log message' ' + # create a root commit with a non-empty tree so that rebase does + # not fail because of an empty commit, and an empty log message
t/t3412-rebase-root.sh test_expect_success 'rebase -i -p --root with conflict (second part)' ' · line 293
Quoted 10 lines
t/t3412-rebase-root.sh +10
-0+ echo root-commit >file && + git add file && + tree=$(git write-tree) && + root=$(git commit-tree $tree </dev/null) && + git checkout -b no-message-root-commit $root && + # do not ff because otherwise neither the patch nor the message + # are looked at and checked for emptiness + test_must_fail env EDITOR=true git rebase -i --force-rebase --root && + echo root-commit >file.expected && + test_cmp file file.expected &&
It is customary, and provides nicer diagnostic output upon failure, to have the "expected" file mentioned first:
test_cmp file.expected file &&
t/t3412-rebase-root.sh test_expect_success 'rebase -i -p --root with conflict (second part)' ' · line 294
Quoted 1 line
t/t3412-rebase-root.sh +1
-0+ git rebase --abort
Do you want to place this under control of test_when_finished somewhere above the "git rebase" invocation to ensure cleanup if something fails before this point?
Quoted 13 lines
t/t3412-rebase-root.sh +13
-0+' + +test_expect_success 'stop rebase --root on empty child log message' ' + # create a root commit with a non-empty tree and provide a log + # message so that rebase does not fail until the root commit is + # successfully replayed + echo root-commit >file && + git add file && + tree=$(git write-tree) && + root=$(git commit-tree $tree -m root-commit) && + git checkout -b no-message-child-commit $root && + # create a child commit with a non-empty patch so that rebase + # does not fail because of an empty commit, but an empty log
t/t3412-rebase-root.sh test_expect_success 'rebase -i -p --root with conflict (second part)' ' · line 317
Quoted 10 lines
t/t3412-rebase-root.sh +10
-0+ # message + echo child-commit >file && + git add file && + git commit --allow-empty-message --no-edit && + # do not ff because otherwise neither the patch nor the message + # are looked at and checked for emptiness + test_must_fail env EDITOR=true git rebase -i --force-rebase --root && + echo child-commit >file.expected && + test_cmp file file.expected && + git rebase --abort
Same two comments as for previous test.
Quoted 5 lines
t/t3412-rebase-root.sh +2
-1+' + test_done --2.0.0
Re: [RFC PATCH 3/7] rebase -i: Stop on root commits with empty log messages
Hi Eric,
Quoted 83 lines · Eric Sunshine
Quoted 68 lines · Fabian Ruch
When
rebaseis executed with--rootbut no--ontois specified,rebasecreates a sentinel commit which is replaced with the root commit in three steps. This combination of options results never in a fast-forward.1. The sentinel commit is forced into the authorship of the root commit.
2. The changes introduced by the root commit are applied to the index but not committed. If this step fails for whatever reason, all commit information will be there and the user can safely run
git-commit --amendafter resolving the problems.3. The new root commit is created by squashing the changes into the sentinel commit which already carries the authorship of the cherry-picked root commit.
The command line used to create the commit in the third step specifies effectless and erroneous options. Remove those.
-
--allow-empty-messageis erroneous: If the root's commit message is empty, the rebase shall fail like for any other commit that is on the to-do list and has an empty commit message.Fix the bug that git-rebase does not fail when the initial commit has an empty log message but is replayed using
--rootis specified. Add test.-
-Cis effectless: The commit being amended, which is the sentinel commit, already carries the authorship and log message of the cherry-picked root commit. The committer email and commit date fields are reset either way.After all, if step two fails,
rebase --continuewon't include these flags in the git-commit command line either.Signed-off-by Fabian Ruch <[email protected]>--- git-rebase--interactive.sh | 4 ++-- t/t3412-rebase-root.sh | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-)git-rebase--interactive.sh +2
-2@@ -539,8 +539,8 @@ do_pick () { git commit --allow-empty --allow-empty-message --amend \ --no-post-rewrite -n -q -C $1 && pick_one -n $1 && - git commit --allow-empty --allow-empty-message \ - --amend --no-post-rewrite -n -q -C $1 \ + git commit --allow-empty --amend \ + --no-post-rewrite -n -q \ ${gpg_sign_opt:+"$gpg_sign_opt"} || die_with_patch $1 "Could not apply $1... $2" elset/t3412-rebase-root.sh +3
-0@@ -278,4 +278,43 @@ test_expect_success 'rebase -i -p --root with conflict (second part)' ' test_cmp expect-conflict-p out ' +test_expect_success 'stop rebase --root on empty root log message' ' + # create a root commit with a non-empty tree so that rebase does + # not fail because of an empty commit, and an empty log messaget/t3412-rebase-root.sh test_expect_success 'rebase -i -p --root with conflict (second part)' ' · line 293
Quoted 10 lines
t/t3412-rebase-root.sh +10
-0+ echo root-commit >file && + git add file && + tree=$(git write-tree) && + root=$(git commit-tree $tree </dev/null) && + git checkout -b no-message-root-commit $root && + # do not ff because otherwise neither the patch nor the message + # are looked at and checked for emptiness + test_must_fail env EDITOR=true git rebase -i --force-rebase --root && + echo root-commit >file.expected && + test_cmp file file.expected &&It is customary, and provides nicer diagnostic output upon failure, to have the "expected" file mentioned first:
Quoted 1 line
test_cmp file.expected file &&
Ok.
Quoted 5 lines
t/t3412-rebase-root.sh test_expect_success 'rebase -i -p --root with conflict (second part)' ' · line 294
Quoted 1 line
t/t3412-rebase-root.sh +1
-0+ git rebase --abortDo you want to place this under control of
test_when_finishedsomewhere above the "git rebase" invocation to ensure cleanup if something fails before this point?
Thanks a lot for the hint, I will place the test_when_finished directly above the rebase -i command line because it is not relevant before, the exact position shouldn't matter though. I didn't know that test_run_ skips the cleanup code if -i (for immediate mode) is passed to the test driver and the test case fails.
Fabian
Quoted 17 lines
Quoted 17 lines
t/t3412-rebase-root.sh +17
-0+' + +test_expect_success 'stop rebase --root on empty child log message' ' + # create a root commit with a non-empty tree and provide a log + # message so that rebase does not fail until the root commit is + # successfully replayed + echo root-commit >file && + git add file && + tree=$(git write-tree) && + root=$(git commit-tree $tree -m root-commit) && + git checkout -b no-message-child-commit $root && + # create a child commit with a non-empty patch so that rebase + # does not fail because of an empty commit, but an empty log + # message + echo child-commit >file && + git add file && + git commit --allow-empty-message --no-edit &&
Quoted 8 lines
t/t3412-rebase-root.sh test_expect_success 'rebase -i -p --root with conflict (second part)' ' · line 317
Quoted 6 lines
t/t3412-rebase-root.sh +6
-0+ # do not ff because otherwise neither the patch nor the message + # are looked at and checked for emptiness + test_must_fail env EDITOR=true git rebase -i --force-rebase --root && + echo child-commit >file.expected && + test_cmp file file.expected && + git rebase --abortSame two comments as for previous test.
Ok.
Quoted 3 lines
Quoted 3 lines
t/t3412-rebase-root.sh +2
-0+' + test_done
RFC PATCH 4/7 rebase -i: Commit only once when rewriting picks
In reply to a message not in the archiveThe options passed to do_pick determine whether the picked commit will be rewritten or not. If the commit gets rewritten, because the user requested to edit the commit message for instance, let pick_one merely apply the changes introduced by the commit and do not commit the resulting tree yet. If the commit is replayed as is, leave it to pick_one to recreate the commit (possibly by fast-forwarding the head). This makes it easier to combine git-commit options like --edit and --amend in do_pick because git-cherry-pick does not support --amend.
In the case of --edit, do not exit_with_patch but assign rewrite to pick the changes with -n. If the pick conflicts, no commit is created which we would have to amend when continuing the rebase. To complete the pick after the conflicts are resolved the user just resumes with git rebase --continue.
If rebase--interactive is used to rebase a complete branch onto some head, rebase creates a sentinel commit that requires special treatment by do_pick. Do not finalize the pick here either because its commit message can be altered as for any other pick. Since the orphaned root commit gets a temporary parent, it is always rewritten. Safely use the rewrite infrastructure of do_pick to create the final commit.
--- git-rebase--interactive.sh | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-)
git-rebase--interactive.sh +18 -13
@@ -63,7 +63,8 @@ msgnum="$state_dir"/msgnum author_script="$state_dir"/author-script # When an "edit" rebase command is being processed, the SHA1 of the -# commit to be edited is recorded in this file. When "git rebase +# commit to be edited is recorded in this file. The same happens when +# rewriting a commit fails, for instance "reword". When "git rebase # --continue" is executed, if there are any staged changes then they # will be amended to the HEAD commit, but only provided the HEAD # commit is still the commit to be edited. When any other rebase
@@ -508,12 +509,15 @@ record_in_rewritten() { # commit message. The editor contents becomes the commit message of # the new head. do_pick () { - edit= + rewrite= + rewrite_amend= + rewrite_edit= while test $# -gt 0 do case "$1" in -e|--edit) - edit=y + rewrite=y + rewrite_edit=y ;; -*) warn "do_pick: ignored option -- $1"
@@ -528,6 +532,9 @@ do_pick () { if test "$(git rev-parse HEAD)" = "$squash_onto" then + rewrite=y + rewrite_amend=y + git rev-parse --verify HEAD >"$amend" # Set the correct commit message and author info on the # sentinel root before cherry-picking the original changes # without committing (-n). Finally, update the sentinel again
@@ -538,22 +545,20 @@ do_pick () { # rebase --continue. git commit --allow-empty --allow-empty-message --amend \ --no-post-rewrite -n -q -C $1 && - pick_one -n $1 && - git commit --allow-empty --amend \ - --no-post-rewrite -n -q \ - ${gpg_sign_opt:+"$gpg_sign_opt"} || + pick_one -n $1 || die_with_patch $1 "Could not apply $1... $2" else - pick_one $1 || + pick_one ${rewrite:+-n} $1 || die_with_patch $1 "Could not apply $1... $2" fi - if test -n "$edit" + if test -n "$rewrite" then - git commit --allow-empty --amend --no-post-rewrite -n -q ${gpg_sign_opt:+"$gpg_sign_opt"} || { - warn "Could not amend commit after successfully picking $1... $2" - exit_with_patch $1 1 - } + git commit --allow-empty --no-post-rewrite -n -q \ + ${rewrite_amend:+--amend} \ + ${rewrite_edit:+--edit} \ + ${gpg_sign_opt:+"$gpg_sign_opt"} || + die_with_patch $1 "Could not rewrite commit after successfully picking $1... $2" fi }
Signature · 1 line
2.0.0
RFC PATCH 5/7 rebase -i: Do not die in do_pick
In reply to a message not in the archiveSince do_pick might be executed in a sub-shell (a modified author environment for instance), calling die in do_pick has no effect but exiting the sub-shell with a failure exit status. Moreover, if do_pick is called while a squash or fixup is happening, die_with_patch will discard $squash_msg as commit message. Indicate an error in do_pick using return statements and properly kill the script at the call sites. Remove unused commit message title argument from do_pick's signature.
--- git-rebase--interactive.sh | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-)
git-rebase--interactive.sh +18 -15
@@ -493,14 +493,10 @@ record_in_rewritten() { # Apply the changes introduced by the given commit to the current head. # -# do_pick [--edit] <commit> <title> +# do_pick [--edit] <commit> # # Wrapper around git-cherry-pick. # -# <title> -# The commit message title of <commit>. Used for information -# purposes only. -# # <commit> # The commit to cherry-pick. #
@@ -508,6 +504,12 @@ record_in_rewritten() { # After picking <commit>, open an editor and let the user edit the # commit message. The editor contents becomes the commit message of # the new head. +# +# The return value is 1 if applying the changes resulted in a conflict +# and 2 if the specified arguments were incorrect. If the changes could +# be applied successfully but creating the commit failed, a value +# greater than 2 is returned. No commit is created in either case and +# the index is left with the (conflicting) changes in place. do_pick () { rewrite= rewrite_amend=
@@ -528,7 +530,11 @@ do_pick () { esac shift done - test $# -ne 2 && die "do_pick: wrong number of arguments" + if test $# -ne 1 + then + warn "do_pick: wrong number of arguments" + return 2 + fi if test "$(git rev-parse HEAD)" = "$squash_onto" then
@@ -545,11 +551,9 @@ do_pick () { # rebase --continue. git commit --allow-empty --allow-empty-message --amend \ --no-post-rewrite -n -q -C $1 && - pick_one -n $1 || - die_with_patch $1 "Could not apply $1... $2" + pick_one -n $1 || return 1 else - pick_one ${rewrite:+-n} $1 || - die_with_patch $1 "Could not apply $1... $2" + pick_one ${rewrite:+-n} $1 || return 1 fi if test -n "$rewrite"
@@ -557,8 +561,7 @@ do_pick () { git commit --allow-empty --no-post-rewrite -n -q \ ${rewrite_amend:+--amend} \ ${rewrite_edit:+--edit} \ - ${gpg_sign_opt:+"$gpg_sign_opt"} || - die_with_patch $1 "Could not rewrite commit after successfully picking $1... $2" + ${gpg_sign_opt:+"$gpg_sign_opt"} || return 3 fi }
@@ -573,21 +576,21 @@ do_next () { comment_for_reflog pick mark_action_done - do_pick $sha1 "$rest" + do_pick $sha1 || die_with_patch $sha1 "Could not apply $sha1... $rest" record_in_rewritten $sha1 ;; reword|r) comment_for_reflog reword mark_action_done - do_pick --edit $sha1 "$rest" + do_pick --edit $sha1 || die_with_patch $sha1 "Could not apply $sha1... $rest" record_in_rewritten $sha1 ;; edit|e) comment_for_reflog edit mark_action_done - do_pick $sha1 "$rest" + do_pick $sha1 || die_with_patch $sha1 "Could not apply $sha1... $rest" warn "Stopped at $sha1... $rest" exit_with_patch $sha1 0 ;;
Signature · 1 line
2.0.0
RFC PATCH 6/7 rebase -i: Prepare for squash in terms of do_pick --amend
In reply to a message not in the archiveRewrite squash and fixup handling in do_next using the atomic sequence
pick_one
commit
in order to test the correctness of a single do_squash or parametrized do_pick and make the subsequent patch reimplementing squash in terms of such a single function more readable.
Might be squashed into the subsequent commit.
--- git-rebase--interactive.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-)
git-rebase--interactive.sh +15 -5
@@ -613,15 +613,15 @@ do_next () { author_script_content=$(get_author_ident_from_commit HEAD) echo "$author_script_content" > "$author_script" eval "$author_script_content" - if ! pick_one -n $sha1 - then - git rev-parse --verify HEAD >"$amend" - die_failed_squash $sha1 "$rest" - fi case "$(peek_next_command)" in squash|s|fixup|f) # This is an intermediate commit; its message will only be # used in case of trouble. So use the long version: + if ! pick_one -n $sha1 + then + git rev-parse --verify HEAD >"$amend" + die_failed_squash $sha1 "Could not apply $sha1... $rest" + fi do_with_author output git commit --amend --no-verify -F "$squash_msg" \ ${gpg_sign_opt:+"$gpg_sign_opt"} || die_failed_squash $sha1 "$rest"
@@ -630,12 +630,22 @@ do_next () { # This is the final command of this squash/fixup group if test -f "$fixup_msg" then + if ! pick_one -n $sha1 + then + git rev-parse --verify HEAD >"$amend" + die_failed_squash $sha1 "Could not apply $sha1... $rest" + fi do_with_author git commit --amend --no-verify -F "$fixup_msg" \ ${gpg_sign_opt:+"$gpg_sign_opt"} || die_failed_squash $sha1 "$rest" else cp "$squash_msg" "$GIT_DIR"/SQUASH_MSG || exit rm -f "$GIT_DIR"/MERGE_MSG + if ! pick_one -n $sha1 + then + git rev-parse --verify HEAD >"$amend" + die_failed_squash $sha1 "Could not apply $sha1... $rest" + fi do_with_author git commit --amend --no-verify -F "$GIT_DIR"/SQUASH_MSG -e \ ${gpg_sign_opt:+"$gpg_sign_opt"} || die_failed_squash $sha1 "$rest"
Signature · 1 line
2.0.0
RFC PATCH 7/7 rebase -i: Teach do_pick the options --amend and --file
In reply to a message not in the archiveThe to-do list command squash and its close relative fixup replay the changes of a commit like pick but do not recreate the commit. Instead they replace the previous commit with a new commit that also introduces the changes of the squashed commit. This is roughly like cherry-picking without committing and using git-commit to amend the previous commit.
The to-do list
pick a Some changes
squash b Some more changes
gets translated into the sequence of git commands
git cherry-pick a
git cherry-pick -n b
git commit --amend
and if cherry-pick supported --amend this would look even more like the to-do list it is based on
git cherry-pick a
git cherry-pick --amend b.
Since do_pick takes care of pick entries and the above suggests squash as an alias for pick --amend, teach do_pick to handle the option --amend and reimplement squash in terms of do_pick --amend. Also teach it the option --file which is used to specify $squash_msg as commit message.
Both --amend and --file are commit rewriting options. If they are encountered during options parsing, assign rewrite and pass --amend (--file respectively) to the rewrite command. Be careful when --amend is used to pick a root commit because HEAD might point to the sentinel commit but there is still nothing to amend. Be sure to initialize $amend so that commits are squashed even when rebase is interrupted for resolving conflicts. It is not a mistake to do the initialization regardless of any conflicts because $amend is always cleared before the next to-do item is processed.
---
Notes:
A question about when to enable the post-rewrite hook.
rebase collects the hashes of all processed commits using record_in_rewritten and runs the post-rewrite script after the rebase is complete. Two points seem to confuse me.
1) For a pick the hash is record_in_rewritten regardless of whether the hash changed or not (the commit was recreated or the head was fast-forwarded). Ok, the hook can figure that out. Is this behaviour intended?
2) For a reword the amend disables the post-rewrite hook but for a squash (or fixup) the hook is executed each time the squash commit is amended. Does not this result in the hook being executed twice for each scheduled squash command? Once for the amend and once for the rebase. The hook most likely does not figure that out.
This patch never executes the post-rewrite hook when processing the to-do list. The execution after the rebase is finished is still conducted. I am uncertain whether this is correct. The tests seem to succeed with both implementations.
git-rebase--interactive.sh | 65 ++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 25 deletions(-)
git-rebase--interactive.sh +40 -25
@@ -493,7 +493,7 @@ record_in_rewritten() { # Apply the changes introduced by the given commit to the current head. # -# do_pick [--edit] <commit> +# do_pick [--file <file>] [--amend] [--edit] <commit> # # Wrapper around git-cherry-pick. #
@@ -505,6 +505,17 @@ record_in_rewritten() { # commit message. The editor contents becomes the commit message of # the new head. # +# --amend +# After picking <commit>, replace the current head commit with a new +# commit that also introduces the changes of <commit>. +# +# _This is not a git-cherry-pick option._ +# +# -F <file>, --file <file> +# Take the commit message from the given file. +# +# _This is not a git-cherry-pick option._ +# # The return value is 1 if applying the changes resulted in a conflict # and 2 if the specified arguments were incorrect. If the changes could # be applied successfully but creating the commit failed, a value
@@ -514,9 +525,30 @@ do_pick () { rewrite= rewrite_amend= rewrite_edit= + rewrite_message= while test $# -gt 0 do case "$1" in + -F|--file) + if test $# -eq 0 + then + warn "do_pick: option --file specified but no <file> given" + return 2 + fi + rewrite=y + rewrite_message=$2 + shift + ;; + --amend) + if test "$(git rev-parse HEAD)" = "$squash_onto" || ! git rev-parse --verify HEAD + then + warn "do_pick: nothing to amend" + return 2 + fi + rewrite=y + rewrite_amend=y + git rev-parse --verify HEAD >"$amend" + ;; -e|--edit) rewrite=y rewrite_edit=y
@@ -561,6 +593,7 @@ do_pick () { git commit --allow-empty --no-post-rewrite -n -q \ ${rewrite_amend:+--amend} \ ${rewrite_edit:+--edit} \ + ${rewrite_message:+--file "$rewrite_message"} \ ${gpg_sign_opt:+"$gpg_sign_opt"} || return 3 fi }
@@ -617,38 +650,20 @@ do_next () { squash|s|fixup|f) # This is an intermediate commit; its message will only be # used in case of trouble. So use the long version: - if ! pick_one -n $sha1 - then - git rev-parse --verify HEAD >"$amend" - die_failed_squash $sha1 "Could not apply $sha1... $rest" - fi - do_with_author output git commit --amend --no-verify -F "$squash_msg" \ - ${gpg_sign_opt:+"$gpg_sign_opt"} || - die_failed_squash $sha1 "$rest" + do_with_author do_pick --amend -F "$squash_msg" $sha1 \ + || die_failed_squash $sha1 "Could not apply $sha1... $rest" ;; *) # This is the final command of this squash/fixup group if test -f "$fixup_msg" then - if ! pick_one -n $sha1 - then - git rev-parse --verify HEAD >"$amend" - die_failed_squash $sha1 "Could not apply $sha1... $rest" - fi - do_with_author git commit --amend --no-verify -F "$fixup_msg" \ - ${gpg_sign_opt:+"$gpg_sign_opt"} || - die_failed_squash $sha1 "$rest" + do_with_author do_pick --amend -F "$fixup_msg" $sha1 \ + || die_failed_squash $sha1 "Could not apply $sha1... $rest" else cp "$squash_msg" "$GIT_DIR"/SQUASH_MSG || exit rm -f "$GIT_DIR"/MERGE_MSG - if ! pick_one -n $sha1 - then - git rev-parse --verify HEAD >"$amend" - die_failed_squash $sha1 "Could not apply $sha1... $rest" - fi - do_with_author git commit --amend --no-verify -F "$GIT_DIR"/SQUASH_MSG -e \ - ${gpg_sign_opt:+"$gpg_sign_opt"} || - die_failed_squash $sha1 "$rest" + do_with_author do_pick --amend -F "$GIT_DIR"/SQUASH_MSG -e $sha1 \ + || die_failed_squash $sha1 "Could not apply $sha1... $rest" fi rm -f "$squash_msg" "$fixup_msg" ;;
Signature · 1 line
2.0.0