1

Often I want to break up a large commit:

git commit -a -m "Monolith"

I know how to split a commit (git gui is my friend) but sometimes I actually want to rework it by hand...

git branch temp          # Keep a reference to all my hard work
git reset --hard HEAD~   # Rewind 'my-topic' branch

# hack hack
git commit -a -m "Refactor 1"
# hack hack
git commit -a -m "Refactor 2"

To complete it, I then want to apply another commit that brings the code back to exactly the state stored in branch temp, usually including the original commit message.

Is there an easy way to do this?

I imagined that a merge-strategy on cherry-pick might get the job done, but alas none of these did:

  git cherry-pick --strategy=theirs temp   # I am told about conflicts
  git status                               # ... But here I find no changes to commit!
  git cherry-pick --strategy=ours temp     # Worth a try. Nope, empty commit.
  git cherry-pick --strategy=recursive --strategy-option=ours temp  # Partial result, bits are missing

I know of one way to achieve this:

git log -1 temp # note the SHA
git checkout temp
git reset --soft my-topic
git commit -C <SHA-from-above>  # branch 'temp' now contains the desired commit
git checkout HEAD -B my-topic
git br -d temp

However this seems a very error-prone way to go about it. Any little mistake and the commits I want to keep might not be in any branch (git reflog to the rescue...). I'd like to find something more logical and easier to remember/do.

Luke Usherwood
  • 3,082
  • 1
  • 28
  • 35

4 Answers4

2

To recreate the tree at temp, there are (at least) two fairly simple ways: One is

git checkout temp -- .

and the other is

git diff temp | git apply --index

After that, you create the commit with the same commit message using

git commit -C temp

The two options each having their pros and cons. The checkout route does not remove files that are not present in temp anymore. The apply route does not work with binary files.

Side note: With modern Git you would use git restore instead of git checkout, but I'm and old-timer and haven't learnt to use it.

j6t
  • 9,150
  • 1
  • 15
  • 35
  • If there are files to be removed, then `git checkout tmp -- .` won't suffice. A general solution is to `git rm -- .` first if there is any doubt. – Mark Adelsberger Jan 16 '21 at 18:12
  • Thanks, that's great - I think this is the kind of answer I was expecting to get, something simple I was just missing. The first suggestion seems logical & easy to understand the limitation about not staging file removals. – Luke Usherwood Jan 17 '21 at 23:04
1

j6t's answer has some reasonable options, but I prefer a simplified version of the procedure you yourself suggest. (I'm not sure what errors you're afraid of making, but I suspect they arise from the fact that you're taking unnecessary steps.)

git checkout temp
git reset --soft my_topic
git checkout my_topic
git commit -C temp
git branch -D temp

This may seem like a strange preference, since git checkout -- . (one of their suggestions) looks like a one-liner; but note my comment on their answer. And their are other subtle gotchas that can creep in - like what if you forget you're not at the worktree root. You can work around all of those, of course...

So you can either be in the habit of doing git rm first, or "know" when you need to (an opportunity to make mistakes). And you can use :/: instead of . as a matter of habit (and get used to explaining it to everyone you work with), or always double-check that you're in the root (another opportunity to make mistakes). And then you get

git rm -- :/:
git checkout temp -- :/:
git commit -C temp
git branch -D temp

which is only one simple command "less" than the solution I'm suggesting.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
  • Well this question is turning into a veritable gold mine of info. Thanks for improving my "incantation" & providing the extra insights. So is the main benefit of `:/:` to ensure the complete even when executed from a sub-directory? Or are there other differences too? – Luke Usherwood Jan 17 '21 at 23:14
  • 1
    @LukeUsherwood Nothing else; `:/:` is just a path prefix that always means "the root of the work tree" – Mark Adelsberger Jan 18 '21 at 00:09
0

Forget about your temp branch. Instead of reset hard, reset mixed.

The result is that the index is reset but the working tree is not. Now you can add and commit in any combinations you like, and then just add everything else and commit to finish up.

See my https://stackoverflow.com/a/59675191/341994 for more.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • No, I do actually want to throw away my work and create brand new commits. (Not keep my work and commit bits of it.) They might include things done differently from the final commit, taking a different path to get there - e.g. to help tell a clean "story" to a code reviewer. Since reset moves the branch pointer I can't see how reset-mixed achieves this? Maybe steps would help. – Luke Usherwood Jan 16 '21 at 08:57
0

The direct route to a straight import of another commit's snapshot is the core command to do exactly that:

git read-tree -u temp

followed by

git commit -C temp

to commit that with temp's commit message.

git read-tree is what underlies almost every command that updates the index and work tree from existing repo contents. git reset is some option-picking logic around git update-ref HEAD and git read-tree; git checkout is also some option-picking logic around git update-ref HEAD and git read-tree; git merge is some fairly heavy-duty cleanup work on the results of a git read-tree ... yeah. By the way, git commit is some option-picking logic around git update-ref HEAD git write-tree and git commit-tree (which latter adds a single tiny object to the object db).

jthill
  • 55,082
  • 5
  • 77
  • 137
  • Perfect! Note using `-u` by itself gives an error, but following the breadcrumbs I changed that to `-mu` and then it work as advertised. I've confirmed this also stages file removals (as expected). – Luke Usherwood Jan 17 '21 at 23:32
  • I'll accept this answer as it is a perfect match for what I was asking for. The other answers have all been very insightful too, for example this command cannot take path-patterns so if for some reason I wanted to restore just a sub-directory the other answers will be a great reference for me. – Luke Usherwood Jan 17 '21 at 23:36
  • Yah, I have to confess I left the `-m` / `--reset` choice as an exercise for the reader intentionally. Glad you found the ticket. – jthill Jan 18 '21 at 00:08
  • 1
    btw, you *can* read subdirectories, with `--prefix` you can move them around arbitrarily. `git read-tree -u --prefix=read/to/here/ somecommit:from/there`. – jthill Jan 18 '21 at 00:11
  • Ah-ha! Another great tip. I had tried a few things: without `--prefix` I found paths were rejected, and with `--prefix` I made a huge mess :-) (duplicating everything). Now I see the point & intended usage. – Luke Usherwood Jan 18 '21 at 00:47
  • Pleasure doing business with you. – jthill Jan 18 '21 at 00:54