3

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 root with 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.

Nathan Basanese
  • 8,475
  • 10
  • 37
  • 66
  • 1
    if you goofed on the single, only commit in your repo. could you not use `git commit --amend`? i most likely misunderstand the base of your problem. – castis May 03 '16 at 16:12

2 Answers2

3

You can rebase on top of --root:

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>"
Joseph Silber
  • 214,931
  • 59
  • 362
  • 292
  • // , `git commit --amend --no-edit --author="...`, 'Twerked, entered as follows in the project's top level directory: `$ git commit --amend --no-edit --author="Nathan Bassanese "` However, `git rebase -i --root`, not so much. After I removed the commit in interactive rebase, it was still there. – Nathan Basanese May 03 '16 at 17:17
  • // , After I tried removing the commit's line in interactive rebase, I tried applying "fixup", to see if that would somehow remove the log entry: `[vagrant@localhost vagrant]$ git rebase -i --root` `Cannot 'fixup' without a previous commit` ... but it failed. Anyway, thanks for the thoughtful answer, but I don't think I can, in good conscience, mark this as accepted until I can successfully test it. – Nathan Basanese May 03 '16 at 17:20
  • // , Notably, the `git commit` command with the `--amend` and `--author` flags changed the commit in one branch, but not in another. I had to run it on each branch. – Nathan Basanese May 03 '16 at 17:26
  • // , Also, running `git rebase -i --root` without a commit leads to an interesting result. :) – Nathan Basanese May 03 '16 at 17:32
  • // , AHA, I think I rebased `master` back on to the `root`, with no tracked files, according to the first line of output from `$ git lg`: `* 6b64313 (HEAD -> master)`. However, I am now, apparently, the author of the root. At least, my contact info. shows up when I run `$ git log`. – Nathan Basanese May 03 '16 at 17:37
2

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.

  1. Branch names (like master) are moveable pointers that point to one particular commit.
  2. 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.)
  3. 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.
  4. Rebase works by doing repeated git cherry-pick operations, on an anonymous branch grown from the --onto target. (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.)
  5. A single git cherry-pick works 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.

Community
  • 1
  • 1
torek
  • 448,244
  • 59
  • 642
  • 775
  • // , I'm going to read this when I've had more sleep, but my first impression is that my difficulty removing the "first ever" commit comes from the contradiction between point #1 and point #3 above. Thanks for bringing the "empty tree" to my attention, and for the thoughtful answer. – Nathan Basanese May 03 '16 at 23:03
  • 2
    Git "solves" that by not letting the branch pointer move until there is a replacement commit, i.e., never actually removing that first ever commit. (In fact, except for `git gc` and its plumbing, git never removes *any* commits, or any other objects. It's build around *adding new stuff*, with no changes to any existing objects ever. This is from the nature of hashes, which are content sensitive.) When the plan is to start at existing commit `E` and add new cherry-picked commits, and your first pick result is supposed to be parent-less, you have a problem, though. :-) – torek May 03 '16 at 23:21
  • // , I guess a conclusive answer to whether I can, indeed, remove that first commit would be a solid "no", then? – Nathan Basanese May 04 '16 at 14:46
  • The thing with Git is that you never *remove* anything, you just make it *unreachable*. The nodes in the graph that are reachable (by starting from some external reference like a branch label) are shown, and the nodes that are not, are not. Using `--amend` does not change or discard anything, it just adds new stuff. Rebase does not change anything, it just adds new stuff. Even `git filter-branch` just adds new stuff: if you filter the entire repository, it *copies* the entire thing. Eventually, `git gc` removes *unreferenced* objects. (And, *cloning* copies only ref'd objects.) – torek May 04 '16 at 16:58
  • "To create any commit, we need to be on a branch". What does this mean? `git checkout && touch foo.txt && git add . && git commit -m "foo"` works fine. – Joseph Silber May 17 '16 at 03:50
  • @JosephSilber: the new commit is created on, in this detached-HEAD case, the (single, special) anonymous branch. It's true that this is not "on a branch" in `git status`'s phrasing, and calling it "being on the special anonymous branch" is a bit peculiar, but consider what you just said as well: `git checkout `: we must have an existing commit, and a new commit will have `` as its parent, which means the new commit will not be a root commit. (But I will add this as a footnote, at least.) – torek May 17 '16 at 09:26