Bertrand
Bertrand

Reputation: 1064

Git apply vs am

I generated a patch using the following command

git log -p -m -1 --pretty=email --first-parent XXXX.

I'd like to apply this patch to another repository (having the same files but not necessarily the same history).

When using git apply the patch is correctly applied, except that I have to commit the files myself with the right user, date ...

When using git am I have the following errors:

error: toto.cpp: patch does not apply

I just don't manage to understand why it would work with apply but no with am

Any idea how I can apply it with am ?

Edit: The answers given in linked answer just explain that apply won't commit the changes while am will. More it says that am uses apply in background so it seems it's something specific to the am command that fails.

Upvotes: 3

Views: 1535

Answers (1)

torek
torek

Reputation: 489083

The git am command is deliberately picky about its input format so that it can create a new commit whose hash ID is identical to the hash ID of the original commit, which in turn means that the new commit that git am created is bit-for-bit identical to the original commit.

The git apply command is deliberately not-so-picky about its input format because you, not git apply, will create a new commit that is not bit-for-bit identical to any original commit.

Your question starts with the command:

git log -p -m -1 --pretty=email --first-parent

which suggests that you are showing a merge. The git format-patch command never includes merges in its output, because git am cannot make merges. Since the point of git am is to make a bit-for-bit identical commit, while the point of git format-patch is to produce output suitable for input to git am and git am cannot make merges, there's no need for git format-patch to present a merge commit.

These combine to make the git am tool useless for your particular purpose (which appears to be convert a merge commit to a changeset against its first parent, transport that changeset through email or something similar to email, and then apply it at the other end to get a different commit with similar but not identical metadata, as if via cherry-pick). You will just have to write your own tool, or use git apply and make the commit manually.

As the above paragraph suggests, one method you could use to write your own tool is to cherry-pick the merge (with -m 1) atop the first parent of the merge (probably on a detached HEAD), use git format-patch to format that patch, and use git am to apply that patch elsewhere. This script is quite untested but might work:

#! /bin/sh

. $(git --exec-path)/git-sh-setup

# abort if on orphan branch (not worth the necessary hackery)
git rev-parse -q --verify HEAD 2>/dev/null ||
    die 'this does not work on an orphan branch'

# stolen out of git-filter-branch
finish_ident() {
    # Ensure non-empty id name.
    echo "case \"\$GIT_$1_NAME\" in \"\") GIT_$1_NAME=\"\${GIT_$1_EMAIL%%@*}\" && export GIT_$1_NAME;; esac"
    # And make sure everything is exported.
    echo "export GIT_$1_NAME"
    echo "export GIT_$1_EMAIL"
    echo "export GIT_$1_DATE"
}

set_ident () {
    parse_ident_from_commit author AUTHOR committer COMMITTER
    finish_ident AUTHOR
    finish_ident COMMITTER
}

# begin format-patch-even-if-merge code

hash=$(git rev-parse --verify "$1") || exit # verify that it's valid
hash=$(git rev-parse $hash^{commit}) || exit # and that it's a commit
parents=$(git rev-parse $hash^@)
set $parents

# if a root commit or ordinary commit, just use git format-patch
case $# in
0|1) git format-patch --stdout -1 $1; exit;;
*) ;; # merge - use cherry-pick
esac

# this is the meat of the trick, here
firstparent=$1
exec 3>&1 1>&2 # save stdout and redirect to stderr

echo "cherry picking $hash onto $firstparent to make it format-able"

# save where we were (branch or hash); arrange to return
# there on exit or ^C etc
if ! returnto=$(git symbolic-ref --short HEAD 2>/dev/null); then
    # already detached, save hash ID
    returnto=$(git rev-parse HEAD)
fi
trap "git checkout $returnto; exit" 0 1 2 3 15

# Use the identity of the merge creator for new commits.
# NOTE: this should be optional and probably NOT the default,
# so it is commented out here.
#eval "$(set_ident <$hash)" ||
#    die "setting author/committer failed for commit $hash"

# Move to a detached head on the first parent of the merge.
git checkout $firstparent || exit

# Now we can cherry-pick the merge to a non-merge.
git cherry-pick -m 1 $hash || exit

# Now show the commit we just made, to original stdout.
git format-patch --stdout -1 HEAD 1>&3

A few more notes, just for completeness

If you wish to transport a merge commit intact, the easy way to do that is through the standard git push or git fetch protocols. If those are not available for some reason, the git bundle command can make a file (which Git calls a bundle) that contains all the data needed to transport some portion of a repository across a barrier that push/fetch cannot itself cross. You can then run git fetch on the bundle file, so that you extract the necessary information after crossing that barrier some other way.

Note, however, that you need all the objects reachable from the merge commit: the bundle must contain every object that the other Git lacks. The whole point of the fetch and push commands is to let Git compute this set of objects: the two Gits talk to each other, to figure out what the sending Git has that the receiving Git lacks. That's what goes into the bundle. While it's OK for a bundle to have extra objects (the receiving Git can ignore them), it's a waste of resources to transport them, so fetch or push will compute the minimal object-set, pack it up into a "thin pack" (the fetch/push equivalent of a bundle), send it over the wire, and let the other end unpack it.

Upvotes: 4

Related Questions