I'd like to do "the hardest version" of cherry-pick/merge/rebase/checkout, what means that state of app on my branch begins to look exactly like in the cherry-picked commit (but with keeping history of my branch). In fact I could duplicate my repo, delete everything in my branch and next copy whole content from duplicated version set to needed commit. But well, that's not handy and I believe there's some easier way.
I already tried git cherry-pick <hash> --strategy-option theirs
, but that's not perfect, because it doesn't remove files not existing in cherry-picked commit, what results in big mess in my case.
So, how can I do this?
Edit: I clarified that I need also keep my history, what was not obvious first.
That's not a cherry-pick at all. Don't use
git cherry-pick
to make it: usegit commit
to make it. Here's a very simple recipe1 to make it:If you want to copy the commit message and such from commit
<hash>
, consider adding-c <hash>
to thegit commit
line.1This is not the simplest, but it should be understandable. The simpler ones use plumbing commands after the initial
git checkout
, e.g.:or:
(untested and for the second one you'll have to construct a commit message).
Long
Remember that Git stores commits, with each commit being a complete snapshot of all source files, plus some metadata. The metadata for each commit includes the name and email address of whoever makes the commit; a date-and-time-stamp for when the commit was made; a log message to say why the commit was made; and, crucially for Git, the hash ID of the parent of the commit.
Whenever you have the hash ID of some commit, we say that you are pointing to the commit. If one commit has the hash ID of another commit, the commit with the hash ID points to the other commit.
What this means is that these hash IDs, embedded within each commit, form a backwards-looking chain. If we use single letters to stand in for commits, or number them
C1
,C2
, and so on in sequence, we get:or:
The actual name of each commit is of course some big ugly hash ID, but using letters or numbers like this makes it much easier for us, as humans, to deal with them. In any case, the key is that if we somehow save the hash ID of the last commit in the chain, we end up with the ability to follow the rest of the chain backwards, one commit at a time.
The place we have Git store these hash IDs is in branch names. So a branch name like
master
just stores the real hash ID of commitH
, whileH
itself stores the hash ID of its parentG
, which stores the hash ID of its parentF
, and so on:These backwards-looking links, from
H
toG
toF
, plus the snapshots saved with each commit plus the metadata about who made the commit and why, are the history in your repository. To retain the history that ends inH
, you simply need to make sure that the next commit, when you make it, hasH
as its parent:By making the new commit, Git changes the name
master
to remember the hash ID of new commitI
, whose parent isH
, whose parent is (still)G
, and so on.Your goal is to make commit
I
using the snapshot that's associated with some other commit, such asK
below:Git actually builds new commits out of whatever is in the index, rather than what's in the source tree. So we start with
git checkout master
to make commitH
the current commit andmaster
the current branch, which fills in the index and work-tree from the contents of commitH
.Next, we want the index to match commit
K
—with no other files than those that are inK
—so we start by removing every file from the index. For sanity (i.e., so that we can see what we're doing) we let Get do the same to the work-tree, which it does automatically. So we rungit rm -r .
after making sure that.
refers to the entire index / work-tree pair, by making sure we're at the top of the work-tree and not in some sub-directory / sub-folder.Now only untracked files remain in our work-tree. We can remove these too if we like, using plain
rm
orgit clean
, though in most cases they're harmless. If you wish to remove them, feel free to do that. Then we need to fill in the index—the work-tree once again comes along for the ride—from commitK
, so we rungit checkout <hash-of-K> -- .
. The-- .
is important: it tells Git don't switch commits, just extract everything from the commit named here. Our index and work-tree now match commitK
.(If commit
K
has all files that we have inH
, we could skip thegit rm
step. We only need thegit rm
to remove files that are inH
but are not inK
.)Last, now that we have the index (and work-tree) matching commit
K
, we're safe to make a new commit that is likeK
but does not connect toK
.If you want a merge, use
git merge --no-commit
The above sequence results in:
where the saved source snapshot in commit
I
exactly matches that in commitK
. However, the history produced by readingmaster
, finding that it points toI
, and then reading commitI
and on backwards toH
andG
andF
and so on, never mentions commitK
at all.You might instead want a history that looks like this:
Here, commit
I
reaches back to both commitsH
andK
.Making this variant of commit
I
is a little trickier, because aside from using thegit commit-tree
plumbing command, the only way to make commitI
is to usegit merge
.Here, the easy way is to run
git merge -s ours --no-commit
, as in:We use
-s ours
here to make things go faster and more smoothly. What we're building is really the result ofgit merge -s theirs
, except for the fact that there is nogit merge -s theirs
. The-s ours
means ignore their commit, just keep the contents from our commitH
. Then we throw that out and replace it with the content from their commitK
, and then we finish the merge to get a merge commitI
that points to bothH
andK
.As before, there are plumbing command tricks that make this even easier. They're just not obvious unless you understand the low level storage format that Git uses internally. The "remove everything, then check out a different commit's contents" method is really obvious, and is easy to remember.