Git Worktrees and GitButler
blog.gitbutler.com
blog.gitbutler.com
The workflow I do is
1. git clone https://path.to/repo.git
2. mv repo/.git repo.git && rm -rf ./repo && mkdir repo && mv repo.git repo/.git && cd repo
3. git config --bool core.bare true
Now you can just create worktrees in the root of the git repo.
This has the advantage that the `~/git/<project>` directory is still understood as root of the git repo and the worktrees are never checked out inside the working tree directory.
git clone --bare https://path.to/repo.git
mv repo.git .git
now we can create worktrees in the same folder.
> git clone --bare https://path.to/repo.git .git
But for this two-patch flow, I'm just going to use two branches.
I personally use it all the time on longer lived projects. Stashing/unstashing is fine, but I like really like the flexibility of just being able to switch over to another branch to fix something, review a PR, or anything else with zero mental overhead of keeping track of stashed work.
In other words, I wouldn't feel safe doing weird Git operations in a worktree, so why use one in the first place? If I want a true throwaway repo, I'll just re-clone in another directory (and make sure to remove the remote, to be extra safe in case I accidentally push).
Also, git worktree requires me to make a new directory anyway! So why would I not just put a fresh clone in there? To save a few megabytes of the .git index?
I've never had any issues with worktrees after heavy and extensive worktree usage since 2017. I use it regularly on big projects. The only time I had an issue with git was actually on a small non-worktree repo after 10 years of using git. The .git/ metadata got corrupted somehow, never understood why or how.
When you have a worktree based workflow, you're getting the benefit of a single .git/ database managing them. Whereas if you have multiple standalone clones, you have to manually keep them up to date. With worktrees doing a `git fetch` will ensure all worktrees are able to do git operations on the most update to date git data. Whereas separate clones means you need to do N git fetches.
Saving a few megabytes literally doesn't matter. Your project dependencies will likely use up tens if not hundreds of megabytes when you install them (pip packages, node modules, etc). Disk space is never really an issue anymore is it? Disks are hundreds of GBs now even on low end machines. I probably have multiple GBs of redundant disk usage because of my worktrees, but the convenience is well worth it IMO.
Maybe there is "git update master from remote and throw away my local changes because if i committed something there that was a mistake". But probably not, sadly, because technically the "git pull" operation to pull origin/master into the local tracking branch master updates the working copy where master is checked out, and that's what worktree is trying to avoid (changing a directory you're not in). I guess "git pull; git checkout -b new_branch remotes/origin/master" is what I should write, but that's more bytes to type.
git fetch origin
git checkout -b new_work origin/master
Git pull is essentially git fetch + merge. You don't have to update your local master branch to check out a new branch based on it.As long as you set the upstream to master, it works fine, you can pull in upstream main/master changes without issue. It does mean you have these "wt-" branches, but it works well enough for me, never got in my way or caused me to stop using worktrees.
My workflow is like this: For each worktree I add, checkout a new branch, wt-N, with it's upstream set to main/master. If I need to checkout some other branch in a worktree, do it, finish up, check out wt-N again, git pull to bring it up to date with upstream. Leaving idle worktrees on a "wt-" branch so I don't have the issue of "branch X is already checked out out worktree Y"
Alternatively you could do `git switch -c new_work && git pull -f origin master` which will create a new branch (you could use `git checkout -b` as well but like you said, more bytes to type) and then force pull from master on origin into the current branch (which is now your new branch) replacing the history with origin/master.
Checking out the same commit is fine. You just can’t checkout the same branch.
This is not a problem for me because I either use detached head (if say I’m going to build from some commit) or restrict branches to certain worktrees naturally (if I am backporting to `old-version` on a worktree then... it’s not like that backport branch is ever going to be relevant in my main worktree where I do most of my work).
I have two workflows heavily benefitting from worktrees. The first is simple enough...code archeology. I want to do it without disturbing my working tree. Yes, this could have also been done with a separate clone, but I'd have to fetch in that clone, and that's just extra keystrokes.
The second is that I like to have a "synthesis" worktree...one that includes PRs I'm reviewing and ones I'm creating. Both benefit from my regularly building and using them (e.g., I might incidentally find bugs through usage I otherwise missed through testing or code-reading, or I might spot pending integration problems). I don't want multiple build locations...I want to run everything in one build.
But when I've finished developing a PR, need to create a clean version of it from the master branch. In my secondary worktree, I can just cherry-pick commits from my synthesis worktree to produce a clean branch to push. I could do this in the synthesis worktree, too, but at the cost of almost certainly forcing a complete rebuild. In a second clone, I'd have to create patch files because the clone wouldn't have access to the commit objects for cherry-picking.
When you check out a branch, your working directory is expected to be in sync with it. Internally the HEAD points to the branch which then points at the commit. When you make changes on one worktree, in general it doesn't update the other worktrees.
So if you change the reference that the branch is pointing at, when you look inside the other worktree that points to that branch, it has no way of knowing it was initially looking at a different commit and it just thinks that the diff between the current head of that branch and your current working directory is a bunch of uncommitted changes.
If you use --detach or refer directly to the commit instead you get the behavior that is expected which is that it points to the commit itself and not the branch (which then points to the commit).
> Also, git worktree requires me to make a new directory anyway! So why would I not just put a fresh clone in there? To save a few megabytes of the .git index?
The problem is that not all repos are small and often it's easier to just do things in one repo than to need to push everything up and pull it down to move between multiple repos.
Also not all repos are small. The linux repo (which if you are working in the higher spec side of embedded software you probably will be using with multiple different downstreams) is at least a gigabyte in size nowadays. I like having the whole repo when I can because it makes looking for stuff locally easier but I don't want to lug around multiple copies of that repo if I can avoid it.
Worktrees made working on that monolith easier to do my types of tasks (updating tests that were specific to the UI team I was on, also the only person willing to touch java code).
It sounds kinda backwards, but if you have to support multiple versions of a product that still brings in hundreds of millions in revenue it's nice to use git worktree to basically check out different versions of your product in time.
I mostly use worktrees for very separate things. Like: long-running build, way old or way new versions of the app. Then it doesn’t make sense to mix-and-match virtual branches. So when I want to build the app for deployment I don’t want to worry about whatever other changes getting in the way. Git worktrees doesn’t solve the same problem as what GitButler does. Worktrees is a streamlined way of the manual separate-clone workflow for the same repository. (Technically they are all distinct repositories once you clone them but ya know.)
I do have use for separate-from-branch files. Like notes to myself and test scripts that aren’t going into the branch. Crucially these files have nothing to do with the main work: the files themselves are not involved, so there can never be merge conflicts.
This GitButler workflow makes sense for things that (1) won’t cause merge conflicts and (2) which won’t step on each others’ toes. The example about a translation and code change is nice. Doing a translation at the same time as a code change is not likely to “break the build”.
The submitter unhelpfully didn’t mention the name in the title.
This is where he lost me, do I now need an UI to use git? ridiculous
You and I don't - but, judging by what I've seen from coworkers, we are in the minority.
It works just fine with Sublime Merge, so long as you add the worktree as the repo - not the bare repo. I do wish there was better integration.
I tried Worktrees a couple of years ago but not having any integration with them with Rider led me to walk away.
If you want to force it to do it anyways you just use -f/--force.
When you check out a branch, the expectation is that the working tree is in sync with the current head commit. If you check out the same branch in multiple places, then if you commit on worktree a, worktree b won't say that it's still on the old commit, it'll just say that the diff between the current head and the contents of worktree b's working directory are a bunch of uncommitted changes. That's why it's unintuitive.
This is clearly not the intended behavior for most users when they want to check out the same branch in multiple places. They want to check out a given commit that happens to be the head of the current branch in multiple places. To do that you use --detach so that the worktree head points to the commit rather than the branch itself.
That means that I can easily do a review (I like having the code open in my IDE during a review), without having to stash my own work.
Cheers Jochen
> You can stash everything, create a new branch, switch to it, fix the bug, commit and push it, then switch back to what you were working on.
Also comfortable with fixing the bug on my current branch, leaving it there to validate as I complete the other stuff, then later pull commits out to new branches to create atomic PRs. Assuming the bug can wait that is, otherwise yep off to a new branch, and no amount of git magic prevents an interruption from being disruptive.
I know worktrees are a thing but I feel like the branching model is enough complexity to both be useful and remember. No problem if people want to use a more complex workflow with a GUI, this is for them, not for me.
If you're just talking worktrees, the benefit is what it says on the tin, multiple parallel working directories. They're a nice to have, not world-changing or anything.
The example in the article is contrived to sell a separate kind of workflow tool in the product he's building. I don't use worktrees like that.
My use case is heretical software development, but sometimes I work with projects that take a long time to build. And even longer to test. Worktrees let me minimize the amount of rebuilding when switching branches, or kick off a full test run and go work in another branch.
For example, let's say you are working on a static site (this is just an example). You can have a web server running that is serving the main worktree from https://mysite.local/, and any worktrees from https://mysite.local/~[worktree]/. Then, if you want to make a particular branch available for someone to review in their browser to provide feedback, you can direct them to the worktree URL while you continue doing other work either in your main branch, or on other branches altogether.
- You can fix the bug and commit it into your feature branch and try to get them both deployed together.
- You can stash everything, create a new branch, switch to it, fix the bug, commit and push it, then switch back to what you were working on.
- You could locally clone the repository to a new directory and work on the other branch there, then push it back into the main one and then push from there back upstream.
--
What?
Why couldn't you, like, create another branch off of main, switch to that, and fix the bug there, then resume working on your feature branch?
Then when one of the two (bugfix or feature branch) is merged into main, you rebase main into the other.
I’ll keep an open mind, but if you’re going to shake up a workflow as solid and well understood as git, you better have a damn good offering.
You're supposed to be working in a feature branch when your were given the bug to fix. Would you not have commited (or, worse, pushed) anything to it already? Most times you already have, so it's not like stashing your latest changes and getting to the latest state of the feature branch is neutral.
Plus, what big benefit would stashing have, since you'll still need to create a separate bugfix branch to create the bugfix PR off anyway? If you get to context switch while you were working on my-feature-branch, you still need to create my-bugfix-branch and push the fix from that, no?
Besides nobody guarantees you ahead of time that the bug will just be solved fast and they get to return to working on the feature pronto. So a bugfix branch makes more sense than a stash, which is more of a short term thing.
Just making a bugfix branch is neater.
So? That's what amend and/or squash is for. So that you don't need to be in some "perfect" condition to commit. Or you can do a soft git reset as you say.
But sure, even git stash would make more sense than worktrees for the TFA scenario I quoted.
(Again this is what git-reset(1) and friends are for)
Git might be, but how many people want to use git is to provide useful commit histories, both for any reviewers at the time and any code spelunkers 10 years into the future. Having a tool that tries to make that easier isn't a bad idea.
And by primitive I mean that I want to be able to commit whenever I feel like I want a snapshot. For any reason. Not hindered by concepts like does-it-build. That’s the low level. Then at the higher level are things like “public history” and “tested history”. That’s facilitated by the low/mid-level history rewriting tools.
Some people I know use Intellij’s “shelve” feature or whatever it is called. Interestingly it does provide some features that Git does not seem to have—and overlaps with GitButler—but it’s own bespoke thing, not integrated with Git.
And using these extra concepts on top of (or under?) Git doesn’t make sense for my workflow. Because the VCS is already there. So I don’t need to think about if I’m ready-to-commit—just make a WIP or a TEST commit, maybe discard later or maybe rewrite them.
For me, Git covers everything from snapshotting some private notes I have on the work that I’m currently doing to making a nice:
> useful commit [history], both for any reviewers at the time and any code spelunkers 10 years into the future.
It provides that whole range.
I think this is the key comment - that might be true, but I don't see a real criticism of making tools for not-avgcorrection to facilitate their workflows better. I agree things can be done in Git. I disagree that they must be done in Git.
Why not just commit it?
I mean, if it's not a problem to do that (and I don't find either option a problem personally) then fine, but it doesn't seem like it's a "better" option except in some small cases (like forgetting you stashed).
> You can stash everything, create a new branch, switch to it, fix the bug, commit and push it, then switch back to what you were working on.
And OP said this (snarkily)
> Why couldn't you, like, create another branch off of main, switch to that, and fix the bug there, then resume working on your feature branch?
They're the same except the blog author was more specific!
Worktrees are a different way of managing multiple branches in flight. They're not hard to use, and using them doesn't imply any incompetency with git. They are likely being used to cope with another kind of problem.
Here's one example:
I'm working on a trunk-based project, and I am upgrading a core framework like Angular or Django. That's a fairly intrusive change that involves modifying some untracked on-disk artifacts, like node_modules/ and package lockfiles. Switching branches doesn't help me manage those artifacts, and might require a full teardown and rebuild of my dev environment each switch. Even if I maintain a meticulous commit history and never have uncommitted "WIP stuff" that I need to stash, switching from my branch to fix a bug in the main branch could be annoying and intrusive just because of the dev environment. This is a great case for a separate worktree.
Why would you ever have build artifacts, incremental or not, as part of your code repo?
Instead of stashing or anything like that we would commit with the text "WIP". Every time we had a stable point, we would make a "WIP" commit and then continue, giving us a good point to revert to if the next steps don't work.
If someone interrupts me I would make a WIP commit and then revert it and then make the change they're asking about.
Later on we would squash the WIP commits and make real commits out of the changes.
Another nice thing about this workflow is that everything goes into the reflog.
Just one concept instead of (1) commits (2) stash (3) oh these changes, eh, I don’t know what to do with them... let’s think about it after lunch.
This is kind-of the million dollar question, though, I think? What would such a reason be?
(I ask from a perspective of genuine curiosity, not of implying that you're wrong to have them)
Just for the purposes of comparison - my own "bright line between [good and bad]" is "does the commit message just contain 'WIP', or is it actually fully written-out?" - but I can see that "is the work committed or not?" also works. Thanks for explaining!
On some _very_ rare occasions, I might find that, by the time I've gotten the change to a reviewable state, it's actually too large to be meaningfully reviewed in one go, in which case I'll go back and retroactively (re-)implement it reviewable-chunk-by-reviewable-chunk (on a different branch), and get each of them reviewed and merged in turn. But, 99% of the time, I simply:
* use WIP commits as "checkpoints" along the way while implementing
* get to a minimal mergable change
* use `rebase -i` to squash everything into a single, well-written, reviewable commit
* rinse and repeat
The question is, if that exists why are the virtual trees meaningfully different?
I mean, that's option 2 of what you're quoting, and is generally my preferred option. That said, there are cases where it breaks down and I end up wanting worktrees or independent clones, generally involving ≈`.gitignore`d local data:
• A newly created bugfixing branch may take awhile (minutes? hours?) for me to build. I can either go on a coffee break while waiting for it (since I can't very well test my ability to reproduce the bug, nor verify my bugfixes work, without having said branch in a compiling state), or I can have said build occur in a secondary worktree - and continue working on my original task in my primary worktree while waiting for it to build.
• If I've got a lot of `.gitignore`d config file tweaks related to my major feature work / profiling / debugging / ???, I might not want to bring those over to my bugfixing session. `git stash push --all` is an option, but that will get build artifacts - which will take awhile to stash - not just my local config files. A fun ancedote: I once wasted an hour failing to reproduce a game's audio crash because a local `user.ini` file had the entire audio system disabled via feature flag. (I now prefer setting the volume to 0 instead.)
• I do a bunch of porting and cross-platform abstraction stuff, which makes my build times even worse than usual by having to repeat builds N times. I might not want to invalidate by main build cache for all that, when my bugfixing branch might only reasonably need local testing on 1 platform, and gain next to no benefit from the previous cache as is (depending on how far diverged the branch I was working from and the branch where the bugfix should occur are.)
No, option 2 is to stash.
Their three weird options are:
(a) fix the bug on the feature branch and "try to get them both deployed together"
(b) git stash, fix the bug in a new branch, come back
(c) locally clone the repository
At least (b) is somewhat sane, but it's still weird that the even more code-managementy option of "commit your halfway feature work/create new branch for the bugfix/come back to the feature and amend/squash/soft reset" isn't even mentioned.
Your cases are more involved than the contrived scenario you give, where their weird options don't make sense.
...this is just by-hand stashing with alternative commands and extra "WIP" commit messages.
Which, uh, is admittedly an option. One that won't easily preserve your index independently from your working tree - awkward if you're in the middle of painstakingly separating out changes to add to the next commit with `git add -p` - and one that risks "WIP" commits ending up in your tree... but it is an option.
The index + commits is flexible enough. The stash concept is not needed for me.
I don’t want to fiddle with dropping the stash one too many times, forgetting what they were about and so on. Merge conflicts because I didn’t pop before resuming..
It’s just a mostly useless additional “thing”.
How "cloning the repo on a different folder on the same machine just to work on a bug fix" is different from the main way Git is supposed to be used (creating a new branch for the bug fix and working there, then checking out your feature branch again)?
In that it doesn't make sense.
They might as well go full 1980s style "codebase_1/ codebase_2/ codebase_3/ codebase_3.bak/" and so on copies, and skip code management altogether...
> You can stash everything, create a new branch, switch to it, fix the bug, commit and push it, then switch back to what you were working on.
OP said this (you and a few others reiterated or agreed with it)
> Why couldn't you, like, create another branch off of main, switch to that, and fix the bug there, then resume working on your feature branch?
I can't see the difference. Seeing them side by side they're virtually identical except the blog author specified stashing!
But is it overkill for some fast translation fixes? Yeah, probably:)