It won't capture file removals, for instance:
$ git checkout draft_feature_a $(git diff --name-only master draft_feature_a)
error: pathspec 'ttt.py' did not match any file(s) known to git.
You can do the same thing with an actual rebase-and-squash-etc (I used "fixup" below, pretty much the same thing, really; the original commits are all still viewable on draft_feature_a
):
$ git checkout -b draft_feature_a
[work, commit, etc - I made two commits: add newfile and rm ttt.py]
$ git checkout master
Switched to branch 'master'
$ git checkout -b feature_a
Switched to a new branch 'feature_a'
$ git merge draft_feature_a
Updating 523bacb..3a486fc
Fast-forward
newfile | 0
ttt.py | 14 --------------
2 files changed, 14 deletions(-)
create mode 100644 newfile
delete mode 100644 ttt.py
git rebase -i master
[edit to make everything but the first a "fixup"]
...
2 files changed, 14 deletions(-)
create mode 100644 newfile
delete mode 100644 ttt.py
Successfully rebased and updated refs/heads/feature_a.
$ git commit --amend
[edit commit message]
I actually use workflows like this a lot, although most of the time I'll just "git rebase -i" the draft work. Note that the original commits are available whether or not you make a separate "draft" branch, they just lose their name and you have to dig through the reflog to find the commit ID. You can add a new name instead of making a new branch and "git merge"-ing:
(first, let's clean up the previous example)
$ git checkout master
Switched to branch 'master'
$ git branch -D draft_feature_a
Deleted branch draft_feature_a (was 3a486fc).
$ git branch -D feature_a
Deleted branch feature_a (was 0fc36f0).
(now a new example)
$ git checkout -b feature_a
Switched to a new branch 'feature_a'
[work work work]
[time to clean up, let's stick a label on this version so I can find it easily:]
$ git branch messy_feature_a feature_a
$ git rebase -i master
Once the rebase is done, with everything squashed, fixed-up, rearranged, commit message(s) edited, etc., if I decide I screwed something up, all my "draft" (low-quality/messy) work is still available by name under the "messy" name. When I'm satisfied, and don't want the old name, I delete it manually (git branch -D
).
The trick to understanding this is that every time you do stuff in git, you only ever add new commits. The old ones stick around in your repo until (eventually) you do something implicit or explicit to "garbage collect" them. As long as they have a branch label name (or some other "visible" name, like a tag) that makes the commits name-able by something other than the 3a486fc
style SHA1 names, they last "forever". Deleting a branch simply erases the label. After a month or three, the unlabeled commits are finally garbage-collected. (More precisely, it still has a name in the reflog, until that expires: reflog entries have a time limit. See the documentation for git reflog
, especially the --expire=<time>
parameter.)
Similarly, a "rebase" makes a new series of commits, leaving all the old commits in there. When the rebase is done git peels the label off the end of the old series of commits and pastes it onto the end of the new series of commits:
A -- B -- C [label: master]
\
D -- E [label: feature_a]
[rebase feature_a on master, and squash or fixup to make commit DE which is D+E]
A -- B -- C [label: master]
| \
| D -- E [no label!]
\
DE [label: feature_a]
If you add an extra label before doing the rebase, the current-branch feature_a
label is peeled off and moved to the new commit(s), but the other label (messy_feature_a
) sticks around and gives you easy access to commit E, and hence the entire chain (D and E branching off C).