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-pickto make it: usegit committo 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 commitline.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
masterjust stores the real hash ID of commitH, whileHitself stores the hash ID of its parentG, which stores the hash ID of its parentF, and so on:These backwards-looking links, from
HtoGtoF, 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, hasHas its parent:By making the new commit, Git changes the name
masterto remember the hash ID of new commitI, whose parent isH, whose parent is (still)G, and so on.Your goal is to make commit
Iusing the snapshot that's associated with some other commit, such asKbelow: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 masterto make commitHthe current commit andmasterthe 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
rmorgit 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
Khas all files that we have inH, we could skip thegit rmstep. We only need thegit rmto remove files that are inHbut 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 likeKbut does not connect toK.If you want a merge, use
git merge --no-commitThe above sequence results in:
where the saved source snapshot in commit
Iexactly matches that in commitK. However, the history produced by readingmaster, finding that it points toI, and then reading commitIand on backwards toHandGandFand so on, never mentions commitKat all.You might instead want a history that looks like this:
Here, commit
Ireaches back to both commitsHandK.Making this variant of commit
Iis a little trickier, because aside from using thegit commit-treeplumbing command, the only way to make commitIis to usegit merge.Here, the easy way is to run
git merge -s ours --no-commit, as in:We use
-s ourshere 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 oursmeans 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 commitIthat points to bothHandK.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.