Recovering from "(no branch)" during a git rebase

I was using git rebase to merge changesets from one subversion repository onto code from a completely different repository. Figuring out multiple svn-remotes and the attendant issues was fun1, though once I figured out I did want to rebase, which onto what, how to delete a remote branch, and how to use rebase --interactive to edit git svn’s ported commit messages, it worked pretty well. But that’s not important.

During the rebase, I ended up off the branch I was trying to do the merge “in,” where git branch said I was on (no branch). However, like everyone else on the internet, I blithely continued past that point. How do I recover from (no branch) without losing the completed rebase?

mpaschal@mpaschal-mt:~/svn/mt/git$ git branch -a
* (no branch)
  atompub
  master
  git-svn
  github/master
  trunk

Of the search results I consulted, Mark Guzman best described what happened:

Little did I know that I had entered the “(no branch)” state. At this point I was pretty much gunning to create orphaned blobs, commits and other such items.

So (no branch) is when HEAD is a commit that is no longer one of the leading branch commits (which is what it means to be “on a branch”). If as in this case I want atompub to match HEAD, I have to git merge the outstanding HEAD commits over to atompub. That works fine since the new commits are changesets atop atompub in the first place; it’s as though I’m on a branch of atompub, only I never explicitly branched, so it doesn’t have a name. Instead I have to refer to it by the commit ID.

As I hadn’t switched back to the real branch yet, I didn’t have to recover my nameless HEAD commit the way Mark did. Instead I could look directly at the log:

mpaschal@mpaschal-mt:~/svn/mt/git$ git log -1 --pretty=oneline
658b8173ab396c7bb765f990c2bc2fdc7d639c86 Merged all ...

Then switch and merge:

mpaschal@mpaschal-mt:~/svn/mt/git$ git checkout atompub
Switched to branch "atompub"
mpaschal@mpaschal-mt:~/svn/mt/git$ git merge 658b817
Updating 461f2f7..658b817
Fast forward
  ...

It’s like magic!2

1 Not actually fun.

2 It’s a long incantation you have to research to discover, and getting it wrong can have disastrous consequences.

“No such file or directory” checking out gitosis-admin repo (git-shell not on path)

Am I the only person who’s found git 1.6.0 doesn’t work with gitosis 0.2? I found a couple references to this error trying to check out the gitosis-admin project, but no solutions—at least not the one I had to make up.

mpaschal@hostname:~$ git clone git@hostname:gitosis-admin.git
Initialized empty Git repository in /home/mpaschal/gitosis-admin/.git/
Traceback (most recent call last):
  File "/usr/local/bin/gitosis-serve", line 8, in <module>
    load_entry_point('gitosis==0.2', 'console_scripts', 'gitosis-serve')()
  File "/usr/local/lib/python2.5/site-packages/gitosis-0.2-py2.5.egg/gitosis/app.py", line 24, in run
    return app.main()
  File "/usr/local/lib/python2.5/site-packages/gitosis-0.2-py2.5.egg/gitosis/app.py", line 38, in main
    self.handle_args(parser, cfg, options, args)
  File "/usr/local/lib/python2.5/site-packages/gitosis-0.2-py2.5.egg/gitosis/serve.py", line 192, in handle_args
    os.execvp('git-shell', ['git-shell', '-c', newcmd])
  File "/usr/local/lib/python2.5/os.py", line 353, in execvp
    _execvpe(file, args)
  File "/usr/local/lib/python2.5/os.py", line 389, in _execvpe
    func(fullname, *argrest)
OSError: [Errno 2] No such file or directory
fatal: The remote end hung up unexpectedly
mpaschal@hostname:~$

gitosis sets up so it runs git-shell as the git user’s login command, but git 1.6.0 apparently installs everything in the “gitexecdir,” which by default ends up /usr/local/libexec/git-core. Which of course is not on the user’s path. Normally that’s fine since you write git command and /usr/local/bin/git rewrites that as git-command, and /usr/local/bin/git knows exactly where all those are, but that means you can’t yourself run git-shell directly.

The gitosis directions I was looking at also suggest making /bin/sh the git user’s shell, which means .bash_profile isn’t evaluated on login. So I couldn’t just add /usr/local/libexec/git-core to git’s PATH (even if I put it in regular .profile—dunno if I was goofing it up somehow, or if ssh default commands don’t start the shell first, or what).

I gave up and copied git-shell to /usr/local/bin, where regular git lives.

git equivalent to “svn copy” for forking files with history?

As might be obvious from my previous post, I don’t yet grok git. (It took three tries over about a week to figure out what I posted there.) My other major question as a dual git/subversion user is how do I svn copy in git? I don’t see that adequately answered anywhere.

The most common use of svn copy is to branch, which is precisely what I don’t mean here. git seems to promote branches to a first-order concept, in that the entire git repository exists across branches, and there are specific commands for branching and merging. You can’t have a nonstandard trunk/branch/tag hierarchy like you do sometimes in subversion, because there is no hierarchy. git branches are completely orthogonal to your file structure.

Git From the Bottom Up suggests (perhaps a little facetiously) phrasing your problem in git’s language in order to understand:

Understanding commits is the key to grokking Git. You’ll know you have reached the Zen plateau of branching wisdom when your mind contains only commit topologies, leaving behind the confusion of branches, tags, local and remote repositories….

In these terms, you find branches are really names for other commits besides the master head main trunk commit. It’s not really that branches are first-order things, but that branches are names for commits instead of files. Either way, they’re completely orthogonal to the filesystem.

Looking again just now for the answer to my question, I found this thread, which illustrates git’s current position on copying, and how it’s contrary to this second use of svn copy that I’m trying to figure out:

svn copy::
Duplicate something in working copy or repository, remembering history.
cp A B; git add B::
Git doesn’t have a direct equivalent of svn copy. It’s arguable whether it needs it once the user knows they can git-add so easily.

Git wins. Git’s ability to detect copies after-the-fact, mean that a git-copy isn’t necessary.

svn copy is more like git checout -b, i.e. it’s primary purpose is not to “copy” things, it is to create branches. You generally do not copy code (I hope).

Well, in fact, I often do copy code with svn, because I want to fork a file with history. Often I discover when I’m working on (say) some class, I’ve accreted unrelated functionality around the class’s real work, and I need to separate it out. Obviously I can do that just fine and check both parts in, but if I naïvely fork one file in twain—by doing a real file copy and adding the new file, say—the new one will lose all its history. The first commit git will know about for it is the one where it appears fully formed from Zeus’ head, though it happens to share an equivalent blob with some other file in the commit.

In git terms, you can see why it’s not obvious how to copy with history: to duplicate svn copy A B, you want git to understand when you ask about B’s history to include all of A’s previous commits. It’s as though you want to change all A’s commits retroactively to include B, sort of. It’s more about the behavior of the tools than anything you can articulate in a git repository’s data.

So I looked for behavior: lo and behold, Andy Parkin’s message above notes that git can “detect copies after-the-fact,” and I guess he means for example git log’s -C and --find-copies-harder options. According to the manual, this seems to be exactly the behavior I need:

-C
Detect copies as well as renames. See also --find-copies-harder.
--find-copies-harder
For performance reasons, by default, -C option finds copies only if the original file of the copy was modified in the same changeset. This flag makes the command inspect unmodified files as candidates for the source of copy. This is a very expensive operation for large projects, so use it with caution. Giving more than one -C option has the same effect.

If only it worked that way. See the terminalcast I did showing what I mean: once I duplicate the file, svn log doesn’t show fred’s initial commit in wilma’s history, even with the -C or --find-copies-harder flags.

I would be delighted to be wrong, but as far as I can tell, it’s not possible to fork files in git, while it’s trivial with svn copy.

Moving commits from git to subversion

It’s fairly well documented what to do if you have a subversion repository and want to develop on it with git. What if you have a project you started in a git repository, but now need to publish it to a subversion repo?

After several attempts and resets, this seems to be what you have to do to check a project you built with local git into an arbitrary place in a subversion repository.

  1. Make a new git repo: mkdir ~/import; cd ~/import; git init
  2. Make the new remote directory in the svn repo: svn mkdir http://example.com/proj/
  3. Link up with the empty path using git-svn: git svn clone -T '' http://example.com/proj/
  4. Add your original repo as a remote: git remote add dev file:///home/username/work/proj
  5. git pull dev master to pull in all the original git repo’s commits.
  6. git svn rebase to rebase all the commits on top of the svn commit.
  7. In my case, the rebase halted on some commits in the git history where I added files. I had to git add the files manually, then git rebase --continue.
  8. git svn dcommit (or with -n to check… but it’s just a list of commit IDs, so it sure didn’t make me feel that much better about doing it)

You can then really check it worked by comparing the subversion content to your git repo:

  1. cp -R ~/work/proj proj-git
  2. rm -r proj-git/.git
  3. svn export http://example.com/proj/ proj-svn
  4. diff -rub proj-svn proj-git

As usual in UNIX, silence is golden.