{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreibd4ehe66b47d4ucz2rkenjwqailiplzo25ew7rlhvzdkn7d35ghe",
    "uri": "at://did:plc:46ti67tc37qcmwp2vaynk6fq/app.bsky.feed.post/3mk6rus3cgf22"
  },
  "path": "/posts/developing_a_git_worktree_helper_with_copilot/",
  "publishedAt": "2026-04-23T19:50:20.910Z",
  "site": "https://blogops.mixinet.net",
  "tags": [
    "GitHub Copilot CLI",
    "worktree",
    "Powerlevel10k\ntheme",
    "aider",
    "claude-code"
  ],
  "textContent": "Over the past few weeks I’ve been developing and using a personal command-line tool called `gwt` (_Git Worktree_) to manage Git repositories using worktrees. This article explains what the tool does, how it evolved, and how I used GitHub Copilot CLI to develop it (in fact the idea of building the script was also to test the tool).\n\n## The Problem: Managing Multiple Branches\n\nI was working on a project with multiple active branches, including orphans; the regular branches are for fixes or features, while the orphans are used to keep copies of remote documents or store processed versions of those documents.\n\nThe project also uses a special orphan branch that contains the scripts and the CI/CD configuration to store and process the external documents (it is on a separate branch to avoid mixing its operation with the main project code).\n\nThe plan is trigger a pipeline against the special branch from remote projects to create or update the doc branch for it in our git repository, retrieving artifacts from the remote projects to get the files and put them on an orphan branch (initially I added new commits after each update, but I changed the system to use force pushes and keep only one commit, as the history is not really needed).\n\nThe original documents have to be changed, so, after ingesting them, we run a script that modifies them and adds or updates another branch with the processed version; the contents of that branch are used by the `main` branch build process (there we use `git fetch` and `git archive` to retrieve its contents).\n\nWhen working on the scripts to manage the orphan branches I discovered the worktree feature of `git`, a functionality that allows me to keep multiple branches checked out in parallel using a single `.git` folder, removing the need to use `git switch` and `git stash` when changing between branches (until now I’ve been a heavy user of those commands).\n\nReading about it I found that a lot of people use worktrees with the help of a wrapper script to simplify the management. After looking at one or two posts and the related scripts I decided to create my own using a specific directory structure to simplify things.\n\nThat’s how I started to work on the `gwt` script; as I also wanted to test `copilot` I decided to build it using its help (I have a pro license at work and wanted to play with the cli version instead of integrated into an editor, as I didn’t want to learn a lot of new keyboard shortcuts).\n\n## The gwt Philosophy: Opinionated and Transparent\n\n`gwt` enforces a simple, filesystem-visible model:\n\n  * **Exactly one bare repository** named `bare.git` (treated as an implementation detail)\n  * **One worktree directory per branch** where the directory name matches the branch name\n  * **Single responsibility** : `gwt` doesn’t try to be a general `git` wrapper; it only handles operations that map cleanly to this layout\n\n\n\nThe repository structure looks like this:\n\n\n    my-repo/\n    +-- bare.git/           # the Git repository (internal)\n    +-- main/               # worktree for branch \"main\"\n    +-- feature/api/        # worktree for branch \"feature/api\"\n    +-- fix/docs/           # worktree for branch \"fix/docs\"\n    +-- orphan-history/     # worktree for the \"orphan-history\" branch\n\nThe tool follows five core design principles:\n\n  1. **Explicit over clever** : Git commands are not hidden or reinterpreted\n  2. **Transparent execution** : Every operation is printed before it happens\n  3. **Safe, preview-first operations** : Destructive commands default to preview, confirmation, then apply\n  4. **Shell-agnostic core** : The script never changes the caller’s working directory (shell wrappers handle that)\n  5. **Opinionated but minimal** : Only commands that fit the layout model are included\n\n\n\n## Core Commands\n\nThe script provides these essential commands:\n\n  * `gwt init <url>` — Clone a repository and set up the `gwt` layout\n  * `gwt convert <dir>` — Convert an existing Git checkout to the `gwt` layout\n  * `gwt add [--orphan] <branch> [<base>]` — Create a new worktree (optionally orphaned)\n  * `gwt remove <branch>` — Remove a worktree and unregister it (asks the user to remove the local branch too, useful when removing already merged branches)\n  * `gwt rename <old> <new>` — Rename a branch AND its worktree directory\n  * `gwt list` — List all worktrees\n  * `gwt default [<branch>]` — Get or set the default branch\n  * `gwt current` — Print the current worktree or branch name\n\n\n\nExcept `init` and `convert` all of the commands work inside a directory structure that follows the `gwt` layout, which looks for the `bare.git` folder to find the root folder of the structure.\n\nAs I don’t want to hide which commands are really used by the wrapper, all `git` and filesystem operations pass through a single `run` shell function that prints each command before executing it. This gives complete visibility into what the tool is doing.\n\nAlso, destructive operations (`remove`, `rename`) default to preview mode:\n\n\n    $ gwt remove feature-old --dry-run\n\n    + git -C bare.git branch -d feature-old\n    + git -C bare.git worktree remove feature-old/\n\n    Apply these changes? [y/N]:\n\nThe user sees exactly what will happen, can verify it’s correct, and only then confirm execution.\n\n## Incremental Development with Copilot\n\nThe `gwt` script has grown from 597 lines in its original version (`git-wt`) to 1,111 lines when writing the first draft of this post.\n\nThis growth happened through incremental, test-driven development, with each feature being refined based on real usage patterns.\n\nWhat follows is a little history of the script evolution written with the help of `git log`.\n\n### Initial version\n\nFirst I wrote a design document and asked `copilot` to create the initial version of the `git-wt` script with the original core commands.\n\nI started to use the tool with a remote repostory (I made copies of the branches in some cases to avoid missing work) and fixed bugs (trivial ones with `neovim`, larger ones asking `copilot` to fix the issues for me, so I had less typing to do).\n\n###### Note:\n\nAs I used `copilot` I noticed that when you make manual changes it is important to tell the tool about them, otherwise it gets confused and sometimes tries to remove manual changes.\n\n### First command update\n\nOne of the first commands I had to enhance was `rename`:\n\n  * as I normally use branches with `/` on their name and my tool checks out the _worktrees_ using the branch name as the path inside the `gwt` root folder (i.e. a `fix/rename` branch creates the `fix` directory and checks the branch inside the `fix/rename` folder) the `rename` command had to clean up the empty parent directories\n  * when renaming a worktree we move the folders and fix the references using the `worktree repair` command to make things work locally, but the rename also affects the remote branch reference, to avoid surprises the command unsets the remote branch reference so it can be pushed again using the new name (of course, the user is responsible of managing the old remote branch, as the `gwt` can’t guess what it should do with it).\n\n\n\n### Integration with the shell\n\nAs I use `zsh` with the Powerlevel10k\ntheme I asked `copilot` to help me add visual elements to the prompt when working with `gwt` folders, something that I would have never tried without help, as it would have required a lot of digging on my part on how to do it, as I never looked into it.\n\nThe initial version of the code was on an independent file that I sourced from my `.zshrc` file and it prints `__`on the right part of the prompt when we are inside a`gwt` folder (note that if the folder is a worktree we see the existing git integration text right before it, so we have the previous behavior and we see that it is a `gwt` friendly repo) and if we are on the root folder or the `bare.git` folder we see `__gwt` or `__bare` (I added the text because there are no git promts on those folders).\n\nI also asked `copilot` to create `zsh` autocompletion functions (I only use `zsh`, so I didn’t add autocompletion for other shells). The good thing here is that I wouldn’t have done that manually, as it would have required some reading to get it right, but the output of `copilot` worked and I can update things using it or manually if I need to.\n\nOne thing I was missing from the script was the possibility of changing the working directory easily, so I wrote a `gwt` wrapper function for `zsh` that intercepts commands that require shell cooperation (changing the working directory) and delegates everything else to the core script.\n\nCurrently the function supports the following enhanced commands:\n\n  * `cd [<branch>]`: change into a worktree or the default one if missing\n  * `convert <dir>`: convert a checkout, then cd into the initial worktree\n  * `add [--orphan] <branch> [<base>]`: create a worktree, then cd into it on success\n  * `rename <old> <new>`: rename a worktree, then cd into it if we were inside it\n\n\n\nNote that the `cd` command will not work on other shells or if the user does not load my wrapper, but the rest will still work without the working directory changes.\n\n### Renaming the command\n\nAs I felt that `git-wt` was a long name I renamed the tool to `gwt`, I could have done it by hand, but using `copilot` I didn’t have to review all files by myself and it did it right (note that I have it configured to always ask me before doing changes, as it sometimes tries to do something I don’t want and I like to check its changes …​ as I have the files in git repos, I manually add the files when I like the status and if the cli output is not clear I allow it to apply it and check the effects with `git diff` so I can validate or revert what was done).\n\n### The `convert` command\n\nAfter playing with one repo I added the `convert` subcommand for migrating existing checkouts, it seemed a simple task at first, but it took multiple iterations to get it right, as I found multiple issues while testing (in fact I did copies of the existing checkouts to be able to re-test each update, as some of the iterations broke them).\n\nThe version of the function when this post was first edited had the following comment explaining what it does:\n\n\n    # ---------------------------------------------------------------------------\n    # convert - convert an existing checkout into the gwt layout\n    # ---------------------------------------------------------------------------\n    #\n    # Must be run from the parent directory of <dir>.\n    #\n    # Steps:\n    #   1. Read branch from the checkout's HEAD\n    #   2. Rename <dir> to <dir>.wt.tmp (sibling, same filesystem)\n    #   3. Create <dir>/ as the new gwt root\n    #   4. Move <dir>.wt.tmp/.git to <dir>/bare.git; set core.bare = true\n    #   5. Fix fetch refspec (bare clone default maps refs directly, no remotes/)\n    #   6. Add a --no-checkout worktree so git wires up the metadata and\n    #      creates <dir>/<branch>/.git (the only file in that dir)\n    #   7. Move that .git file into the real working tree (<dir>.wt.tmp)\n    #   8. Remove the now-empty placeholder directory\n    #   9. Move the real working tree into place as <dir>/<branch>\n    #  10. Reset the index to HEAD so git status is clean\n    #      (--no-checkout leaves the index empty)\n    #  11. Create <dir>/.git -> bare.git symlink so plain git commands work\n    #      from the root without --git-dir\n    #\n    # The .git file ends up at the same absolute path git recorded in step 5,\n    # so no worktree repair is needed. Working tree files are never modified.\n\nThe `.git` link was added when I noticed that I could run commands that don’t need the checked out files on the root of the `gwt` structure, which is handy sometimes (i.e. a `git fetch` or a `git log`, that shows the log of the branch marked as `default`).\n\nAfter playing with commands that used the `bare.git` folder I updated the `init` and `convert` commands to keep the origin refs, ensuring that the remote tracking works correctly.\n\n### Improving the `add` command\n\nWhile playing with the tool on more repos I noticed that I also had to enhance the `add` command to better handle worktree creation, depending on my needs.\n\nRight now the tool supports the following use cases:\n\n  * if the `branch` exists locally or on origin, it just checks it out.\n  * if the `branch` does not exist, we create it using the given base branch or, if no base is given, the current _worktree_ (if we are in the root folder or `bare.git` the command fails).\n  * as I needed it for my project, I added a `--orphan` option to be able to create orphan branches directly.\n\n\n\n### Moving to a single file\n\nEventually I decided to make the tool self contained; I removed the design document (I moved the content to comments on the top of the script and details to comments on each function definition) and added a pair of commands to print the code to source for the `p10k` and `zsh` integration (autocompletion & functions), leaving everything in a single file.\n\nNow my `.zshrc` file adds the following to source both things:\n\n\n    # After loading the p10k configuration\n    if type gwt >/dev/null 2>&1; then\n      source <(gwt p10k)\n    fi\n    [...]\n    # After loading autocompletion\n    if type gwt >/dev/null 2>&1; then\n      source <(gwt zsh)\n    fi\n\n### Versioning\n\nAs I modified the script I found interesting to use CalVer-based versioning (the version variable has the format `YYYY.mm.dd-r#`) so I added a subcommand to show its value or bump it using the current date and computing the right revision number.\n\n### About the use of `copilot`\n\nAlthough I’ve never been a fan of AI tools I have to admit that the `copilot` CLI has been very useful for building the tool:\n\n  * **Rapid prototyping** : Each commit represented a small feature or fix that I could implement, test immediately in my actual workflow, and iterate on based on the result\n  * **Edge case handling** : Rather than trying to anticipate every scenario upfront, I could ask Copilot how to handle edge cases as they appeared in real usage\n  * **Script refinement** : Questions like \"how do I clean up empty directories after a rename\" or \"how do I detect if I’m inside a specific worktree\" were quickly answered with working code\n  * **Shell integration** : The Zsh wrapper and completion system grew from simple prototypes to sophisticated features, with each iteration informed by how I actually used the tool\n\n\n\nFor example, the `convert` command started as a simple rename operation, but evolved to also create a `.git` symlink and intelligently handle various migration scenarios—all because I used it repeatedly and refined the implementation each time.\n\n## Self-Contained and Opinionated\n\n`gwt` is deliberately opinionated:\n\n  * **Zsh & Powerlevel10k Integration**: The tool includes built-in Zsh shell integration, accessed via `source <(gwt zsh)` and supports adding a prompt segment when using `p10k`, as described earlier.\n  * **Directory Structure** : The `bare.git` directory name is non-negotiable. This is how `gwt` discovers the repository root from any subdirectory, and how the tool knows whether a directory is a gwt repository. The simplicity of this marker means the discovery mechanism is foolproof and requires no configuration.\n  * **No Configuration Files** : `gwt` deliberately has no configuration. There are no `.gwtrc` files or config directories. This makes it portable; the tool works the same way everywhere, and repositories can be shared across systems without synchronizing configuration.\n\n\n\n## From Script to System\n\nWhat started as a small helper script for managing worktrees has become a complete system:\n\n  1. **Core script** (`gwt`): 1,111 lines of pure shell, no external dependencies\n  2. **Shell integration** : Zsh functions and completions\n  3. **Prompt integration** : Powerlevel10k segment\n  4. **Documentation** : Built-in help and design philosophy documentation\n\n\n\nThe script is self-contained, everything needed for the tool to work is in a single file.\n\nThis makes it trivial to update (just replace the script) or audit (no hidden dependencies).\n\n## Development with AI support\n\nDeveloping `gwt` with `copilot` taught me some things:\n\n  * **Incremental refinement works well for small tools** : Each iteration informed the next, resulting in a tool that handles real use cases elegantly\n  * **Transparency is a feature** : Making operations visible builds confidence and is easier to debug\n  * **Opinionated tools can be powerful** : By constraining the problem space (one bare repo, one worktree per branch), the solution becomes simpler and more robust\n  * **Shell integration matters** : The same core commands are easier to use when they can automatically change directories and provide completions\n  * **Real-world testing is essential** : I wouldn’t have discovered the need for automatic directory cleanup or context-aware `cd` behavior without actually using the tool daily\n\n\n\n## What was next?\n\nThe tool is stable and handles my daily workflow well, so my guess is that I would keep using it and fixing issues if or when I found them, but I do not plan to include additional features unless I find a use case that justifies it (i.e. I never added support for some of the `worktree` subcommands, as it is easier to use the `git` versions if I ever needed them).\n\n## What really happened\n\nWhile editing this post I discovered that I needed to add another command to it and fixed a bug (see below).\n\nWith those changes and the inclusion of a license and copyright notice (just in case I distribute it at some point) now the script is 1,217 lines long instead of the 1,111 it had when I started to write this entry.\n\n### Submodule Support\n\nWhen I converted this blog repository to the `gwt` format and tried to preview the post using `docker compose`, it failed because the worktree I was on didn’t have the Git submodule initialized.\n\nMy blog theme is included on the repository as a submodule, and when I used `gwt` to check out different branches in worktrees, the submodule was not initialized in the new worktrees.\n\nThis led me to add new internal function and a `gwt submodule` command to handle submodule initialization; the internal function is called from `convert` and `add` (when converting a repo or adding a worktree) and the public command is useful to update the submodules on existing branches.\n\n### Path Handling with Branch Names Containing Slashes\n\nThe second discovery was a bug in how the tool handled branch names containing slashes (e.g., `feature/new-api`, `docs/user-guide`), the worktree directories are created with the branch name as the path, so a branch like `feature/new-api` would create two nested folders (`feature` and `new-api` inside it).\n\nHowever, there was a mismatch in how the `zsh` wrapper function resolved worktree paths (initially it used shell parameter expansion, i.e. `rel=\"${cwd#\"$REPO_ROOT\"/}\"`), versus how the core script calculated them, causing the `cd` command to fail or navigate to the wrong location when branch names contained slashes.\n\nThe fix involved ensuring consistent path resolution throughout the script and wrapper (now it uses a function that processes the `git worktree list` output), so that `gwt cd feature/new-api` correctly navigates to the worktree directory regardless of path depth.\n\n## Conclusion\n\n`gwt` is a tool that solves a real problem: managing multiple Git branches simultaneously without context-switching overhead.\n\nI’m sure I’m going to keep using it for my projects, as it simplifies some workflows, although I’ll also use `switch` and `stash` in some cases, but I like the use of multiple worktrees in parallel.\n\nIn fact I converted this blog repository checkout to the `gwt` format to work on a separate branch as it felt the right approach even if I’m the only one using the repo now, and it helped me improve the tool, as explained before.\n\nAlso, it was a good example of how to use AI tools like `copilot` to develop a simple tool and keep it evolving while using it.\n\nIn any case, although I find the `copilot` useful and has saved me time, I don’t trust it to work without supervision, it worked well, but got stuck some times and didn’t do the things as I wanted in multiple occasions.\n\nI also have an additional problem now …​ I’ve been reading about it, but I don’t really know which models to use or how the premium requests are computed (I’ve only been playing with it since last month and I ran out of requests the last day of the month on purpose, just to see what happened …​ it stops working …​ ;).\n\nOn my work machine I’ve been using a specific user account with a _GitHub Copilot Business_ subscription and I only used the `Anthropic Claude Sonnet 4.6` model and with my personal account I configured the `Anthropic Claude Haiku 4.5` model, but I’ve only used that to create the initial draft of this post (I ended up rewriting most of it manually anyway) and to review the final version (I’m not a native speaker and it was useful for finding typos and improving the style in some parts).\n\nI guess I’ll try other models with `copilot` in the future and check other command line tools like aider or claude-code, but probably only using free accounts unless I get a payed account at work, as I have with _GitHub Copilot_.\n\nTo be fair, what I will love to be able to do is to use local models (`aider` can do it), but the machines I have are not powerful enough. I tried to run a simple test and it felt really slow, but when I have the time or the need I’ll try again, just in case.",
  "title": "Sergio Talens-Oliag: Developing a Git Worktree Helper with Copilot",
  "updatedAt": "2026-04-23T17:40:00.000Z"
}