From: Igor Djordjevic <igor.d.djordjevic@gmail.com>
To: git@vger.kernel.org
Cc: Johannes Sixt <j6t@kdbg.org>,
Nikolay Shustov <nikolay.shustov@gmail.com>,
Johannes Schneider <mailings@cedarsoft.com>,
Patrik Gornicz <patrik-git@mail.pgornicz.com>,
Martin Waitz <tali@admingilde.org>,
Shawn Pearce <spearce@spearce.org>, Sam Vilain <sam@vilain.net>,
Jakub Narebski <jnareb@gmail.com>
Subject: [SCRIPT/RFC 3/3] git-commit--onto-parent.sh
Date: Sun, 26 Nov 2017 23:45:10 +0100 [thread overview]
Message-ID: <7ea28777-1e68-09e3-5f39-4ca0291e3f36@gmail.com> (raw)
In-Reply-To: <8998e832-f49f-4de4-eb8d-a7934fba97b5@gmail.com>
Finally, "git-commit--onto-parent.sh"[*1*] shows an initial script
version for you to examine, test out and hopefully comment on :)
Especially interesting part might be index-only three-way file merge,
through usage of "git-merge-one-file--cached" script. Of course, this
still only works for some trivial resolutions, where in case of more
complex ones, involving unresolved conflicts, we back-out and fail.
Still, it should be more powerful than `git-apply`.
Consider this proof of concept and work in progress, an idea where
I`d like feedback on everything you come up with or find interesting,
even parameter name possibly used instead of "--onto-parent" (and its
short version), or approach in general.
For example, it might make sense to separate commit creation (on
current HEAD`s parent) and its actual re-merging into integration
test branch, where "--remerge" (or something) parameter would be used
on top of "--onto-parent" to trigger both, if/when desired.
Another direction to think in might be introducing more general
"--onto" parameter, too (or instead), without "parent" restriction,
allowing to record a commit on top of any arbitrary commit (other
than HEAD). This could even be defaulted to "git commit <commit-ish>"
(no option needed), where current "git commit" behaviour would then
just be a special case of omitted <commit-ish> defaulting to HEAD,
aligning well with other Git commands sharing the same behaviour.
Alas, rewind to present...
Please do note that I`m still relatively new to Git, and pretty new
to both Linux and scripting in general (on Windows as well), and the
whole concept of open-source software contributing, even, so please
bare with me (or at least don`t get upset too much, lol), and do feel
free to share your thoughts and remarks, even the trivial or harsh
ones -- I`m grateful to learn and expand my knowledge, hopefully
producing something useful in return :) Heck, might be I`m totally
off-track here as well.
p.s. For some context - nowadays I mostly work in Delphi, and
occasionally in C#, though through last 20 years I`ve been involved
with C, Pascal, Basic, but also PHP, JavaScript, and whatnot - even
good old assembly from time to time, when needed :)
Regards, Buga
[*1*] "git-commit--onto-parent.sh", probably too heavily commented in
the first place, but as I`m new to everything here I kind of feel the
plain words might unfortunately describe my intention a bit better
than my code, for now at least.
--- 8< ---
#!/bin/sh
#
# Copyright (c) 2017 Igor Djordjevic
i=$#
while test $i != 0
do
#
# Parameter parsing might be uninteresting here, as the whole
# script is currently just a wrapper around `git commit`, for a
# functionality that conceptually belongs there directly.
#
case "$1" in
--onto-parent=*)
onto_parent="${1#*=}"
shift && i=$(expr $i - 1)
;;
--onto-parent)
shift && i=$(expr $i - 1)
onto_parent="$1"
shift && i=$(expr $i - 1)
;;
-a|--a|--al|--all)
all=t
#
# For now, `git commit` "--all" option is special-cased in
# terms of being stripped out of the original command line
# (to be passed to `git commit`) and processed manually, as
# once commit is to be made, due to states of index and
# working tree, "--all" is most probably NOT what the user
# wants nor expects ;)
#
shift && i=$(expr $i - 1)
;;
*)
# parameters to pass down to `git commit`
set -- "$@" "$1"
shift && i=$(expr $i - 1)
;;
esac
done
main () {
#
# Store current HEAD (ref or commit) and verify that
# --onto-parent is valid and amongst its parents.
#
head="$(git symbolic-ref --short --quiet HEAD)" ||
head="$(git rev-parse HEAD^0)" &&
verify_onto_parent "$head" "$onto_parent" || exit 1
#
# As both HEAD and "--onto-parent" could be refs, where underlying
# commits could change, store original commits for later parents
# processing, getting updated parent list for new/updated
# merge commit.
#
head_commit="$(git rev-parse "$head"^0)" &&
onto_parent_commit="$(git rev-parse "$onto_parent"^0)" || exit 1
#
# Custom processing of stripped "--all" parameter - if we were to
# just pass it to `git commit`, "--all" would most probably yield
# an unexpected result in the eyes of the user, as it would include
# _all changes from all the other merge commit parents as well_,
# not just the changes we may actually wanted to "push down"
# (commit) onto specified parent (what would `git diff` show),
# due to state of index and working tree at the time of commit.
#
if test -n "$all"
then
git add --update
fi
#
# Abort if no cached changes, nothing to be committed.
#
git diff --cached --quiet
if test $? -eq 0
then
printf >&2 '%s\n' "error: no changes added to commit"
exit 1
fi
#
# Backup current index to be restored (and committed) in the end.
#
merge_index="$(git write-tree)" || exit 1
#
# Reset index to destination parent (without touching working
# tree), and try applying cached changes.
#
# In case changes do not apply cleanly onto desired parent, abort.
#
apply_changes "$head_commit" "$onto_parent_commit" "$merge_index" "$onto_parent" || exit 1
#
# Move HEAD to specified parent (without touching working tree)
# to prepare to record a commit there, and make the commit,
# passing parameters through to `git commit`.
#
# In case of error, or when `git commit` didn`t actually make
# the commit, like when --dry-run parameter is provided (for
# example), abort, as there is nothing to do - no new commit, no
# need to produce the "updated" merge commit, either.
#
# Note that we don`t abort right away, as restoring original
# index and HEAD position is needed all the same, so we
# potentially abort only once that is done, a bit further below.
#
# [ This is something that could be thought of a bit more,
# might be forbidding passing through of some `git commit`
# parameters in the first place, like --dry-run...? ]
#
move_head "$onto_parent" &&
git commit "$@"
if test $? -ne 0 || {
new_parent_commit="$(git rev-parse HEAD^0)"
test "$new_parent_commit" = "$onto_parent_commit"
}
then
no_commit=t
fi
#
# Remove entry from HEAD reflog, not to pollute it with
# uninteresting in-between steps we take, leaking implementation
# details to end user.
#
# We do left it inside corresponding branch reflog where commit
# is made (if $onto_parent was a branch), though, as that`s where
# it still matters.
#
git reflog delete HEAD@{0}
#
# Restore original index state and move HEAD to original position,
# (still not touching working tree), aborting if previously
# signalled.
#
git read-tree --reset "$merge_index" &&
move_head "$head" &&
test -z "$no_commit" || exit 1
#
# Drop original HEAD merge commit to have it replaced by
# upcoming "updated" merge commit.
#
# This step is needed for eventually getting an expected merge
# message out of "git fmt-merge-msg", as it seems HEAD dependent
# as well, beside being input format picky already...?
#
#git update-ref --create-reflog -m "reset: moving to HEAD^" HEAD HEAD^ || exit 1
reflog_ref="$(git symbolic-ref --short --quiet HEAD)"
git update-ref HEAD HEAD^ || exit 1
#
# Remove both HEAD and underlying reference reflog entries this
# time, as here we really want to mask previous step completely,
# being taken just to satisfy "git fmt-merge-msg" expectations.
#
if test -n "$reflog_ref"
then
git reflog delete "$reflog_ref"@{0}
fi
git reflog delete HEAD@{0}
#
# Prepare "updated" merge commit message and parent list.
#
if test -n "$(git rev-parse --verify --quiet $head_commit^2^{commit})"
then
merge_parents="$(get_merge_parents "$head_commit" "$onto_parent" "$onto_parent_commit" "$new_parent_commit")" &&
merge_message="$(get_merge_message "$merge_parents")" || exit 1
else
#
# As we`re actually selling the option as "--onto-parent" and
# not "--onto-MERGE-parent", we might as well properly support
# a special case where HEAD commit is not a merge.
#
# Existing HEAD commit will come after commit to be made,
# basically being kind of rebased onto new commit (but still
# not touching working tree), where we can then also reuse
# original HEAD commit authorship, and message, too (instead
# of building a merge one).
#
merge_parents="$new_parent_commit" &&
merge_message="$(git show -s --format=%B "$head_commit")" &&
reuse_authorship $head_commit || exit 1
fi
merge_parent_commits="$(get_merge_parent_commits "$merge_parents")" &&
#
# Do the actual commit, updating HEAD accordingly.
#
merge_commit="$(printf '%s\n' "$merge_message" |
git commit-tree "$merge_index" $merge_parent_commits)" &&
git update-ref --create-reflog -m "$merge_message" HEAD "$merge_commit" || exit 1
}
verify_onto_parent () {
#
# $1 starting point head (ref or commit)
# $2 parent of $1 to commit onto (ref or commit)
#
local head="$1"
local onto_parent="$2"
if test -z "$onto_parent"
then
printf >&2 '%s\n' "error: no parent provided"
printf >&2 '%s\n' "(use \"--onto-parent <commit-ish>\""
return 1;
fi
if test -z "$(git rev-parse --verify --quiet "$onto_parent"^{commit})"
then
printf >&2 '%s\n' "error: '$onto_parent' not valid commit object"
return 1
fi
local onto_parent_commit="$(git rev-parse "$onto_parent"^0)"
for parent_commit in $(git rev-parse $head^@)
do
if test "$parent_commit" = "$onto_parent_commit"
then
return 0
fi
done
printf >&2 '%s\n' "error: '$onto_parent' not parent of '$head'"
return 1
}
apply_changes () {
#
# $1 original/starting point HEAD commit
# $2 parent commit of $1 to apply changes to
# $3 index with changes on top of $1 to apply/merge onto $2
# $4 original parameter value of $2 (ref or commit), used for
# prettier message only
#
local head_commit="$1"
local onto_parent_commit="$2"
local merge_index="$3"
local onto_parent="$4"
git read-tree --reset $onto_parent_commit &&
#
# Attempt simple patching first - take differences between
# $head_commit and $merge_index and try applying to current index
# (previously reset to $onto_parent_commit).
#
git diff-tree --binary --patch --find-renames --find-copies $head_commit $merge_index |
git apply --cached 2>/dev/null &&
return 0
printf '%s\n' "Unable to apply cleanly onto '$onto_parent', trying simple merge"
#
# A bit more aggressive approach - try merging with resolving
# trivial conflicts on tree level only (involving file as a whole,
# no conflicts inside file itself).
#
# Note that we take $head_commit as merge-base, producing such
# three-way merge result that basically all changes between
# $onto_parent_commit and $head_commit are reversed, as they`re
# also included inside $merge_index, where only differences
# between $head_commit and $merge_index are applied (in a
# three-way merge manner) to $onto_parent_commit, being exactly
# what we want here.
#
git read-tree -i -m --aggressive $head_commit $onto_parent_commit $merge_index || exit 1
git write-tree >/dev/null 2>&1 &&
return 0
printf '%s\n' "Simple merge did not work, trying automatic merge"
#
# Final attempt - try merging with resolving trivial conflicts on
# file level, too (conflicts inside file itself).
#
# Notice usage of "git-merge-one-file--cached" script here, being
# a slightly tweaked version of original "git-merge-one-file",
# not touching working tree but stuffing trivial three-way
# file merge resolution back into index directly.
#
# If still left with conflicts that need to be resolved manually,
# abort... and go home, you`re drunk.
#
if ! git merge-index -o git-merge-one-file--cached -a
then
# abort, cleanup
git read-tree --reset $merge_index
exit 1
fi
}
move_head () {
#
# $1 destination ref or commit
#
# Move HEAD to $1 without touching the working tree.
#
# Kind of "soft checkout", where original "git checkout" touches
# the working tree, and "git reset --soft" does not move HEAD,
# both undesired here.
#
local destination="$1"
local destination_commit="$(git rev-parse --verify --quiet $destination^0)" ||
{
printf >&2 '%s\n' "fatal: invalid reference: $destination"
return 1
}
#local reflog_message="$(get_checkout_reflog_message $destination)"
local destination_ref="$(git rev-parse --symbolic-full-name $destination)"
case "$destination_ref" in
refs/heads/*)
# can`t use "update-ref --no-deref" as it writes commit only,
# instead of ref, essentially detaching HEAD to that commit
#git symbolic-ref -m "$reflog_message" HEAD "$destination_ref"
git symbolic-ref HEAD "$destination_ref"
;;
refs/tags/*|\
refs/remotes/*|\
"")
# can`t use "symbolic-ref" as it refuses to write commit only,
# expecting a valid ref instead (value inside "refs/")
#git update-ref --create-reflog -m "$reflog_message" --no-deref HEAD "$destination_commit"
git update-ref --no-deref HEAD "$destination_commit"
# mask this step as end-user uninteresting implementation detail
git reflog delete HEAD@{0}
;;
*)
printf >&2 '%s\n' "fatal: invalid reference: $destination_ref"
return 1
;;
esac
return 0
}
get_merge_parents () {
#
# $1 original/starting point HEAD commit
# $2 parent of $1 to commit onto (ref or commit)
# $3 original commit of $2 (if $2 is ref, otherwise equals $2)
# $4 new commit (onto $2, to be new merge parent)
#
# Walk original merge commit parents to find the one we`re posting
# onto (or amending, even), and update/replace it accordingly with
# new commit, becoming a new parent of upcoming new/updated merge
# commit.
#
# Where possible, prefer taking ref over commit, making for a
# prettier merge commit message.
#
local head_commit="$1"
local onto_parent="$2"
local onto_parent_old_commit="$3"
local new_parent_commit="$4"
local merge_parents=
local onto_parent_new_commit="$(git rev-parse $onto_parent^0)"
for parent_commit in $(git rev-parse $head_commit^@)
do
local merge_parent=
if test "$parent_commit" = "$onto_parent_old_commit"
then
if test "$onto_parent_new_commit" = "$new_parent_commit"
then
# $onto_parent is a branch (updateable ref)
merge_parent="$onto_parent"
else
merge_parent="$new_parent_commit"
fi
else
parent_ref="$(git for-each-ref --points-at $parent_commit --count=1 --format="%(refname)")"
if test -n "$parent_ref"
then
merge_parent="$parent_ref"
else
merge_parent="$parent_commit"
fi
fi
# echo to flatten whitespace
merge_parents="$(echo $merge_parents $merge_parent)"
done
if test -n "$merge_parents"
then
printf '%s\n' "$merge_parents"
return 0
else
return 1
fi
}
get_merge_message () {
#
# $@ merge_parents
#
# Provide to-be merge commit message using
# existing `git fmt-merge-msg` machinery.
#
local merge_heads="$(get_merge_heads $@)" &&
local merge_message="$(printf "$merge_heads" | git fmt-merge-msg)"
if test -n "$merge_message"
then
printf '%s\n' "$merge_message"
return 0
else
return 1
fi
}
get_merge_heads () {
#
# $@ merge_parents
#
# Provide input for `git fmt-merge-msg` to get
# nicely formatted merge commit message.
#
# Final result loosely mimics FETCH_HEAD file layout.
#
local merge_heads=
local merge_head=
#
# Skip the first parent, as that is the original merge
# destination, where we`re only interested in parents to be
# merged into it.
#
shift
for merge_parent in $@
do
local merge_parent_ref="$(git rev-parse --symbolic-full-name $merge_parent)"
local merge_parent_commit="$(git rev-parse $merge_parent)"
case "$merge_parent_ref" in
refs/heads/*)
merge_head="$merge_parent_commit\t\tbranch '${merge_parent_ref#refs/heads/}' of ."
;;
refs/tags/*)
merge_head="$merge_parent_commit\t\ttag '${merge_parent_ref#refs/tags/}' of ."
;;
refs/remotes/*)
merge_head="$merge_parent_commit\t\tremote-tracking branch '${merge_parent_ref#refs/remotes/}' of ."
;;
*)
merge_head="$merge_parent_commit\t\t'$(git rev-parse --short $merge_parent_commit)' of ."
;;
esac
merge_heads="$merge_heads$merge_head\n"
done
if test -n "$merge_heads"
then
# '\n' already appended
printf '%s' "$merge_heads"
return 0
else
return 1
fi
}
reuse_authorship () {
#
# $1 commit to reuse authorship from
#
local commit="$1"
GIT_AUTHOR_NAME="$(git show -s --format=%an $commit)"
GIT_AUTHOR_EMAIL="$(git show -s --format=%ae $commit)"
GIT_AUTHOR_DATE="$(git show -s --format=%at $commit)"
export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
}
get_merge_parent_commits () {
#
# $@ merge_parents (might contain ref)
#
# Provide to-be merge commit parent parameters
# in format suitable for `git commit-tree`.
#
local merge_parent_commits=
for merge_parent in $@
do
merge_parent_commits="$(printf '%s\n' "$merge_parent_commits -p $(git rev-parse $merge_parent^0)")"
done
if test -n "$merge_parent_commits"
then
printf '%s\n' "$merge_parent_commits"
return 0
else
return 1
fi
}
get_checkout_reflog_message () {
#
# $1 destination ref or commit
#
local destination="$1"
local source=
source="$(git symbolic-ref --short --quiet HEAD)" ||
source="$(git rev-parse HEAD^0)"
printf '%s' "checkout: moving from $source to $destination"
}
main "$@"
next prev parent reply other threads:[~2017-11-26 23:01 UTC|newest]
Thread overview: 26+ messages / expand[flat|nested] mbox.gz Atom feed top
2017-11-26 22:35 [SCRIPT/RFC 0/3] git-commit --onto-parent (three-way merge, no working tree file changes) Igor Djordjevic
2017-11-26 22:36 ` [SCRIPT/RFC 1/3] setup.sh Igor Djordjevic
2017-11-26 22:36 ` [SCRIPT/RFC 2/3] git-merge-one-file--cached Igor Djordjevic
2017-11-26 22:45 ` Igor Djordjevic [this message]
2017-11-27 21:54 ` [SCRIPT/RFC 0/3] git-commit --onto-parent (three-way merge, no working tree file changes) Johannes Sixt
2017-11-28 1:15 ` Igor Djordjevic
2017-11-29 19:11 ` Johannes Sixt
2017-11-29 23:10 ` Igor Djordjevic
2017-12-01 17:23 ` Johannes Sixt
2017-12-04 2:33 ` Igor Djordjevic
2017-12-06 18:34 ` Johannes Sixt
2017-12-06 18:40 ` Junio C Hamano
2017-12-08 0:15 ` Igor Djordjevic
2017-12-08 16:24 ` Junio C Hamano
2017-12-08 23:54 ` Igor Djordjevic
2017-12-09 2:18 ` Alexei Lozovsky
2017-12-09 3:03 ` Igor Djordjevic
2017-12-09 19:00 ` [SCRIPT/RFC 0/3] git-commit --onto-parent (three-way merge,noworking " Phillip Wood
2017-12-09 19:01 ` [SCRIPT/RFC 0/3] git-commit --onto-parent (three-way merge, noworking " Phillip Wood
2017-12-10 1:20 ` Igor Djordjevic
2017-12-10 12:22 ` [SCRIPT/RFC 0/3] git-commit --onto-parent (three-way merge,noworking " Phillip Wood
2017-12-10 23:17 ` Igor Djordjevic
2017-12-11 1:13 ` Alexei Lozovsky
2017-12-11 1:00 ` Alexei Lozovsky
2017-11-30 22:40 ` [SCRIPT/RFC 0/3] git-commit --onto-parent (three-way merge, no working " Chris Nerwert
2017-12-03 23:01 ` Igor Djordjevic
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: http://vger.kernel.org/majordomo-info.html
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=7ea28777-1e68-09e3-5f39-4ca0291e3f36@gmail.com \
--to=igor.d.djordjevic@gmail.com \
--cc=git@vger.kernel.org \
--cc=j6t@kdbg.org \
--cc=jnareb@gmail.com \
--cc=mailings@cedarsoft.com \
--cc=nikolay.shustov@gmail.com \
--cc=patrik-git@mail.pgornicz.com \
--cc=sam@vilain.net \
--cc=spearce@spearce.org \
--cc=tali@admingilde.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://80x24.org/mirrors/git.git
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 read-only IMAP folder(s) and NNTP newsgroup(s).