1

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.

Hugo y
  • 1,421
  • 10
  • 20
jaydisc
  • 369
  • 2
  • 11
  • `fetch` does not add commits to the master. You'd better `pull` instead. – Serge Oct 04 '19 at 12:03
  • 1
    Not all that important to the problem, but a Git repository initialized with `--separate-git-dir` is *not* a bare repository, it's just one where the work-tree is at point A and the repository is at point B. – torek Oct 04 '19 at 15:54
  • 1
    The main problem here is that the *index* is in the repository as well, so you've created a new, empty repository, then populated it with some commits (found by the name `network/master`), but you still have no `master` and the index of your new empty repository is still empty. You should be able to, at this point, run `git branch master network/master` to create the (local) branch name `master` pointing to the same commit as `network/master`. This will by default set the upstream for `master` to `network/master` too; add `--no-track` to prevent that if you don't want it, or [continued] – torek Oct 04 '19 at 15:57
  • 1
    ... or unset it afterwards. Before or after that, fill in the index from this commit using `git read-tree` and any name or hash ID that represents this commit. For instance, `git read-tree master` after creating the name `master`, or `git read-tree network/master` at any time. After doing both of these you will have a local branch named `master` and a populated index, and `git status` will have three things to compare: a commit found via HEAD, a populated index—these two will match—and your existing work-tree. – torek Oct 04 '19 at 15:59
  • @Serge, if I pulled, it would update the working directory. – jaydisc Oct 06 '19 at 04:34

3 Answers3

2

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:

  • There are no local branches, in spite of the current branch being the one named master. The branch master doesn't exist.

  • The index—the collection of files that would go into the next commit you would make—is empty.

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/

torek
  • 448,244
  • 59
  • 642
  • 775
1

I think this is the best way:

The commands you entered is a good start:

git init --separate-git-dir=/desired/path/to/bare/git/repo
git remote add network ssh://host/path/to/repo
git fetch network

Now, you have a new repository, and your work tree remains completely unchanged. Git creates a branch named "master" automatically. The branch network/master contains all the commit history. But your local master is an orphan..no commits on it. The solution is to point your local master to the network master:

git reset network/master

Your local working tree remains unchanged...but your index and HEAD are now identical to network/master. Git status and git diff should show your changed files. You can now git commit -a or git commit add and git commit.

David Sugar
  • 1,042
  • 6
  • 8
  • Note that --separate-git-dir=/desired/path/to/bare/git/repo does not actually create a bare repository. You are simply telling git to create the repository for your worktree somewhere other than the default location. – David Sugar Oct 06 '19 at 16:06
  • This looks great and along the lines of what I was thinking, but I already applied @torek's solution which worked perfectly. Thank you. – jaydisc Oct 08 '19 at 00:06
0

torek mentions in his answer:

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 be even clearer, you could not use --bare with --separate-git-dir anyway, considering you want to get a working tree, that you then need to populate (git switch -C master should be enough)

With Git 2.29 (Q4 2020), the purpose of "git init --separate-git-dir(man)" is to initialize a new project with the repository separate from the working tree, or, in the case of an existing project, to move the repository (the .git/ directory) out of the working tree.
It does not make sense to use --separate-git-dir with a bare repository for which there is no working tree, so disallow its use with bare repositories.

See commit ccf236a (09 Aug 2020) by Eric Sunshine (sunshineco).
(Merged by Junio C Hamano -- gitster -- in commit a654836, 24 Aug 2020)

init: disallow --separate-git-dir with bare repository

Signed-off-by: Eric Sunshine

The purpose of "git init --separate-git-dir(man)" is to separate the repository from the worktree.
This is true even when --separate-git-dir is used on an existing worktree, in which case, it moves the .git/ subdirectory to a new location outside the worktree.

However, an outright bare repository (such as one created by "git init --bare"(man)), has no worktree, so using --separate-git-dir to separate it from its non-existent worktree is nonsensical. Therefore, make it an error to use --separate-git-dir on a bare repository.

Implementation note: "git init"(man) considers a repository bare if told so explicitly via --bare or if it guesses it to be so based upon heuristics.

  • In the explicit --bare case, a conflict with --separate-git-dir is easy to detect early.
  • In the guessed case, however, the conflict can only be detected once "bareness" is guessed, which happens after "git init"(man) has begun creating the repository.

Technically, we can get by with a single late check which would cover both cases, however, erroring out early, when possible, without leaving detritus provides a better user experience.


With Git 2.29 (Q4 2020), "git worktree"(man) gained a "repair" subcommand to help users recover after moving the worktrees or repository manually without telling Git.

Also, "git init --separate-work-dir=<path>"(man) no longer corrupts administrative data related to linked worktrees.

See commit 59d876c, commit 42264bc, commit b214ab5, commit bdd1f3e (31 Aug 2020), and commit e8e1ff2 (27 Aug 2020) by Eric Sunshine (sunshineco).
(Merged by Junio C Hamano -- gitster -- in commit eb7460f, 09 Sep 2020)

init: make --separate-git-dir work from within linked worktree

Reported-by: Henré Botha
Signed-off-by: Eric Sunshine

The intention of git init --separate-work-dir=<path>(man) is to move the .git/ directory to a location outside of the main worktree.

When used within a linked worktree, however, rather than moving the .git/ directory as intended, it instead incorrectly moves the worktree's .git/worktrees/<id> directory to <path>, thus disconnecting the linked worktree from its parent repository and breaking the worktree in the process since its local . git(man) file no longer points at a location at which it can find the object database.
Fix this broken behavior.

An intentional side-effect of this change is that it also closes a loophole not caught by ccf236a23a ("init: disallow --separate-git-dir with bare repository", 2020-08-09, Git v2.29.0 -- merge) in which the check to prevent --separate-git-dir being used in conjunction with a bare repository was unable to detect the invalid combination when invoked from within a linked worktree.
Therefore, add a test to verify that this loophole is closed, as well.


With Git 2.31 (Q1 2021), "git worktree repair"(man) learned to deal with the case where both the repository and the worktree moved.

See commit cf76bae (21 Dec 2020) by Eric Sunshine (sunshineco).
(Merged by Junio C Hamano -- gitster -- in commit 8664fcb, 06 Jan 2021)

worktree: teach repair to fix multi-directional breakage

Signed-off-by: Eric Sunshine

git worktree repair(man) knows how to repair the two-way links between the repository and a worktree as long as a link in one or the other direction is sound.

For instance, if a linked worktree is moved (without using git worktree move(man)), repair is possible because the worktree still knows the location of the repository even though the repository no longer knows where the worktree is.
Similarly, if the repository is moved, repair is possible since the repository still knows the locations of the worktrees even though the worktrees no longer know where the repository is.

However, if both the repository and the worktrees are moved, then links are severed in both directions, and no repair is possible.
This is the case even when the new worktree locations are specified as arguments to git worktree repair.

The reason for this limitation is twofold.

  • First, when repair consults the worktree's gitfile (/path/to/worktree/.git) to determine the corresponding <repo>/worktrees/<id>/gitdir file to fix, <repo> is the old path to the repository, thus it is unable to fix the gitdir file at its new location since it doesn't know where it is.
  • Second, when repair consults <repo>/worktrees/<id>/gitdir to find the location of the worktree's gitfile (/path/to/worktree/.git), the path recorded in gitdir is the old location of the worktree's gitfile, thus it is unable to repair the gitfile since it doesn't know where it is.

Fix these shortcomings by teaching repair to attempt to infer the new location of the <repo>/worktrees/<id>/gitdir file when the location recorded in the worktree's gitfile has become stale but the file is otherwise well-formed.

The inference is intentionally simple-minded.
For each worktree path specified as an argument, git worktree repair manually reads the ".git" gitfile at that location and, if it is well-formed, extracts the <id>.
It then searches for a corresponding <id> in <repo>/worktrees/ and, if found, concludes that there is a reasonable match and updates <repo>/worktrees/<id>/gitdir to point at the specified worktree path.
In order for <repo> to be known, git worktree repair must be run in the main worktree or bare repository.

git worktree repair first attempts to repair each incoming /path/to/worktree/.git gitfile to point at the repository, and then attempts to repair outgoing <repo>/worktrees/<id>/gitdir files to point at the worktrees.

This sequence was chosen arbitrarily when originally implemented since the order of fixes is immaterial as long as one side of the two-way link between the repository and a worktree is sound.

However, for this new repair technique to work, the order must be reversed. This is because the new inference mechanism, when it is successful, allows the outgoing <repo>/worktrees/<id>/gitdir file to be repaired, thus fixing one side of the two-way link.
Once that side is fixed, the other side can be fixed by the existing repair mechanism, hence the order of repairs is now significant.

Two safeguards are employed to avoid hijacking a worktree from a different repository if the user accidentally specifies a foreign worktree as an argument.

  • The first, as described above, is that it requires an <id> match between the repository and the worktree. That itself is not foolproof for preventing hijack, so:
  • the second safeguard is that the inference will only kick in if the worktree's /path/to/worktree/.git gitfile does not point at a repository.

git worktree now includes in its man page:

If both the main working tree and linked working trees have been moved manually, then running repair in the main working tree and specifying the new <path> of each linked working tree will reestablish all connections in both directions.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250