How do I delete the first and only commit if it's the only commit in the branch?
I goofed the author info. up on the first commit.
However, it seems like most of the rebase or tree modification operations rely on some other commit already existing.
Even when I run Git’s interactive rebase command, all I see in my little list of commits is a single line, “noop”. :/ (Solved this by running my rebase against the Git tree’s
git rebase -i --root, but removing the line for the first commit I made did not actually remove it from the tree.)
Rebasing on top of
root, or the first commit, does not work.
[vagrant@localhost vagrant]$ git rebase -i HEAD~1 fatal: Needed a single revision invalid upstream HEAD~1 [vagrant@localhost vagrant]$ git rebase -i HEAD Unknown command: rnoop Please fix this using 'git rebase --edit-todo'. [vagrant@localhost vagrant]$ git rebase --edit-todo [vagrant@localhost vagrant]$ git rebase --abort
The following looked like a relevant possible alternative answer, but I think something more, or at least more specific, is worth an answer for this special case:
Delete commits from a branch in Git
This other answer (How do you undo the last commit?) resulted in the following:
[vagrant@localhost vagrant]$ git reset --soft HEAD~ fatal: ambiguous argument 'HEAD~': unknown revision or path not in the working tree. Use '--' to separate paths from revisions, like this: 'git <command> [<revision>...] -- [<file>...]' [vagrant@localhost vagrant]$ git reset --soft HEAD
Here, for those still reading this, is the vim buffer with
noop I mentioned earlier:
noop # Rebase 3d1c632..3d1c632 onto 3d1c632 (1 command(s)) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out
For those wondering why I don’t just edit the files and re-add them to the commit, or something, I had incorrect author information, which I corrected only after I had run “git commit“, and I changed the author information while I had the commit message open for editing.
Maybe I could try deleting the entire
.git folder, or something, but I’d prefer a more elegant way.
Plus, if I don’t delete the entire
.git folder, I keep my hooks and stuff, and I have a feeling that answers to this sort of question will speak to fundamental design principles in Git that I may not yet understand.
2 Solutions collect form web for “How do I delete the first and only commit if it's the only commit in the branch?”
You can rebase on top of
git rebase -i --root
To simply change the author on the last commit, use this:
git commit --amend --no-edit --author="The Name <email@domain>"
I see you have worked through the mechanics of fixing the one particular issue.
I will try to address your more general question, though:
… I have a feeling that answers to this sort of question will speak to fundamental design principles in Git that I may not yet understand.
Here are all the tricky bits involved in this particular process.
- Branch names (like
master) are moveable pointers that point to one particular commit.
- Each commit points to its parent commit(s). A root commit, by definition, has no parent. (Not relevant here but useful to know: a merge commit is simply defined as any commit with at least two parents.)
- To create any commit, we need to be on a branch,1 but to create a root commit, which has no parent, we need to have the branch have no commits—but see point #1: a branch name points to a commit.
- Rebase works by doing repeated
git cherry-pickoperations, on an anonymous branch grown from the
--ontotarget. (Once the repeated cherry-picking is done, the new anonymous branch tip commit replaces the original branch tip commit, i.e., we re-point the branch to the new tip.)
- A single
git cherry-pickworks by diffing the commit against its parent. For instance, if the parent has three files and the commit being picked has four, the commit added a new file. If just one of the three carried-over files is different as well, then whatever changed in that one file makes up the rest of that commit (well, the commit’s author and other metadata count too: those are also brought over in the cherry-pick). Having turned the original commit into a set of changes, Git then applies the same changes to some other (already-existing) commit and makes a new commit out of that. The new commit’s parent is the other commit.
These principles create a Gordian knot that
--root slices through. It’s also worth noting that
--root was new in Git version 1.6.2, and heavily modified in 1.7.12 (I vaguely recall using rebase before then and encountering some problems).
Git always had to solve the tension between #1 and #3 (the solution itself was improved and properly formalized at some point when
git checkout grew the
--orphan option). The trick here is that, when on what is called an “unborn branch”, Git records the current branch name in
HEAD as usual, but leaves the branch name itself un-created.
(Here it helps to know—although this is an implementation detail that you are not supposed to worry about—that the branch pointers are currently stored in one or both of two places. Git stores these name-to-ID mappings in small individual files in
.git/refs/ and/or a single flat text file,
.git/packed-refs. If a branch name is in neither place, but is in
HEAD, Git considers this an “unborn branch”.)
When making a new commit, if the current state is “on unborn branch”, Git makes a root commit. The new commit ID becomes the ID for the now-created branch and we are now out of the self-contradictory state of being on a branch, and yet not having a commit.
We still have the problem that
git cherry-pick works by comparing a commit to its parent. This is why, if you wish to cherry-pick a merge commit, you must specify which parent: a merge is a commit with two or more parents, so it is not clear which one to diff against. But what about a root commit, with all of its zero-count-’em-zero parents?
Git solves this particular problem by comparing a cherry-picked root commit against the empty tree. Since the empty tree is empty, every file is newly added, and this is the right result for the cherry-pick.
Hence, to rebase with
--root, we would like to create an orphan branch, then cherry-pick the root commit, and then all the rest of the commits, as usual. But
git rebase uses an anonymous branch. The only normal way to create an orphan branch is by name, but rebase uses “detached HEAD” mode. Moreover, each cherry-pick, including the first, needs to apply changes to an existing commit, but to create a new root commit, we must avoid using some existing parent.
The tricky internals for this are found (indirectly) in this answer from VonC, which links to this commit to the Git code. The rebase script makes a truly empty root commit (using the empty tree, and the
git commit-tree internal “plumbing” command that creates a commit object with user-specified parent IDs), then generates an internal “squash” operation to cherry-pick the existing root commit (with cherry-pick diffing against that same empty tree) and combine it with the dummy empty root commit.
(Here it also helps to note that a “squash” operation during rebase means “amend the previous commit”, a la
git commit --amend. The
--amend operation tells
git commit to make a new commit—the existing one cannot be changed—but to make the new commit’s parents match the existing commit’s parents. This has the effect of shoving the old commit aside: the branch now points to the new commit, which points to the “amended” commit’s parent(s). The amended commit is still in the repository, but unless there is some other way to find it, it is no longer visible.)
(This two-step trick means that the authorship information is handled specially.)
1For the purpose of this claim, at least, being in detached HEAD mode (after
git checkout hash or
git checkout --detach branchname, for instance) counts as being on the special anonymous branch, whose tip is the current commit’s hash.