Skip to content

Repository files navigation

wt

wt is a tool for git worktrees. It also controls the files that git ignores.

The problem

The command git worktree add makes a new checkout. It does not make the files that git ignores. Each new worktree is therefore not complete.

wt keeps one file and makes a link in each worktree. A git hook makes the links.

How to use wt

Start with a repository that you do not have

wt clone git@github.com:you/project.git
cd project/main

The command makes this structure:

project/
├── .bare/ the bare repository. It holds the store and the hook
├── .git the text `gitdir: ./.bare`
└── main/ the first worktree

Start with a repository that is already on your disk

Go into a worktree of the repository. Make the store, the config entry and the hook:

wt init

Then move each ignored file into the store:

wt share .env .auth .envrc

The command moves each file one time. It then makes a link in each worktree that is already present. One command is therefore sufficient for the full repository.

The command refuses a path that contains a tracked file. Git supplies each tracked file, and a link hides it. Give the ignored path instead. For example, give terraform/terraform.tfvars and not terraform/.

Make a worktree

wt add eng-1234

The hook makes the links in the new worktree. The program that makes the worktree is not important. wt add, git worktree add, your editor and an agent all start the hook.

The command finds the branch in this sequence:

  1. A local branch.
  2. A remote branch origin/eng-1234.
  3. A fetch of origin/eng-1234 from the network.
  4. A new branch from the default branch.

Steps 1 and 2 do not use the network. The command does step 3 only if steps 1 and 2 find no branch. Step 3 prevents an error: without it, wt can make a local branch while a remote branch with the same name is already present.

Use the option --no-fetch to stay offline. Use the option --fetch to do a fetch first.

Go to a worktree

wt deny

wt <worktree> is the short form of wt cd <worktree>. Both accept the name of the worktree directory. Both also accept the name of the branch, because a branch can move after you make the worktree. The start of a directory name is sufficient when only one worktree matches.

A command wins over a worktree with the same name, and a short form such as wt d is a command. wt ls therefore prints the worktrees, also when a worktree has the name ls. The long form wt cd ls goes to that worktree, and wt add prints a warning when it makes a directory with the name of a command.

See the worktrees

wt ls

The command prints each worktree, the branch it is on, and how many links are correct. It changes nothing.

Repair the links

The command git clean -xdf removes a link. A worktree can also be older than the config. The command wt sync makes each link that is absent:

wt sync

Remove a worktree

wt delete eng-1234

The command removes the worktree. It then deletes the branch if git merged the branch. wt d eng-1234 is the short form.

Remove each worktree whose work is finished

wt prune

The command asks origin which branches are still present. It then removes each worktree whose work is complete, and it deletes the branch:

ConditionExample
The base branch contains the branch, and origin still has it.A merge commit, and the branch is not deleted yet.
Origin no longer has the upstream branch.A squash merge or a rebase merge, and the branch is deleted.

The second condition is the one that matters for a squash merge. Such a merge writes a new commit, so the branch is not an ancestor of the base branch, and the merge is complete nonetheless.

wt prune removes no branch that is only on your machine. Each condition above needs proof that the branch reached origin: origin has it now, or the config names an upstream branch that origin no longer has. A new branch from wt add holds no commit of its own, so the base branch contains it, and the test for a merge alone cannot tell that branch from finished work. The proof of a push is the rule that keeps wt add eng-1234 safe until you push it.

The command also keeps:

ConditionReason
The worktree has a modified file or an untracked file.Use --force.
You are in the worktree.A removal would leave your shell in a path that is absent.
The worktree is on the base branch, or on no branch.There is no branch to test.

Use --dry-run to read the list and change nothing. Use --no-fetch to stay offline; the command then uses the refs it already has.

The command also removes the administrative files of a worktree whose directory you deleted by hand.

Git does not count the files that it ignores. An ignored file that is not in the store is therefore lost without a warning, as in wt delete.

wt does not destroy a file that is different

The commands wt share and wt sync compare the bytes before they make a link.

ConditionResult
The two files are the same.wt replaces the file with a link.
The two files are different.wt keeps the file and prints a message.

Use the option --force to replace a file that is different.

Commands

CommandShortFunction
wt initMake the store, the config entry and the hook. Move no files.
wt share <path>…wt shMove ignored paths into the store. Link them in each worktree.
wt add <branch> [dir]wt aMake a worktree for a branch.
wt cd <worktree>wt cPrint the path of a worktree. The shell integration then changes directory.
wt <worktree>The short form of wt cd <worktree>.
wt lswt lPrint each worktree, its branch and the condition of its links.
wt syncwt sCompare each worktree with the config. Make the links that are absent.
wt delete <worktree>wt dRemove a worktree. Delete its branch if git merged the branch.
wt prunewt pRemove each worktree whose work is finished. Delete its branch.
wt clone <url> [dir]Clone into the .bare layout. Make the first worktree.
wt shell-init fishPrint the shell integration.

A short form is the command itself, so it also wins over a worktree with the same name: wt d runs wt delete, and wt cd d goes to a worktree named d. wt init and wt clone have no short form. Each is short already, and you run it one time for a repository.

Config file

One global file holds the shared paths for each repository:

# ~/.config/wt/config.toml
[repos."github.com/you/project"]
link = [".env.override", ".auth", ".envrc"]

The key is the remote URL in a normal form. Each form of the URL gives the same key, so one file is correct for SSH and for HTTPS, and on each machine. Put this file in your dotfiles to keep the list.

wt reads $XDG_CONFIG_HOME/wt/config.toml when that variable is set.

The file holds no secret. It holds only path names. The content stays in the store, and the store stays in the repository at <common-directory>/shared/.

Installation

Cargo

cargo build --release
ln -sf "$PWD/target/release/wt"~/.local/bin/wt

Nix

nix profile install github:lorenzolfm/wt

Shell integration

Add this line to config.fish:

wt shell-init fish | source

The integration also gives completion, and it changes the directory after wt add, wt clone, wt cd and wt <worktree>:

PositionCandidates
wt <TAB>each command, each short form and each worktree
wt add <TAB>, wt a <TAB>each local branch and each remote branch
wt delete <TAB>, wt d <TAB>each worktree
wt cd <TAB>, wt c <TAB>each worktree
wt share <TAB>, wt sh <TAB>each ignored path that the config does not have, and files

License

MIT

About

Git worktrees, plus the gitignored files they need to share

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - lorenzolfm/wt: Git worktrees, plus the gitignored files they need to share · GitHub
Skip to content

Repository files navigation

wt

wt is a tool for git worktrees. It also controls the files that git ignores.

The problem

The command git worktree add makes a new checkout. It does not make the files that git ignores. Each new worktree is therefore not complete.

wt keeps one file and makes a link in each worktree. A git hook makes the links.

How to use wt

Start with a repository that you do not have

wt clone git@github.com:you/project.git
cd project/main

The command makes this structure:

project/
├── .bare/ the bare repository. It holds the store and the hook
├── .git the text `gitdir: ./.bare`
└── main/ the first worktree

Start with a repository that is already on your disk

Go into a worktree of the repository. Make the store, the config entry and the hook:

wt init

Then move each ignored file into the store:

wt share .env .auth .envrc

The command moves each file one time. It then makes a link in each worktree that is already present. One command is therefore sufficient for the full repository.

The command refuses a path that contains a tracked file. Git supplies each tracked file, and a link hides it. Give the ignored path instead. For example, give terraform/terraform.tfvars and not terraform/.

Make a worktree

wt add eng-1234

The hook makes the links in the new worktree. The program that makes the worktree is not important. wt add, git worktree add, your editor and an agent all start the hook.

The command finds the branch in this sequence:

  1. A local branch.
  2. A remote branch origin/eng-1234.
  3. A fetch of origin/eng-1234 from the network.
  4. A new branch from the default branch.

Steps 1 and 2 do not use the network. The command does step 3 only if steps 1 and 2 find no branch. Step 3 prevents an error: without it, wt can make a local branch while a remote branch with the same name is already present.

Use the option --no-fetch to stay offline. Use the option --fetch to do a fetch first.

Go to a worktree

wt deny

wt <worktree> is the short form of wt cd <worktree>. Both accept the name of the worktree directory. Both also accept the name of the branch, because a branch can move after you make the worktree. The start of a directory name is sufficient when only one worktree matches.

A command wins over a worktree with the same name, and a short form such as wt d is a command. wt ls therefore prints the worktrees, also when a worktree has the name ls. The long form wt cd ls goes to that worktree, and wt add prints a warning when it makes a directory with the name of a command.

See the worktrees

wt ls

The command prints each worktree, the branch it is on, and how many links are correct. It changes nothing.

Repair the links

The command git clean -xdf removes a link. A worktree can also be older than the config. The command wt sync makes each link that is absent:

wt sync

Remove a worktree

wt delete eng-1234

The command removes the worktree. It then deletes the branch if git merged the branch. wt d eng-1234 is the short form.

Remove each worktree whose work is finished

wt prune

The command asks origin which branches are still present. It then removes each worktree whose work is complete, and it deletes the branch:

ConditionExample
The base branch contains the branch, and origin still has it.A merge commit, and the branch is not deleted yet.
Origin no longer has the upstream branch.A squash merge or a rebase merge, and the branch is deleted.

The second condition is the one that matters for a squash merge. Such a merge writes a new commit, so the branch is not an ancestor of the base branch, and the merge is complete nonetheless.

wt prune removes no branch that is only on your machine. Each condition above needs proof that the branch reached origin: origin has it now, or the config names an upstream branch that origin no longer has. A new branch from wt add holds no commit of its own, so the base branch contains it, and the test for a merge alone cannot tell that branch from finished work. The proof of a push is the rule that keeps wt add eng-1234 safe until you push it.

The command also keeps:

ConditionReason
The worktree has a modified file or an untracked file.Use --force.
You are in the worktree.A removal would leave your shell in a path that is absent.
The worktree is on the base branch, or on no branch.There is no branch to test.

Use --dry-run to read the list and change nothing. Use --no-fetch to stay offline; the command then uses the refs it already has.

The command also removes the administrative files of a worktree whose directory you deleted by hand.

Git does not count the files that it ignores. An ignored file that is not in the store is therefore lost without a warning, as in wt delete.

wt does not destroy a file that is different

The commands wt share and wt sync compare the bytes before they make a link.

ConditionResult
The two files are the same.wt replaces the file with a link.
The two files are different.wt keeps the file and prints a message.

Use the option --force to replace a file that is different.

Commands

CommandShortFunction
wt initMake the store, the config entry and the hook. Move no files.
wt share <path>…wt shMove ignored paths into the store. Link them in each worktree.
wt add <branch> [dir]wt aMake a worktree for a branch.
wt cd <worktree>wt cPrint the path of a worktree. The shell integration then changes directory.
wt <worktree>The short form of wt cd <worktree>.
wt lswt lPrint each worktree, its branch and the condition of its links.
wt syncwt sCompare each worktree with the config. Make the links that are absent.
wt delete <worktree>wt dRemove a worktree. Delete its branch if git merged the branch.
wt prunewt pRemove each worktree whose work is finished. Delete its branch.
wt clone <url> [dir]Clone into the .bare layout. Make the first worktree.
wt shell-init fishPrint the shell integration.

A short form is the command itself, so it also wins over a worktree with the same name: wt d runs wt delete, and wt cd d goes to a worktree named d. wt init and wt clone have no short form. Each is short already, and you run it one time for a repository.

Config file

One global file holds the shared paths for each repository:

# ~/.config/wt/config.toml
[repos."github.com/you/project"]
link = [".env.override", ".auth", ".envrc"]

The key is the remote URL in a normal form. Each form of the URL gives the same key, so one file is correct for SSH and for HTTPS, and on each machine. Put this file in your dotfiles to keep the list.

wt reads $XDG_CONFIG_HOME/wt/config.toml when that variable is set.

The file holds no secret. It holds only path names. The content stays in the store, and the store stays in the repository at <common-directory>/shared/.

Installation

Cargo

cargo build --release
ln -sf "$PWD/target/release/wt"~/.local/bin/wt

Nix

nix profile install github:lorenzolfm/wt

Shell integration

Add this line to config.fish:

wt shell-init fish | source

The integration also gives completion, and it changes the directory after wt add, wt clone, wt cd and wt <worktree>:

PositionCandidates
wt <TAB>each command, each short form and each worktree
wt add <TAB>, wt a <TAB>each local branch and each remote branch
wt delete <TAB>, wt d <TAB>each worktree
wt cd <TAB>, wt c <TAB>each worktree
wt share <TAB>, wt sh <TAB>each ignored path that the config does not have, and files

License

MIT

About

Git worktrees, plus the gitignored files they need to share

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - lorenzolfm/wt: Git worktrees, plus the gitignored files they need to share · GitHub
Skip to content

Repository files navigation

wt

wt is a tool for git worktrees. It also controls the files that git ignores.

The problem

The command git worktree add makes a new checkout. It does not make the files that git ignores. Each new worktree is therefore not complete.

wt keeps one file and makes a link in each worktree. A git hook makes the links.

How to use wt

Start with a repository that you do not have

wt clone git@github.com:you/project.git
cd project/main

The command makes this structure:

project/
├── .bare/ the bare repository. It holds the store and the hook
├── .git the text `gitdir: ./.bare`
└── main/ the first worktree

Start with a repository that is already on your disk

Go into a worktree of the repository. Make the store, the config entry and the hook:

wt init

Then move each ignored file into the store:

wt share .env .auth .envrc

The command moves each file one time. It then makes a link in each worktree that is already present. One command is therefore sufficient for the full repository.

The command refuses a path that contains a tracked file. Git supplies each tracked file, and a link hides it. Give the ignored path instead. For example, give terraform/terraform.tfvars and not terraform/.

Make a worktree

wt add eng-1234

The hook makes the links in the new worktree. The program that makes the worktree is not important. wt add, git worktree add, your editor and an agent all start the hook.

The command finds the branch in this sequence:

  1. A local branch.
  2. A remote branch origin/eng-1234.
  3. A fetch of origin/eng-1234 from the network.
  4. A new branch from the default branch.

Steps 1 and 2 do not use the network. The command does step 3 only if steps 1 and 2 find no branch. Step 3 prevents an error: without it, wt can make a local branch while a remote branch with the same name is already present.

Use the option --no-fetch to stay offline. Use the option --fetch to do a fetch first.

Go to a worktree

wt deny

wt <worktree> is the short form of wt cd <worktree>. Both accept the name of the worktree directory. Both also accept the name of the branch, because a branch can move after you make the worktree. The start of a directory name is sufficient when only one worktree matches.

A command wins over a worktree with the same name, and a short form such as wt d is a command. wt ls therefore prints the worktrees, also when a worktree has the name ls. The long form wt cd ls goes to that worktree, and wt add prints a warning when it makes a directory with the name of a command.

See the worktrees

wt ls

The command prints each worktree, the branch it is on, and how many links are correct. It changes nothing.

Repair the links

The command git clean -xdf removes a link. A worktree can also be older than the config. The command wt sync makes each link that is absent:

wt sync

Remove a worktree

wt delete eng-1234

The command removes the worktree. It then deletes the branch if git merged the branch. wt d eng-1234 is the short form.

Remove each worktree whose work is finished

wt prune

The command asks origin which branches are still present. It then removes each worktree whose work is complete, and it deletes the branch:

ConditionExample
The base branch contains the branch, and origin still has it.A merge commit, and the branch is not deleted yet.
Origin no longer has the upstream branch.A squash merge or a rebase merge, and the branch is deleted.

The second condition is the one that matters for a squash merge. Such a merge writes a new commit, so the branch is not an ancestor of the base branch, and the merge is complete nonetheless.

wt prune removes no branch that is only on your machine. Each condition above needs proof that the branch reached origin: origin has it now, or the config names an upstream branch that origin no longer has. A new branch from wt add holds no commit of its own, so the base branch contains it, and the test for a merge alone cannot tell that branch from finished work. The proof of a push is the rule that keeps wt add eng-1234 safe until you push it.

The command also keeps:

ConditionReason
The worktree has a modified file or an untracked file.Use --force.
You are in the worktree.A removal would leave your shell in a path that is absent.
The worktree is on the base branch, or on no branch.There is no branch to test.

Use --dry-run to read the list and change nothing. Use --no-fetch to stay offline; the command then uses the refs it already has.

The command also removes the administrative files of a worktree whose directory you deleted by hand.

Git does not count the files that it ignores. An ignored file that is not in the store is therefore lost without a warning, as in wt delete.

wt does not destroy a file that is different

The commands wt share and wt sync compare the bytes before they make a link.

ConditionResult
The two files are the same.wt replaces the file with a link.
The two files are different.wt keeps the file and prints a message.

Use the option --force to replace a file that is different.

Commands

CommandShortFunction
wt initMake the store, the config entry and the hook. Move no files.
wt share <path>…wt shMove ignored paths into the store. Link them in each worktree.
wt add <branch> [dir]wt aMake a worktree for a branch.
wt cd <worktree>wt cPrint the path of a worktree. The shell integration then changes directory.
wt <worktree>The short form of wt cd <worktree>.
wt lswt lPrint each worktree, its branch and the condition of its links.
wt syncwt sCompare each worktree with the config. Make the links that are absent.
wt delete <worktree>wt dRemove a worktree. Delete its branch if git merged the branch.
wt prunewt pRemove each worktree whose work is finished. Delete its branch.
wt clone <url> [dir]Clone into the .bare layout. Make the first worktree.
wt shell-init fishPrint the shell integration.

A short form is the command itself, so it also wins over a worktree with the same name: wt d runs wt delete, and wt cd d goes to a worktree named d. wt init and wt clone have no short form. Each is short already, and you run it one time for a repository.

Config file

One global file holds the shared paths for each repository:

# ~/.config/wt/config.toml
[repos."github.com/you/project"]
link = [".env.override", ".auth", ".envrc"]

The key is the remote URL in a normal form. Each form of the URL gives the same key, so one file is correct for SSH and for HTTPS, and on each machine. Put this file in your dotfiles to keep the list.

wt reads $XDG_CONFIG_HOME/wt/config.toml when that variable is set.

The file holds no secret. It holds only path names. The content stays in the store, and the store stays in the repository at <common-directory>/shared/.

Installation

Cargo

cargo build --release
ln -sf "$PWD/target/release/wt"~/.local/bin/wt

Nix

nix profile install github:lorenzolfm/wt

Shell integration

Add this line to config.fish:

wt shell-init fish | source

The integration also gives completion, and it changes the directory after wt add, wt clone, wt cd and wt <worktree>:

PositionCandidates
wt <TAB>each command, each short form and each worktree
wt add <TAB>, wt a <TAB>each local branch and each remote branch
wt delete <TAB>, wt d <TAB>each worktree
wt cd <TAB>, wt c <TAB>each worktree
wt share <TAB>, wt sh <TAB>each ignored path that the config does not have, and files

License

MIT

About

Git worktrees, plus the gitignored files they need to share

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - lorenzolfm/wt: Git worktrees, plus the gitignored files they need to share · GitHub
Skip to content

Repository files navigation

wt

wt is a tool for git worktrees. It also controls the files that git ignores.

The problem

The command git worktree add makes a new checkout. It does not make the files that git ignores. Each new worktree is therefore not complete.

wt keeps one file and makes a link in each worktree. A git hook makes the links.

How to use wt

Start with a repository that you do not have

wt clone git@github.com:you/project.git
cd project/main

The command makes this structure:

project/
├── .bare/ the bare repository. It holds the store and the hook
├── .git the text `gitdir: ./.bare`
└── main/ the first worktree

Start with a repository that is already on your disk

Go into a worktree of the repository. Make the store, the config entry and the hook:

wt init

Then move each ignored file into the store:

wt share .env .auth .envrc

The command moves each file one time. It then makes a link in each worktree that is already present. One command is therefore sufficient for the full repository.

The command refuses a path that contains a tracked file. Git supplies each tracked file, and a link hides it. Give the ignored path instead. For example, give terraform/terraform.tfvars and not terraform/.

Make a worktree

wt add eng-1234

The hook makes the links in the new worktree. The program that makes the worktree is not important. wt add, git worktree add, your editor and an agent all start the hook.

The command finds the branch in this sequence:

  1. A local branch.
  2. A remote branch origin/eng-1234.
  3. A fetch of origin/eng-1234 from the network.
  4. A new branch from the default branch.

Steps 1 and 2 do not use the network. The command does step 3 only if steps 1 and 2 find no branch. Step 3 prevents an error: without it, wt can make a local branch while a remote branch with the same name is already present.

Use the option --no-fetch to stay offline. Use the option --fetch to do a fetch first.

Go to a worktree

wt deny

wt <worktree> is the short form of wt cd <worktree>. Both accept the name of the worktree directory. Both also accept the name of the branch, because a branch can move after you make the worktree. The start of a directory name is sufficient when only one worktree matches.

A command wins over a worktree with the same name, and a short form such as wt d is a command. wt ls therefore prints the worktrees, also when a worktree has the name ls. The long form wt cd ls goes to that worktree, and wt add prints a warning when it makes a directory with the name of a command.

See the worktrees

wt ls

The command prints each worktree, the branch it is on, and how many links are correct. It changes nothing.

Repair the links

The command git clean -xdf removes a link. A worktree can also be older than the config. The command wt sync makes each link that is absent:

wt sync

Remove a worktree

wt delete eng-1234

The command removes the worktree. It then deletes the branch if git merged the branch. wt d eng-1234 is the short form.

Remove each worktree whose work is finished

wt prune

The command asks origin which branches are still present. It then removes each worktree whose work is complete, and it deletes the branch:

ConditionExample
The base branch contains the branch, and origin still has it.A merge commit, and the branch is not deleted yet.
Origin no longer has the upstream branch.A squash merge or a rebase merge, and the branch is deleted.

The second condition is the one that matters for a squash merge. Such a merge writes a new commit, so the branch is not an ancestor of the base branch, and the merge is complete nonetheless.

wt prune removes no branch that is only on your machine. Each condition above needs proof that the branch reached origin: origin has it now, or the config names an upstream branch that origin no longer has. A new branch from wt add holds no commit of its own, so the base branch contains it, and the test for a merge alone cannot tell that branch from finished work. The proof of a push is the rule that keeps wt add eng-1234 safe until you push it.

The command also keeps:

ConditionReason
The worktree has a modified file or an untracked file.Use --force.
You are in the worktree.A removal would leave your shell in a path that is absent.
The worktree is on the base branch, or on no branch.There is no branch to test.

Use --dry-run to read the list and change nothing. Use --no-fetch to stay offline; the command then uses the refs it already has.

The command also removes the administrative files of a worktree whose directory you deleted by hand.

Git does not count the files that it ignores. An ignored file that is not in the store is therefore lost without a warning, as in wt delete.

wt does not destroy a file that is different

The commands wt share and wt sync compare the bytes before they make a link.

ConditionResult
The two files are the same.wt replaces the file with a link.
The two files are different.wt keeps the file and prints a message.

Use the option --force to replace a file that is different.

Commands

CommandShortFunction
wt initMake the store, the config entry and the hook. Move no files.
wt share <path>…wt shMove ignored paths into the store. Link them in each worktree.
wt add <branch> [dir]wt aMake a worktree for a branch.
wt cd <worktree>wt cPrint the path of a worktree. The shell integration then changes directory.
wt <worktree>The short form of wt cd <worktree>.
wt lswt lPrint each worktree, its branch and the condition of its links.
wt syncwt sCompare each worktree with the config. Make the links that are absent.
wt delete <worktree>wt dRemove a worktree. Delete its branch if git merged the branch.
wt prunewt pRemove each worktree whose work is finished. Delete its branch.
wt clone <url> [dir]Clone into the .bare layout. Make the first worktree.
wt shell-init fishPrint the shell integration.

A short form is the command itself, so it also wins over a worktree with the same name: wt d runs wt delete, and wt cd d goes to a worktree named d. wt init and wt clone have no short form. Each is short already, and you run it one time for a repository.

Config file

One global file holds the shared paths for each repository:

# ~/.config/wt/config.toml
[repos."github.com/you/project"]
link = [".env.override", ".auth", ".envrc"]

The key is the remote URL in a normal form. Each form of the URL gives the same key, so one file is correct for SSH and for HTTPS, and on each machine. Put this file in your dotfiles to keep the list.

wt reads $XDG_CONFIG_HOME/wt/config.toml when that variable is set.

The file holds no secret. It holds only path names. The content stays in the store, and the store stays in the repository at <common-directory>/shared/.

Installation

Cargo

cargo build --release
ln -sf "$PWD/target/release/wt"~/.local/bin/wt

Nix

nix profile install github:lorenzolfm/wt

Shell integration

Add this line to config.fish:

wt shell-init fish | source

The integration also gives completion, and it changes the directory after wt add, wt clone, wt cd and wt <worktree>:

PositionCandidates
wt <TAB>each command, each short form and each worktree
wt add <TAB>, wt a <TAB>each local branch and each remote branch
wt delete <TAB>, wt d <TAB>each worktree
wt cd <TAB>, wt c <TAB>each worktree
wt share <TAB>, wt sh <TAB>each ignored path that the config does not have, and files

License

MIT

About

Git worktrees, plus the gitignored files they need to share

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - lorenzolfm/wt: Git worktrees, plus the gitignored files they need to share · GitHub
Skip to content

Repository files navigation

wt

wt is a tool for git worktrees. It also controls the files that git ignores.

The problem

The command git worktree add makes a new checkout. It does not make the files that git ignores. Each new worktree is therefore not complete.

wt keeps one file and makes a link in each worktree. A git hook makes the links.

How to use wt

Start with a repository that you do not have

wt clone git@github.com:you/project.git
cd project/main

The command makes this structure:

project/
├── .bare/ the bare repository. It holds the store and the hook
├── .git the text `gitdir: ./.bare`
└── main/ the first worktree

Start with a repository that is already on your disk

Go into a worktree of the repository. Make the store, the config entry and the hook:

wt init

Then move each ignored file into the store:

wt share .env .auth .envrc

The command moves each file one time. It then makes a link in each worktree that is already present. One command is therefore sufficient for the full repository.

The command refuses a path that contains a tracked file. Git supplies each tracked file, and a link hides it. Give the ignored path instead. For example, give terraform/terraform.tfvars and not terraform/.

Make a worktree

wt add eng-1234

The hook makes the links in the new worktree. The program that makes the worktree is not important. wt add, git worktree add, your editor and an agent all start the hook.

The command finds the branch in this sequence:

  1. A local branch.
  2. A remote branch origin/eng-1234.
  3. A fetch of origin/eng-1234 from the network.
  4. A new branch from the default branch.

Steps 1 and 2 do not use the network. The command does step 3 only if steps 1 and 2 find no branch. Step 3 prevents an error: without it, wt can make a local branch while a remote branch with the same name is already present.

Use the option --no-fetch to stay offline. Use the option --fetch to do a fetch first.

Go to a worktree

wt deny

wt <worktree> is the short form of wt cd <worktree>. Both accept the name of the worktree directory. Both also accept the name of the branch, because a branch can move after you make the worktree. The start of a directory name is sufficient when only one worktree matches.

A command wins over a worktree with the same name, and a short form such as wt d is a command. wt ls therefore prints the worktrees, also when a worktree has the name ls. The long form wt cd ls goes to that worktree, and wt add prints a warning when it makes a directory with the name of a command.

See the worktrees

wt ls

The command prints each worktree, the branch it is on, and how many links are correct. It changes nothing.

Repair the links

The command git clean -xdf removes a link. A worktree can also be older than the config. The command wt sync makes each link that is absent:

wt sync

Remove a worktree

wt delete eng-1234

The command removes the worktree. It then deletes the branch if git merged the branch. wt d eng-1234 is the short form.

Remove each worktree whose work is finished

wt prune

The command asks origin which branches are still present. It then removes each worktree whose work is complete, and it deletes the branch:

ConditionExample
The base branch contains the branch, and origin still has it.A merge commit, and the branch is not deleted yet.
Origin no longer has the upstream branch.A squash merge or a rebase merge, and the branch is deleted.

The second condition is the one that matters for a squash merge. Such a merge writes a new commit, so the branch is not an ancestor of the base branch, and the merge is complete nonetheless.

wt prune removes no branch that is only on your machine. Each condition above needs proof that the branch reached origin: origin has it now, or the config names an upstream branch that origin no longer has. A new branch from wt add holds no commit of its own, so the base branch contains it, and the test for a merge alone cannot tell that branch from finished work. The proof of a push is the rule that keeps wt add eng-1234 safe until you push it.

The command also keeps:

ConditionReason
The worktree has a modified file or an untracked file.Use --force.
You are in the worktree.A removal would leave your shell in a path that is absent.
The worktree is on the base branch, or on no branch.There is no branch to test.

Use --dry-run to read the list and change nothing. Use --no-fetch to stay offline; the command then uses the refs it already has.

The command also removes the administrative files of a worktree whose directory you deleted by hand.

Git does not count the files that it ignores. An ignored file that is not in the store is therefore lost without a warning, as in wt delete.

wt does not destroy a file that is different

The commands wt share and wt sync compare the bytes before they make a link.

ConditionResult
The two files are the same.wt replaces the file with a link.
The two files are different.wt keeps the file and prints a message.

Use the option --force to replace a file that is different.

Commands

CommandShortFunction
wt initMake the store, the config entry and the hook. Move no files.
wt share <path>…wt shMove ignored paths into the store. Link them in each worktree.
wt add <branch> [dir]wt aMake a worktree for a branch.
wt cd <worktree>wt cPrint the path of a worktree. The shell integration then changes directory.
wt <worktree>The short form of wt cd <worktree>.
wt lswt lPrint each worktree, its branch and the condition of its links.
wt syncwt sCompare each worktree with the config. Make the links that are absent.
wt delete <worktree>wt dRemove a worktree. Delete its branch if git merged the branch.
wt prunewt pRemove each worktree whose work is finished. Delete its branch.
wt clone <url> [dir]Clone into the .bare layout. Make the first worktree.
wt shell-init fishPrint the shell integration.

A short form is the command itself, so it also wins over a worktree with the same name: wt d runs wt delete, and wt cd d goes to a worktree named d. wt init and wt clone have no short form. Each is short already, and you run it one time for a repository.

Config file

One global file holds the shared paths for each repository:

# ~/.config/wt/config.toml
[repos."github.com/you/project"]
link = [".env.override", ".auth", ".envrc"]

The key is the remote URL in a normal form. Each form of the URL gives the same key, so one file is correct for SSH and for HTTPS, and on each machine. Put this file in your dotfiles to keep the list.

wt reads $XDG_CONFIG_HOME/wt/config.toml when that variable is set.

The file holds no secret. It holds only path names. The content stays in the store, and the store stays in the repository at <common-directory>/shared/.

Installation

Cargo

cargo build --release
ln -sf "$PWD/target/release/wt"~/.local/bin/wt

Nix

nix profile install github:lorenzolfm/wt

Shell integration

Add this line to config.fish:

wt shell-init fish | source

The integration also gives completion, and it changes the directory after wt add, wt clone, wt cd and wt <worktree>:

PositionCandidates
wt <TAB>each command, each short form and each worktree
wt add <TAB>, wt a <TAB>each local branch and each remote branch
wt delete <TAB>, wt d <TAB>each worktree
wt cd <TAB>, wt c <TAB>each worktree
wt share <TAB>, wt sh <TAB>each ignored path that the config does not have, and files

License

MIT

About

Git worktrees, plus the gitignored files they need to share

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - lorenzolfm/wt: Git worktrees, plus the gitignored files they need to share · GitHub
Skip to content

Repository files navigation

wt

wt is a tool for git worktrees. It also controls the files that git ignores.

The problem

The command git worktree add makes a new checkout. It does not make the files that git ignores. Each new worktree is therefore not complete.

wt keeps one file and makes a link in each worktree. A git hook makes the links.

How to use wt

Start with a repository that you do not have

wt clone git@github.com:you/project.git
cd project/main

The command makes this structure:

project/
├── .bare/ the bare repository. It holds the store and the hook
├── .git the text `gitdir: ./.bare`
└── main/ the first worktree

Start with a repository that is already on your disk

Go into a worktree of the repository. Make the store, the config entry and the hook:

wt init

Then move each ignored file into the store:

wt share .env .auth .envrc

The command moves each file one time. It then makes a link in each worktree that is already present. One command is therefore sufficient for the full repository.

The command refuses a path that contains a tracked file. Git supplies each tracked file, and a link hides it. Give the ignored path instead. For example, give terraform/terraform.tfvars and not terraform/.

Make a worktree

wt add eng-1234

The hook makes the links in the new worktree. The program that makes the worktree is not important. wt add, git worktree add, your editor and an agent all start the hook.

The command finds the branch in this sequence:

  1. A local branch.
  2. A remote branch origin/eng-1234.
  3. A fetch of origin/eng-1234 from the network.
  4. A new branch from the default branch.

Steps 1 and 2 do not use the network. The command does step 3 only if steps 1 and 2 find no branch. Step 3 prevents an error: without it, wt can make a local branch while a remote branch with the same name is already present.

Use the option --no-fetch to stay offline. Use the option --fetch to do a fetch first.

Go to a worktree

wt deny

wt <worktree> is the short form of wt cd <worktree>. Both accept the name of the worktree directory. Both also accept the name of the branch, because a branch can move after you make the worktree. The start of a directory name is sufficient when only one worktree matches.

A command wins over a worktree with the same name, and a short form such as wt d is a command. wt ls therefore prints the worktrees, also when a worktree has the name ls. The long form wt cd ls goes to that worktree, and wt add prints a warning when it makes a directory with the name of a command.

See the worktrees

wt ls

The command prints each worktree, the branch it is on, and how many links are correct. It changes nothing.

Repair the links

The command git clean -xdf removes a link. A worktree can also be older than the config. The command wt sync makes each link that is absent:

wt sync

Remove a worktree

wt delete eng-1234

The command removes the worktree. It then deletes the branch if git merged the branch. wt d eng-1234 is the short form.

Remove each worktree whose work is finished

wt prune

The command asks origin which branches are still present. It then removes each worktree whose work is complete, and it deletes the branch:

ConditionExample
The base branch contains the branch, and origin still has it.A merge commit, and the branch is not deleted yet.
Origin no longer has the upstream branch.A squash merge or a rebase merge, and the branch is deleted.

The second condition is the one that matters for a squash merge. Such a merge writes a new commit, so the branch is not an ancestor of the base branch, and the merge is complete nonetheless.

wt prune removes no branch that is only on your machine. Each condition above needs proof that the branch reached origin: origin has it now, or the config names an upstream branch that origin no longer has. A new branch from wt add holds no commit of its own, so the base branch contains it, and the test for a merge alone cannot tell that branch from finished work. The proof of a push is the rule that keeps wt add eng-1234 safe until you push it.

The command also keeps:

ConditionReason
The worktree has a modified file or an untracked file.Use --force.
You are in the worktree.A removal would leave your shell in a path that is absent.
The worktree is on the base branch, or on no branch.There is no branch to test.

Use --dry-run to read the list and change nothing. Use --no-fetch to stay offline; the command then uses the refs it already has.

The command also removes the administrative files of a worktree whose directory you deleted by hand.

Git does not count the files that it ignores. An ignored file that is not in the store is therefore lost without a warning, as in wt delete.

wt does not destroy a file that is different

The commands wt share and wt sync compare the bytes before they make a link.

ConditionResult
The two files are the same.wt replaces the file with a link.
The two files are different.wt keeps the file and prints a message.

Use the option --force to replace a file that is different.

Commands

CommandShortFunction
wt initMake the store, the config entry and the hook. Move no files.
wt share <path>…wt shMove ignored paths into the store. Link them in each worktree.
wt add <branch> [dir]wt aMake a worktree for a branch.
wt cd <worktree>wt cPrint the path of a worktree. The shell integration then changes directory.
wt <worktree>The short form of wt cd <worktree>.
wt lswt lPrint each worktree, its branch and the condition of its links.
wt syncwt sCompare each worktree with the config. Make the links that are absent.
wt delete <worktree>wt dRemove a worktree. Delete its branch if git merged the branch.
wt prunewt pRemove each worktree whose work is finished. Delete its branch.
wt clone <url> [dir]Clone into the .bare layout. Make the first worktree.
wt shell-init fishPrint the shell integration.

A short form is the command itself, so it also wins over a worktree with the same name: wt d runs wt delete, and wt cd d goes to a worktree named d. wt init and wt clone have no short form. Each is short already, and you run it one time for a repository.

Config file

One global file holds the shared paths for each repository:

# ~/.config/wt/config.toml
[repos."github.com/you/project"]
link = [".env.override", ".auth", ".envrc"]

The key is the remote URL in a normal form. Each form of the URL gives the same key, so one file is correct for SSH and for HTTPS, and on each machine. Put this file in your dotfiles to keep the list.

wt reads $XDG_CONFIG_HOME/wt/config.toml when that variable is set.

The file holds no secret. It holds only path names. The content stays in the store, and the store stays in the repository at <common-directory>/shared/.

Installation

Cargo

cargo build --release
ln -sf "$PWD/target/release/wt"~/.local/bin/wt

Nix

nix profile install github:lorenzolfm/wt

Shell integration

Add this line to config.fish:

wt shell-init fish | source

The integration also gives completion, and it changes the directory after wt add, wt clone, wt cd and wt <worktree>:

PositionCandidates
wt <TAB>each command, each short form and each worktree
wt add <TAB>, wt a <TAB>each local branch and each remote branch
wt delete <TAB>, wt d <TAB>each worktree
wt cd <TAB>, wt c <TAB>each worktree
wt share <TAB>, wt sh <TAB>each ignored path that the config does not have, and files

License

MIT

About

Git worktrees, plus the gitignored files they need to share

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - lorenzolfm/wt: Git worktrees, plus the gitignored files they need to share · GitHub
Skip to content

Repository files navigation

wt

wt is a tool for git worktrees. It also controls the files that git ignores.

The problem

The command git worktree add makes a new checkout. It does not make the files that git ignores. Each new worktree is therefore not complete.

wt keeps one file and makes a link in each worktree. A git hook makes the links.

How to use wt

Start with a repository that you do not have

wt clone git@github.com:you/project.git
cd project/main

The command makes this structure:

project/
├── .bare/ the bare repository. It holds the store and the hook
├── .git the text `gitdir: ./.bare`
└── main/ the first worktree

Start with a repository that is already on your disk

Go into a worktree of the repository. Make the store, the config entry and the hook:

wt init

Then move each ignored file into the store:

wt share .env .auth .envrc

The command moves each file one time. It then makes a link in each worktree that is already present. One command is therefore sufficient for the full repository.

The command refuses a path that contains a tracked file. Git supplies each tracked file, and a link hides it. Give the ignored path instead. For example, give terraform/terraform.tfvars and not terraform/.

Make a worktree

wt add eng-1234

The hook makes the links in the new worktree. The program that makes the worktree is not important. wt add, git worktree add, your editor and an agent all start the hook.

The command finds the branch in this sequence:

  1. A local branch.
  2. A remote branch origin/eng-1234.
  3. A fetch of origin/eng-1234 from the network.
  4. A new branch from the default branch.

Steps 1 and 2 do not use the network. The command does step 3 only if steps 1 and 2 find no branch. Step 3 prevents an error: without it, wt can make a local branch while a remote branch with the same name is already present.

Use the option --no-fetch to stay offline. Use the option --fetch to do a fetch first.

Go to a worktree

wt deny

wt <worktree> is the short form of wt cd <worktree>. Both accept the name of the worktree directory. Both also accept the name of the branch, because a branch can move after you make the worktree. The start of a directory name is sufficient when only one worktree matches.

A command wins over a worktree with the same name, and a short form such as wt d is a command. wt ls therefore prints the worktrees, also when a worktree has the name ls. The long form wt cd ls goes to that worktree, and wt add prints a warning when it makes a directory with the name of a command.

See the worktrees

wt ls

The command prints each worktree, the branch it is on, and how many links are correct. It changes nothing.

Repair the links

The command git clean -xdf removes a link. A worktree can also be older than the config. The command wt sync makes each link that is absent:

wt sync

Remove a worktree

wt delete eng-1234

The command removes the worktree. It then deletes the branch if git merged the branch. wt d eng-1234 is the short form.

Remove each worktree whose work is finished

wt prune

The command asks origin which branches are still present. It then removes each worktree whose work is complete, and it deletes the branch:

ConditionExample
The base branch contains the branch, and origin still has it.A merge commit, and the branch is not deleted yet.
Origin no longer has the upstream branch.A squash merge or a rebase merge, and the branch is deleted.

The second condition is the one that matters for a squash merge. Such a merge writes a new commit, so the branch is not an ancestor of the base branch, and the merge is complete nonetheless.

wt prune removes no branch that is only on your machine. Each condition above needs proof that the branch reached origin: origin has it now, or the config names an upstream branch that origin no longer has. A new branch from wt add holds no commit of its own, so the base branch contains it, and the test for a merge alone cannot tell that branch from finished work. The proof of a push is the rule that keeps wt add eng-1234 safe until you push it.

The command also keeps:

ConditionReason
The worktree has a modified file or an untracked file.Use --force.
You are in the worktree.A removal would leave your shell in a path that is absent.
The worktree is on the base branch, or on no branch.There is no branch to test.

Use --dry-run to read the list and change nothing. Use --no-fetch to stay offline; the command then uses the refs it already has.

The command also removes the administrative files of a worktree whose directory you deleted by hand.

Git does not count the files that it ignores. An ignored file that is not in the store is therefore lost without a warning, as in wt delete.

wt does not destroy a file that is different

The commands wt share and wt sync compare the bytes before they make a link.

ConditionResult
The two files are the same.wt replaces the file with a link.
The two files are different.wt keeps the file and prints a message.

Use the option --force to replace a file that is different.

Commands

CommandShortFunction
wt initMake the store, the config entry and the hook. Move no files.
wt share <path>…wt shMove ignored paths into the store. Link them in each worktree.
wt add <branch> [dir]wt aMake a worktree for a branch.
wt cd <worktree>wt cPrint the path of a worktree. The shell integration then changes directory.
wt <worktree>The short form of wt cd <worktree>.
wt lswt lPrint each worktree, its branch and the condition of its links.
wt syncwt sCompare each worktree with the config. Make the links that are absent.
wt delete <worktree>wt dRemove a worktree. Delete its branch if git merged the branch.
wt prunewt pRemove each worktree whose work is finished. Delete its branch.
wt clone <url> [dir]Clone into the .bare layout. Make the first worktree.
wt shell-init fishPrint the shell integration.

A short form is the command itself, so it also wins over a worktree with the same name: wt d runs wt delete, and wt cd d goes to a worktree named d. wt init and wt clone have no short form. Each is short already, and you run it one time for a repository.

Config file

One global file holds the shared paths for each repository:

# ~/.config/wt/config.toml
[repos."github.com/you/project"]
link = [".env.override", ".auth", ".envrc"]

The key is the remote URL in a normal form. Each form of the URL gives the same key, so one file is correct for SSH and for HTTPS, and on each machine. Put this file in your dotfiles to keep the list.

wt reads $XDG_CONFIG_HOME/wt/config.toml when that variable is set.

The file holds no secret. It holds only path names. The content stays in the store, and the store stays in the repository at <common-directory>/shared/.

Installation

Cargo

cargo build --release
ln -sf "$PWD/target/release/wt"~/.local/bin/wt

Nix

nix profile install github:lorenzolfm/wt

Shell integration

Add this line to config.fish:

wt shell-init fish | source

The integration also gives completion, and it changes the directory after wt add, wt clone, wt cd and wt <worktree>:

PositionCandidates
wt <TAB>each command, each short form and each worktree
wt add <TAB>, wt a <TAB>each local branch and each remote branch
wt delete <TAB>, wt d <TAB>each worktree
wt cd <TAB>, wt c <TAB>each worktree
wt share <TAB>, wt sh <TAB>each ignored path that the config does not have, and files

License

MIT

About

Git worktrees, plus the gitignored files they need to share

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - lorenzolfm/wt: Git worktrees, plus the gitignored files they need to share · GitHub
Skip to content

Repository files navigation

wt

wt is a tool for git worktrees. It also controls the files that git ignores.

The problem

The command git worktree add makes a new checkout. It does not make the files that git ignores. Each new worktree is therefore not complete.

wt keeps one file and makes a link in each worktree. A git hook makes the links.

How to use wt

Start with a repository that you do not have

wt clone git@github.com:you/project.git
cd project/main

The command makes this structure:

project/
├── .bare/ the bare repository. It holds the store and the hook
├── .git the text `gitdir: ./.bare`
└── main/ the first worktree

Start with a repository that is already on your disk

Go into a worktree of the repository. Make the store, the config entry and the hook:

wt init

Then move each ignored file into the store:

wt share .env .auth .envrc

The command moves each file one time. It then makes a link in each worktree that is already present. One command is therefore sufficient for the full repository.

The command refuses a path that contains a tracked file. Git supplies each tracked file, and a link hides it. Give the ignored path instead. For example, give terraform/terraform.tfvars and not terraform/.

Make a worktree

wt add eng-1234

The hook makes the links in the new worktree. The program that makes the worktree is not important. wt add, git worktree add, your editor and an agent all start the hook.

The command finds the branch in this sequence:

  1. A local branch.
  2. A remote branch origin/eng-1234.
  3. A fetch of origin/eng-1234 from the network.
  4. A new branch from the default branch.

Steps 1 and 2 do not use the network. The command does step 3 only if steps 1 and 2 find no branch. Step 3 prevents an error: without it, wt can make a local branch while a remote branch with the same name is already present.

Use the option --no-fetch to stay offline. Use the option --fetch to do a fetch first.

Go to a worktree

wt deny

wt <worktree> is the short form of wt cd <worktree>. Both accept the name of the worktree directory. Both also accept the name of the branch, because a branch can move after you make the worktree. The start of a directory name is sufficient when only one worktree matches.

A command wins over a worktree with the same name, and a short form such as wt d is a command. wt ls therefore prints the worktrees, also when a worktree has the name ls. The long form wt cd ls goes to that worktree, and wt add prints a warning when it makes a directory with the name of a command.

See the worktrees

wt ls

The command prints each worktree, the branch it is on, and how many links are correct. It changes nothing.

Repair the links

The command git clean -xdf removes a link. A worktree can also be older than the config. The command wt sync makes each link that is absent:

wt sync

Remove a worktree

wt delete eng-1234

The command removes the worktree. It then deletes the branch if git merged the branch. wt d eng-1234 is the short form.

Remove each worktree whose work is finished

wt prune

The command asks origin which branches are still present. It then removes each worktree whose work is complete, and it deletes the branch:

ConditionExample
The base branch contains the branch, and origin still has it.A merge commit, and the branch is not deleted yet.
Origin no longer has the upstream branch.A squash merge or a rebase merge, and the branch is deleted.

The second condition is the one that matters for a squash merge. Such a merge writes a new commit, so the branch is not an ancestor of the base branch, and the merge is complete nonetheless.

wt prune removes no branch that is only on your machine. Each condition above needs proof that the branch reached origin: origin has it now, or the config names an upstream branch that origin no longer has. A new branch from wt add holds no commit of its own, so the base branch contains it, and the test for a merge alone cannot tell that branch from finished work. The proof of a push is the rule that keeps wt add eng-1234 safe until you push it.

The command also keeps:

ConditionReason
The worktree has a modified file or an untracked file.Use --force.
You are in the worktree.A removal would leave your shell in a path that is absent.
The worktree is on the base branch, or on no branch.There is no branch to test.

Use --dry-run to read the list and change nothing. Use --no-fetch to stay offline; the command then uses the refs it already has.

The command also removes the administrative files of a worktree whose directory you deleted by hand.

Git does not count the files that it ignores. An ignored file that is not in the store is therefore lost without a warning, as in wt delete.

wt does not destroy a file that is different

The commands wt share and wt sync compare the bytes before they make a link.

ConditionResult
The two files are the same.wt replaces the file with a link.
The two files are different.wt keeps the file and prints a message.

Use the option --force to replace a file that is different.

Commands

CommandShortFunction
wt initMake the store, the config entry and the hook. Move no files.
wt share <path>…wt shMove ignored paths into the store. Link them in each worktree.
wt add <branch> [dir]wt aMake a worktree for a branch.
wt cd <worktree>wt cPrint the path of a worktree. The shell integration then changes directory.
wt <worktree>The short form of wt cd <worktree>.
wt lswt lPrint each worktree, its branch and the condition of its links.
wt syncwt sCompare each worktree with the config. Make the links that are absent.
wt delete <worktree>wt dRemove a worktree. Delete its branch if git merged the branch.
wt prunewt pRemove each worktree whose work is finished. Delete its branch.
wt clone <url> [dir]Clone into the .bare layout. Make the first worktree.
wt shell-init fishPrint the shell integration.

A short form is the command itself, so it also wins over a worktree with the same name: wt d runs wt delete, and wt cd d goes to a worktree named d. wt init and wt clone have no short form. Each is short already, and you run it one time for a repository.

Config file

One global file holds the shared paths for each repository:

# ~/.config/wt/config.toml
[repos."github.com/you/project"]
link = [".env.override", ".auth", ".envrc"]

The key is the remote URL in a normal form. Each form of the URL gives the same key, so one file is correct for SSH and for HTTPS, and on each machine. Put this file in your dotfiles to keep the list.

wt reads $XDG_CONFIG_HOME/wt/config.toml when that variable is set.

The file holds no secret. It holds only path names. The content stays in the store, and the store stays in the repository at <common-directory>/shared/.

Installation

Cargo

cargo build --release
ln -sf "$PWD/target/release/wt"~/.local/bin/wt

Nix

nix profile install github:lorenzolfm/wt

Shell integration

Add this line to config.fish:

wt shell-init fish | source

The integration also gives completion, and it changes the directory after wt add, wt clone, wt cd and wt <worktree>:

PositionCandidates
wt <TAB>each command, each short form and each worktree
wt add <TAB>, wt a <TAB>each local branch and each remote branch
wt delete <TAB>, wt d <TAB>each worktree
wt cd <TAB>, wt c <TAB>each worktree
wt share <TAB>, wt sh <TAB>each ignored path that the config does not have, and files

License

MIT

About

Git worktrees, plus the gitignored files they need to share

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages