Skip to content

Instantly share code, notes, and snippets.

@sellout
Created October 27, 2022 05:00
Show Gist options
  • Select an option

  • Save sellout/3361145fac9bf2dfdc6a9bc18dcdff36 to your computer and use it in GitHub Desktop.

Select an option

Save sellout/3361145fac9bf2dfdc6a9bc18dcdff36 to your computer and use it in GitHub Desktop.
Git worktree layout

I use git worktrees a lot, and I’ve been annoyed by the repo itself having effectively a privileged worktree that’s always there (and refuses to let any other worktree check out the branch it’s on). And then not having a good place to put my other worktrees. If I clone to $PROJECTS/this-project, where do I put my worktrees?

  • If I put them in the directory, the worktree directories are scattered among all the files in the clone’s working tree (and I seem to remember git having some issues with worktrees inside a working tree), but
  • if I put them next to the directory, then the worktree names need to be prefixed with the repo name (and if there are repos foo and foo-charts, it’s easy to forget that foo-charts isn’t just a worktree of foo.

So, my projects tended to look like this:

$PROJECT_DIR
├── my-project                           # a repo
│   ├── .git
│   └── ...                              # but also a working tree
├── my-project-add-some-feature          # a worktree
│   └── ...
├── my-project-charts                    # a different repo, but with a worktree-looking name
│   ├── .git
│   └── ...                              # which also has its own working tree
├── my-project-charts-refactor-something # a worktree in the different repo
│   └── ...
└── my-project-fix-this-bug              # another worktree in the first repo
    └── ...

But now I have a new script for cloning a repo (abbreviated a bit here), which does this:

CLONE_DIR=$PROJECT_DIR/$REPO/repo
git clone $DOMAIN/$ORGANIZATION/$REPO $CLONE_DIR
cd $CLONE_DIR
git checkout $(git commit-tree $(git hash-object -t tree /dev/null) < /dev/null)

which is what I wish git clone --bare did, but doesn’t quite. Basically, you end up with a repo without a working tree. So there’s no branch locked by the repo, and no privileged working tree.

So then I end up with something like this:

$PROJECT_DIR
├── my-project
│   ├── add-some-feature  # a worktree
│   │   └── ...
│   ├── fix-this-bug      # another worktree
│   │   └── ...
│   └── repo              # the “bare” repo
│       └── .git          # the only thing in the “bare” repo
└── my-project-charts     # a different project with a worktree-looking name
    ├── refactor-something
    │   └── ...
    └── repo
        └── .git

It’s annoying to have that script for cloning, so I’ll probably try to get rid of it. But after the initial clone, I really like working like this.

@T3sT3ro
Copy link

T3sT3ro commented Apr 19, 2025

Yeah, I also don't have a clear answer to that. Workflow with git worktrees is as messy as the git CLI itself. Personally I use this structure:

<PROJECT NAME>
├── main                  # git clone <repo> main
│   └── .git              # regular repo
├── dev                   # one worktree
│   ├── .git              # file for worktree
│   └── ...
└── refactor-something    # yet another worktree
    ├── .git              # another file for worktree
    └── ...    

I don't liek this setup, but it gave m less headaches than a bare repo or nested worktrees. I remember having issues with external tools (for example lazygit) or more manual work with refs when I tried using bare repo, so I abandoned that approach.

@sellout
Copy link
Author

sellout commented Apr 27, 2025

@T3sT3ro Yeah, that’s what I’m doing these days, too.

Having the clone on main / trunk / master whatever is nice for me to have a place to just try to reproduce something that someone brings up in a conversation, without having to take the time to make a new worktree (and if that worktree hangs around, then I try to do the same thing again, it complains “you already have main checked out in some other worktree”). So the clone is sort of just a clean version that I keep in sync, and all actual work happens on the worktrees.

It’s not terrible, but I do have various tooling (e.g., Magit) that isn’t keen on this structure. I’ve had to redefine some things so that my-project/some-worktree gets identified with the project name “my-project” and not “some-worktree” … because it’s annoying when you’re looking at a bunch of different repos and they all have the name “main” because of this directory structure.

@sellout
Copy link
Author

sellout commented Feb 27, 2026

FWIW, I now use a very different approach (managed by a tool of my own called astrolabe).

$HOME
├── .local/state/astrolabe/clones
│   ├── buge                         # bare repo
│   ├── duoids                       #     〃
│   ├── pathway                      #     〃
│   ├── yaya                         #     〃
│   └── ...
└── Projects
    ├── community                    # broad “organization”
    │   ├── fix-directory-operations # single task (~ branch name)
    │   │   ├── buge                 # worktree for primary project of the task
    │   │   ├── pathway              # worktree for dependency of buge that needs changes
    │   │   └── yaya                 #                    〃
    │   └── ...
    ├── personal                     # another “organization”
    │   └── ...                      # tasks for that organization
    └── Some Client                  # another “organization”
        └── ...                      # tasks for that organization

This still uses bare repos (actually --bare now), but shifts my development to be task-centric, rather that repo-centric.

The bare repos are all hidden away as Astrolabe’s internal state, and each task directory has worktrees that reference those bare repos.

This helps with a few things

  1. the bare repos are effectively hidden away.
  2. I like to partition my work by whom it’s for (like, a client)
  3. but there are projects that I work on for multiple “organizations”, and my previous approach didn’t handle that well
  4. this assists with my preferred style of poly-repo development. I can have small projects, but still work on them as one1
  5. when one task is done, all of the worktrees associated with it are cleaned up together, rather than scattered around.

Footnotes

  1. Astrolabe does a bunch of stuff for me, like:

    • adding removing repos from a task;
    • letting me know which tasks are clean, or in what state;
    • cleaning up after finished tasks;
    • overriding dependencies so that all of the worktrees in a task depend on each other rather than their usual published dependencies, etc.; and
    • cleaning up bare repos that don’t have any worktrees or stashes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment