2

Say I have two branches in a git repo 'master' and 'feature'. The two branches have diverged from some point. Now I want to take the exact changes to 'script.py' from commit '0123456789abcdef' on the 'feature' branch and apply those changes to the corresponding lines of 'script.py' at the head of the 'master' branch.
git cherry-pick seems like the obvious choice to me here, but it does not do exactly what I want. The trouble is that git cherry-pick brings the 'master' branch up to date with all the changes on 'feature' up to commit '0123456789abcdef'.
I do not want all of those changes applied. I only want exactly the changes that occurred in commit '0123456789abcdef' - nothing more, nothing less. I thought it would be smart if I could somehow exploit common history to figure out where the corresponding changed lines might have moved in 'master' relative to the commit from 'feature', yet without applying all those preceding changes on 'master'.
How can I do this? Surely, I can copy-and-paste it from the output of a git diff, but knowing Git somewhat well it seems to me it must have a better way to do it that I just do not know yet.

Edit: it seems the above behaviour must have been a result of a mistake I can no longer reproduce. Based on @torek's answer, I have repeated the correct procedure of a cherry-pick and it produces the result I wanted.

Thomas Arildsen
  • 1,079
  • 2
  • 14
  • 31
  • Git does not deal in "files" or "changes." It deals in _commits_, which are entire snapshots of the whole state of your project. If you like the _state_ of a certain file in a certain commit, and you want that to be the _state_ of the file at the head of the current branch, use `git show` to extract that file as it is in that commit into your working tree and _commit_ that state. – matt Jan 26 '21 at 18:28
  • 1
    Like you said you can use `git diff` between two commits of scripts.py file and output the difference to a file and then use `git apply` to apply it. – rootkonda Jan 26 '21 at 18:31
  • Thanks @matt, I know. However, we as humans are usually capable of talking to each other in slightly less restricted vocabularies or even more abstract terms than computers, *and still even understand each other...* Git can do `git diff` and I call the output of that "changes". Those are the ones I am interested in. I do not like any particular state of the repository. I like a particular difference between two states of my repository and I wish to apply that (relative) difference on top of a completely different state of my repository. – Thomas Arildsen Jan 26 '21 at 19:35
  • Also @matt, Git *does* deal in "files" in the sense that I can for example `git checkout [commit] -- [a particular file]` so it makes good sense to also talk about files in the context of Git as well. Now can we please get on with "parsing" the actual question as humans and refrain from lecturing off-topic as @rootkonda does rather relevantly? – Thomas Arildsen Jan 26 '21 at 19:38
  • Thanks @rootkonda, this was a helpful pointer. However, due to the diverging histories, the changes do not apply at the same lines of the files on 'master' as in the source commit. I was hoping Git was somehow clever enough to use history to figure out where the corresponding lines have moved now in the files on 'master'. – Thomas Arildsen Jan 26 '21 at 19:47

1 Answers1

3

In this claim:

git cherry-pick seems like the obvious choice to me here, but it does not do exactly what I want. The trouble is that git cherry-pick brings the 'master' branch up to date with all the changes on 'feature' up to commit '0123456789abcdef'.

you have conflated cherry-pick with merge. If you were to run:

git checkout master
git merge 0123456789abcdef

that would locate the merge base of the current tip commit of master, and commit 0123456789abcdef, and do what you say. But git cherry-pick 0123456789abcdef locates instead the parent commit of 0123456789abcdef, then invokes Git's merge engine on the three commits involved here.

The effect is that git cherry-pick will find the changes from 0123456789abcdef^—the parent of 0123456789abcdef—to 0123456789abcdef, and apply them to the current or HEAD commit. This will affect more than one file, if more than one file has any difference from 0123456789abcdef^ to 0123456789abcdef (use git diff 0123456789abcdef^ 0123456789abcdef, or more simply, git show 0123456789abcdef, to see; note that 0123456789abcdef must not be a merge commit).

If you only want the 0123456789abcdef^-to-0123456789abcdef changes for one particular file applied, use git cherry-pick -n so that git cherry-pick does the initial part of the work, but then stops before committing the result. This gives you the opportunity to back out changes to other files, using git reset or git restore. (If using git reset, be sure to supply file names; git restore is new in Git 2.23, and is a more limited but more flexible variant of git reset in this particular regard.)

Note that git cherry-pick does not do the equivalent of:

git diff master 0123456789abcdef

That is, it's not looking to make the master version of any file match the version of any file in 0123456789abcdef. What Git will end up doing is a three-way merge, combining the diffs produced by:

git diff --find-renames 0123456789abcdef^ HEAD

(i.e., what you changed with respect to the parent of 0123456789abcdef) with those produced by:

git diff --find-renames 0123456789abcdef^ 0123456789abcdef

(i.e., what they changed with respect to their own parent). The combined changes are then applied to the snapshot from 0123456789abcdef^: this adds your changes to their parent, plus their changes to their parent. The end result—if there are no conflicts—is that you've added their changes to your current commit, where "their changes" are with respect to their commit's parent's snapshot; but this also accounts for any line motion, or even file-name-changes, between their commit's parent and your commit.

Hence, based on your question, it seems that cherry-pick is the answer. If it's not, you're going to need to provide more information about why that is the case.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Thanks for the thorough explanation. Now that I re-try on my repository, it does what I originally wanted. I must have made some mistake when I wrote my original question. Strangely, I cannot reproduce it now. My best guess is that I may have cherry-picked onto another branch than 'master' or I may in a blunder actually have typed `merge` instead of `cherry-pick`. – Thomas Arildsen Jan 28 '21 at 21:18