gitgit-remotegit-bare

How to restore a locally lost git bare repo (using separate-git-dir) with one of its remotes


A long time ago, I set up a repo using git --separate-git-dir because I did want the repo inside the working directory. Unfortunately, that separate (presumably "bare"?) repo has been lost to hard drive failure, and I want to rebuild it using the contents of a remote I had added (which was pushed to frequently).

How do I do that?

I tried by recreating a new, empty git repo:

git init --separate-git-dir=/desired/path/to/bare/git/repo

This of course creates a .git file in the working directory with the contents, gitdir: /desired/path/to/bare/git/repo

Then, I did:

git remote add network ssh://host/path/to/repo
git fetch network

But, if I run git status, I get

On branch master

No commits yet

What I'd like to get

I only ever used (and pushed) the master branch. I am hoping to get this repo to a state WITHOUT MODIFYING THE WORKING DIRECTORY where I can type git status and hopefully it only sees the changes between the current, unmodified working directory and the last commit pushed to the remote, which should now be in my local bare?

I'm really just trying to pick up where I left off.

Thank you.


Solution

  • TL;DR: what remains is to create a master branch, probably with git branch master network/master (perhaps with various flags; see below), and fill in the index, probably using git read-tree. Hence:

    git branch master network/master
    git read-tree master
    

    is probably sufficient.


    Let me turn my comments above into an actual answer now that I have a bit of time to spare. We've noted that --separate-git-dir doesn't make the repository bare—a bare repository is one with no work-tree—it just puts the work-tree and repository (.git/* files) in two different locations in the file system. To make this function, Git adds a .git file in the work-tree, containing the path name of the repository itself, as you noted.

    So, in this case, that repository itself has been damaged or destroyed. You have a work-tree with no repository. You've created a new, empty repository using another git init --separate-git-dir=... and run the commands:

    git remote add network ssh://host/path/to/repo
    

    (I would probably have named the remote origin rather than network but there's nothing wrong with network, it's just a bit unconventional.)

    What you needed to do next is populate the repository itself with commits, and you did that:

    git fetch network
    

    Your repository itself (in whichever directory) now has some set of commits, copied from the Git repository at the URL at which you ran git fetch.

    At this point almost everything is normal. Two things are missing:

    If you were to make a commit now, that would create a commit with no parent commit, and with no content (the empty tree), and create the branch master in the process, but that's pretty clearly not what you want. Instead, you probably want:

    1. To create the local name master to match the remote-tracking name origin/master that points to the final commit of the branch master in the network repository from which you copied all its commits. To do that, use:

      git branch master network/master
      

      You can use anything you like as the final argument, as long as it names any actual commit: a raw hash ID will suffice, or something like network/master^, or network/master~3, or whatever. If you do use the name network/master, Git recognizes that this is a remote-tracking name.1 As a consequence, git branch sets this remote-tracking name as the upstream of the new (local) branch name master.

      More precisely, the automatic upstream setting is the default action for git branch here (and for some forms of git checkout as well). You can configure Git to change the default, or you can add command line flags to override whatever default you have or have not set.

      To prevent this upstream-setting, use git branch --no-track master network/master. To force the upstream-setting, use git branch --track master network/master, which you can abbreviate as git branch --track network/master.

      Whether and when to set the upstream setting is up to you. You can always change it later using git branch --set-upstream, or remove it later using git branch --unset-upstream.

    2. Now that master exists—or you can do this before creating master, but I'd do it afterward as it just feels simpler and easier to get right—you will want to populate your index from the commit you've selected as your current commit. Normally—in a situation other than this "repair broken Git repository" case—we'd do steps 1 and 2 all together at once using git checkout, but you want to do this without disturbing the current work-tree, and git checkout disturbs the current work-tree. So we do this as a separate step 2, using one of Git's lower level plumbing commands:2

      git read-tree master
      

      (or git read-tree HEAD or git read-tree @ if you want to type less: all three do the same thing). This simply reads the named commit into the index, without doing anything else at all: it replaces whatever was in the index (which was nothing) with whatever is in the named commit.

    After doing the git branch and git read-tree, git status will be able to compare the current commit—the one named by HEAD / master—with the current index contents—they will match of course—and then compare the current index contents with the current work-tree contents. These will match or differ based on which commit you chose when setting up your own master in step 1, and in any case you'll be ready to git add and make new commits if you like.


    1A remote-tracking name is any name whose full spelling starts with refs/remotes/. In this case network/master is really refs/remotes/network/master. Git documentation mostly calls these remote-tracking branch names but the word "branch" here is, I think, more misleading than not, so I omit it.

    These names exist in your Git repository and are automatically created and updated based on the branch names that your Git gets from the other Git—the one at the URL stored under the name network—whenever your Git does a git fetch from the Git at network. A successful git push network also updates any branches that they set based on your Git's requests.

    2The distinction between plumbing and porcelain in Git is really in terms of who is meant to use a command: a porcelain command is meant to be user-friendly and goal-oriented,3 and a plumbing command is meant to be a command that a porcelain command might use as one of a series of such commands (and/or in combination with other system utilities) in order to accomplish some actual goal. Thus, plumbing commands tend to be extra-specific and mechanism-oriented (git read-tree, "fill index from commit"; git rev-parse, "turn human-readable name into internal hash ID"; git update-ref, "write raw hash ID into arbitrary reference").

    3Or perhaps "less actively user-hostile". :-) See also https://git-man-page-generator.lokaltog.net/