4

Suppose I have a git repository, working on some branches, and at some point I want to have no branch checked-out. Can I do that?

In other words, can I get the effect of git clone path-to-repo --no-checkout, but on an existing repository?

Edit: Actually, it would be much better if the master branch files would not show up as deleted. So, the ideal answer would not be quite like git clone with --no-checkout.

Note: If your suggestions require a recent version of git, please say so.

einpoklum
  • 118,144
  • 57
  • 340
  • 684

2 Answers2

6

This is just an expansion of Schwern's answer, which is correct—but you may need to add a git rm -r of all files, and maybe a git clean as well. There is also one corner case where you can do nothing at all, and maybe you don't really want to remove files.

Long

There are several possible answers to this, depending on what you're really looking for. Let's start with some background and definitions:

  • A new, totally empty repository has no commits. A branch name can only exist if it identifies a commit and with no commits there are none to identify. So this repository has no branches. Curiously, you're still on a branch. It's just that the branch that you are on does not exist.

    In this kind of repository you have only one option, which is be on an "orphan branch". We'll come back to this in a moment.

  • Otherwise—when there are commits—you can have any number of valid, existing branch names, each of which must point to some existing, valid commit. You can be on exactly one of these branch names, or you can be in detached HEAD mode, or you can use the "orphan branch" mode.

The orphan branch mode is the oddest of the three, but let's describe it first, then the other two:

  • Orphan mode: HEAD contains a branch name. This is the current branch, so you are on a branch. The odd part is that the branch you are on does not exist. A git branch command will list the branch names that do exist, but not the branch name that you are on, because it does not exist.

  • Normal mode: HEAD contains a branch name. That is the current branch, and the commit hash ID stored in that branch name is the current commit.

  • Detached-HEAD mode: HEAD contains a commit hash ID. That is the current commit; there is no current branch.

A git clone normally results in normal mode. The branch name that becomes the current branch is the one you select at git clone time, with your -b option. There are some exceptions to this rule, though: if your -b names a tag, Git will check out that tag, putting you in detached-HEAD mode. If you don't specify a -b option, your Git asks the other Git what branch name they recommend, and uses that name, or a fallback name; if that name or the fallback name fail to name a branch, you wind up in orphan mode, with a nonexistent branch as the current branch. If you do specify a -b option, the name must name an existing branch or tag in the other Git repository, otherwise the clone command as a whole fails.

The -n option has no effect on the mode of the new clone. You are in normal, detached-HEAD, or orphan mode exactly as you would be without it. The only effect the -n option has is that it prevents the initial git checkout. The branch name is still created locally, when using normal mode, so that if there are branches in the repository you've cloned, you will be on one of them, with that branch name existing locally and pointing to the same commit as the remote-tracking name. This is a weird special case that I'd argue is a minor bug, since if you ran git init, git remote add, and git fetch without doing a git checkout you'd be left in orphan mode instead. (It's the git checkout step that creates the branch, when using commands other than git clone itself, so skipping it should leave you in orphan mode. But it doesn't.)

Git's index and your working tree

The above is all about HEAD—what's stored in it, and which branch and/or commit is current, if any—and branch names. But when we look around in a non-bare repository, we see a bunch of files, stored as ordinary everyday files. These files are not what is in Git. What is in Git are the branch and tag and other names—as a sort of secondary database—and also a series of commits and supporting objects, stored in the primary (and usually much larger) database.

The commits function as archives and as history. Each commit stores a full snapshot of every file, as if in a tar or rar or zip archive. Each commit also stores some metadata, including the name of the person who made the commit, and so on. Everything in these archives is strictly, totally read-only: inherently so, because it's all addressed by numbers produced via a cryptographically-strong hash function. Any attempt to change any stored data results in incorrect hash values,1 which Git detects, and reports as a corrupted repository.

But with no ability to change these files—or even view them, in most programs—these archives would be useless. So Git will extract an archive into a working tree: an area where you can see and use your files. This is—at least initially—what you have in your working tree: the result of extracting some commit. The commit that is extracted is the current commit.

Technically, this is all we need: the commits as archives-and-history, and a working copy. When we consider these as "the current commit" and "the working tree copy", that's two copies. Those are all there are in some version control systems. But for whatever reasons, Git inserts a third copy of each file, in between the frozen-for-all-time current-commit copy and the readable, writable, useful version in your working tree. This makes the working tree copy become the third copy, as it were: the second copy of each file exists in Git's index.

The format of the files in Git's index is that of files in commits: they're pre-compressed and pre-de-duplicated. The files in the commit archives are all de-duplicated, which generally saves tons of space. One trick that Git uses to go fast here is that the index copies are pre-de-duplicated, so that there's no work needed at commit time. What this means here is that the index copies of files take almost no space. For instance, the expanded-out files for the Git project for Git occupy about 57 MiB on one of my machines here, but the index needed to hold those same files is only 368790 bytes. (Note: neither of these numbers counts the .git directory.) But in principle there are three copies of the commit: HEAD—the commit itself—plus the index plus the working tree copy.


1Unless, that is, you can spend enough compute time to produce hash collisions. See How does the newly found SHA-1 collision affect Git? Note that this does not happen by accident, and it's impractical for most groups to do it even on purpose today (though it's no longer beyond the abilities of large companies like Google, nor various nation-states).

How all this relates to git clone -n

When you use git clone -n, you get one of the three modes for HEAD: orphan branch, detached HEAD, or normal. But Git does not run git checkout, and it's git checkout that fills in Git's index and your working tree. So you have a nominally-empty index and working tree.2

Hence, if you wish to reproduce this condition exactly, you will need to:

  1. determine what kind of HEAD setup to use; and
  2. empty out the index and working tree.

For simplicity in part 1, you can simply assume normal mode and do nothing. For simplicity in part 2, you can use git read-tree --empty, which erases the index, followed by git clean with various options. You can use git read-tree --empty -u to remove all indexed files, leaving only untracked files behind in the working tree. Or you can choose to leave the working tree alone.

If you wish to reproduce a detached HEAD (complicating part 1 a bit), you have two choices:

  • run git checkout --detach or git checkout of anything that is not a branch name, or
  • with Git 2.23 or later, run git switch --detach with any commit specifier.

The specified commit (or the HEAD commit, when using git checkout --detach with no arguments) becomes the current commit and you are now in detached-HEAD mode. The commit you check out here (or switch to with git switch) will fill Git's index and update files in your working tree, except for the special cases outlined in Checkout another branch when there are uncommitted changes on the current branch.

To get into orphan mode, use either git checkout --orphan or git switch --orphan. Be aware of this sneaky incompatibility: the old checkout method leaves Git's index and your working tree undisturbed. The git switch command empties the index and cleans up your working tree as if with git read-tree --empty -u.

(In all cases, untracked files are undisturbed, whether or not those untracked files are also ignored.)


2An empty index is a non-zero-length file as the index has headers and trailers. These contain cryptographic hashes, so as to detect on-disk corruption, just as the repository object database does. To make this work conveniently, Git treats a non-existent index as "empty", and creates the empty index in memory, and will write it with correct checksums once it becomes appropriate to do so.

The top level of the working tree normally contains the .git directory that is the repository proper, so that an "empty" working tree is never quite empty. You can, however, split the repository and working tree directories apart with various options.


Final notes and conclusion

Whatever mode you go into, note that running git commit will now try to create a new commit as usual:

  • In orphan mode, this will create a new commit with no parent (a new root commit) and create the branch whose name is in HEAD. The branch now exists, and holds one root commit, not connected to any other commit via the commit graph.

    (It's probably a bad idea to do this while concluding a merge. I don't know what happens if you try this.)

  • In detached-HEAD mode, this will create a commit with the usual parent (the current commit as parent, plus any additional commits from an in-progress merge). Git will then store the new commit's hash ID in HEAD, which continues to be detached, but now points to a commit that can only be found via HEAD.

  • In normal mode, this will create a new commit as usual (including concluding a merge as usual, just like with the detached-HEAD mode), and then store the new commit's hash ID in the branch name stored in HEAD.

The new commit will store, as its snapshot, whatever files are in Git's index. If you emptied the index, that's an empty tree. If you left or put files in the index, those are the files in the snapshot.

There's likely no real reason to do any of this, but the safest of these various modes is probably the new-orphan-branch mode with an empty index and empty working tree. That way, no one will accidentally git commit a new empty tree on some existing branch. The simplest of these is probably the detached-HEAD mode; this way, you can clean out the index and working tree, or not, without too much concern.

torek
  • 448,244
  • 59
  • 642
  • 775
  • 1. See my clarifying edit. 2. I _think_ what I would like, based on your description, is an orphan commit with no files in it, so that `git status` doesn't complain about anything being deleted; and no branch is current. 3. One needs to name the orphan branch when using `git checkout --orphan`, at least my version of git (2.20.1). – einpoklum Mar 24 '21 at 23:44
  • Yes, `git switch --orphan ` (or the same with `git checkout` but then you need the `git rm` or `git read-tree --empty` trick). Note that `git switch` was new in 2.23. – torek Mar 25 '21 at 01:09
  • Thanks, `git checkout --detach ...` did it for me – Meir Gabay Oct 19 '21 at 16:15
2

Yes, delete the files.

git clone --no-checkout is still on a branch, it just has no files.

Schwern
  • 153,029
  • 25
  • 195
  • 336
  • Well, I see what you mean, but actually, that's not quite what I want. I mis-spoke when I said I want to have the exact effect of `git clone --no-checkout`; see clarifying edit. – einpoklum Mar 24 '21 at 23:46
  • 1
    @einpoklum You can't. Unless the repository is bare or empty, afaik there's always a checked out commit. You could do something goofy like make an orphaned commit with no files and check that out. Perhaps explain what you're trying to accomplish? – Schwern Mar 25 '21 at 01:44
  • @Schwern What he is trying to accomplish is pretty simple: return to the initial empty directory state because the files are not needed yet and/or the space is needed. Whatever the reason, returning to the empty working copy post clone is a desirable need. – Laurent Giroud May 13 '22 at 01:50
  • @LaurentGiroud What process needs this? – Schwern May 13 '22 at 02:17
  • @Schwern I am sure you can find some if you think about it for 10 minutes. ;) Let's say I am working in a number of repositories, using detached heads to examine their content and that I choose the convention that an empty working copy is a "done" one. Or I just need the disk space. Or I do not want to have to bother checking git status to know if I am currently working on a repo, if the working copy is empty, no command to run. – Laurent Giroud May 13 '22 at 02:32
  • @LaurentGiroud That all seems very tortured. – Schwern May 13 '22 at 16:08
  • @Schwern reclaiming disk space or wanting a fresh state does not seem extremely tortured but to each their own. – Laurent Giroud Mar 06 '23 at 00:59