Skip to content

Commit 8e8666b

Browse files
committed
Add cd-into-temporary-worktree value for machete.traverse.whenBranchNotCheckedOutInAnyWorktree git config key
1 parent 9702ab8 commit 8e8666b

13 files changed

Lines changed: 355 additions & 60 deletions

RELEASE_NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Release notes
22

3-
## New in git-machete 3.39.3
3+
## New in git-machete 3.40.0
44

5+
- added: `cd-into-temporary-worktree` value for `machete.traverse.whenBranchNotCheckedOutInAnyWorktree` git config key (suggested by @andrii0lomakin)
56
- changed: when a branch is behind remote (needs a pull), `git machete traverse` now skips rebase,
67
just as it did already when branch is diverged from & older than remote (suggested by @lsierant)
78
- fixed: when `git machete traverse` failed on a rebase within a worktree,

docs/man/git-machete.1

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
2828
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
2929
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
3030
..
31-
.TH "GIT-MACHETE" "1" "Mar 30, 2026" "" "git-machete"
31+
.TH "GIT-MACHETE" "1" "Mar 31, 2026" "" "git-machete"
3232
.SH NAME
33-
git-machete \- git-machete 3.39.3
33+
git-machete \- git-machete 3.40.0
3434
.sp
3535
git machete is a robust tool that \fBsimplifies your git workflows\fP\&.
3636
.sp
@@ -494,13 +494,20 @@ The default value of this key is \fBtrue\fP\&.
494494
Configuration key value can be overridden by the presence of the \fB\-\-push\fP or \fB\-\-push\-untracked\fP flags.
495495
.TP
496496
.B \fBmachete.traverse.whenBranchNotCheckedOutInAnyWorktree\fP:
497-
Controls the behavior of \fBgit machete traverse\fP when checking out a branch that is not currently checked out in any worktree.
497+
Controls the behavior of \fBgit machete traverse\fP when it needs to act on a branch that is not currently checked out in any worktree.
498498
.sp
499-
The default value is \fBcd\-into\-main\-worktree\fP, which means that \fBtraverse\fP will change directory to the main worktree before checking out the branch.
500-
.sp
501-
Set to \fBstay\-in\-the\-current\-worktree\fP to make \fBtraverse\fP stay in whatever worktree has already been reached by that point,
502-
and check out the branch there instead.
503-
Note that this worktree might be different then the initial working directory where \fBtraverse\fP started.
499+
Allowed values:
500+
.INDENT 7.0
501+
.IP \(bu 2
502+
\fBcd\-into\-main\-worktree\fP (default): change directory to the main worktree and check out the branch there.
503+
.IP \(bu 2
504+
\fBstay\-in\-the\-current\-worktree\fP: check out the branch in whichever worktree \fBtraverse\fP is currently operating in,
505+
without changing directory. Note that this worktree might differ from the one where \fBtraverse\fP originally started.
506+
.IP \(bu 2
507+
\fBcd\-into\-temporary\-worktree\fP: create a new worktree in a temporary directory, check out the branch there,
508+
and remove this temporary worktree once \fBtraverse\fP moves on to the next branch (or finishes).
509+
This ensures that no existing (non\-temporary) worktree has its checked\-out branch changed by \fBtraverse\fP\&.
510+
.UNINDENT
504511
.TP
505512
.B \fBmachete.worktree.useTopLevelMacheteFile\fP:
506513
The default value of this key is \fBtrue\fP, which means that the path to branch layout file will be \fB\&.git/machete\fP
@@ -2062,7 +2069,7 @@ whether a gray edge is displayed in \fBstatus\fP,
20622069
whether \fBtraverse\fP suggests to slide out the branch.
20632070
.UNINDENT
20642071
.TP
2065-
.B \fBmachete.status.extraSpaceBeforeBranchName\fP
2072+
.B \fBmachete.status.extraSpaceBeforeBranchName\fP:
20662073
To make it easier to select branch name from the \fBstatus\fP output on certain terminals
20672074
(like Alacritty \%<https://\:github\:.com/\:alacritty/\:alacritty>), you can add an extra space between └─ and branch name
20682075
by setting \fBgit config machete.status.extraSpaceBeforeBranchName true\fP\&.
@@ -2303,19 +2310,26 @@ If set to \fBfalse\fP, this remote will not be fetched before the traversal.
23032310
The default value of this key is \fBtrue\fP\&.
23042311
This is useful for excluding remotes that are temporarily offline, or take a long time to respond.
23052312
.TP
2306-
.B \fBmachete.traverse.push\fP
2313+
.B \fBmachete.traverse.push\fP:
23072314
Set to \fBfalse\fP to change the behavior of \fBgit machete traverse\fP so that it doesn\(aqt push branches by default.
23082315
The default value of this key is \fBtrue\fP\&.
23092316
Configuration key value can be overridden by the presence of the \fB\-\-push\fP or \fB\-\-push\-untracked\fP flags.
23102317
.TP
2311-
.B \fBmachete.traverse.whenBranchNotCheckedOutInAnyWorktree\fP
2312-
Controls the behavior of \fBgit machete traverse\fP when checking out a branch that is not currently checked out in any worktree.
2313-
.sp
2314-
The default value is \fBcd\-into\-main\-worktree\fP, which means that \fBtraverse\fP will change directory to the main worktree before checking out the branch.
2318+
.B \fBmachete.traverse.whenBranchNotCheckedOutInAnyWorktree\fP:
2319+
Controls the behavior of \fBgit machete traverse\fP when it needs to act on a branch that is not currently checked out in any worktree.
23152320
.sp
2316-
Set to \fBstay\-in\-the\-current\-worktree\fP to make \fBtraverse\fP stay in whatever worktree has already been reached by that point,
2317-
and check out the branch there instead.
2318-
Note that this worktree might be different then the initial working directory where \fBtraverse\fP started.
2321+
Allowed values:
2322+
.INDENT 7.0
2323+
.IP \(bu 2
2324+
\fBcd\-into\-main\-worktree\fP (default): change directory to the main worktree and check out the branch there.
2325+
.IP \(bu 2
2326+
\fBstay\-in\-the\-current\-worktree\fP: check out the branch in whichever worktree \fBtraverse\fP is currently operating in,
2327+
without changing directory. Note that this worktree might differ from the one where \fBtraverse\fP originally started.
2328+
.IP \(bu 2
2329+
\fBcd\-into\-temporary\-worktree\fP: create a new worktree in a temporary directory, check out the branch there,
2330+
and remove this temporary worktree once \fBtraverse\fP moves on to the next branch (or finishes).
2331+
This ensures that no existing (non\-temporary) worktree has its checked\-out branch changed by \fBtraverse\fP\&.
2332+
.UNINDENT
23192333
.UNINDENT
23202334
.sp
23212335
\fBEnvironment variables:\fP

docs/source/cli/status.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,6 @@ When colors are disabled, relation between branches is represented in the follow
9797
``machete.squashMergeDetection``:
9898
.. include:: git-config-keys/squashMergeDetection.rst
9999

100-
``machete.status.extraSpaceBeforeBranchName``
100+
``machete.status.extraSpaceBeforeBranchName``:
101101
.. include:: git-config-keys/status_extraSpaceBeforeBranchName.rst
102102
:end-line: 3

docs/source/cli/traverse.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,10 @@ This behavior can be customized using ``machete.traverse.whenBranchNotCheckedOut
173173
``machete.traverse.fetch.<remote>``:
174174
.. include:: git-config-keys/traverse_fetch_remote.rst
175175

176-
``machete.traverse.push``
176+
``machete.traverse.push``:
177177
.. include:: git-config-keys/traverse_push.rst
178178

179-
``machete.traverse.whenBranchNotCheckedOutInAnyWorktree``
179+
``machete.traverse.whenBranchNotCheckedOutInAnyWorktree``:
180180
.. include:: git-config-keys/traverse_whenBranchNotCheckedOutInAnyWorktree.rst
181181

182182
**Environment variables:**
Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
Controls the behavior of ``git machete traverse`` when checking out a branch that is not currently checked out in any worktree.
1+
Controls the behavior of ``git machete traverse`` when it needs to act on a branch that is not currently checked out in any worktree.
22

3-
The default value is ``cd-into-main-worktree``, which means that ``traverse`` will change directory to the main worktree before checking out the branch.
3+
Allowed values:
44

5-
Set to ``stay-in-the-current-worktree`` to make ``traverse`` stay in whatever worktree has already been reached by that point,
6-
and check out the branch there instead.
7-
Note that this worktree might be different then the initial working directory where ``traverse`` started.
5+
* ``cd-into-main-worktree`` (default): change directory to the main worktree and check out the branch there.
6+
7+
* ``stay-in-the-current-worktree``: check out the branch in whichever worktree ``traverse`` is currently operating in,
8+
without changing directory. Note that this worktree might differ from the one where ``traverse`` originally started.
9+
10+
* ``cd-into-temporary-worktree``: create a new worktree in a temporary directory, check out the branch there,
11+
and remove this temporary worktree once ``traverse`` moves on to the next branch (or finishes).
12+
This ensures that no existing (non-temporary) worktree has its checked-out branch changed by ``traverse``.

git_machete/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '3.39.3'
1+
__version__ = '3.40.0'

git_machete/client/traverse.py

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import itertools
2+
import tempfile
23
from enum import auto
34
from typing import Dict, List, Optional, Type, Union
45

56
from git_machete.annotation import Annotation, Qualifiers
67
from git_machete.client.base import PickRoot
78
from git_machete.client.with_code_hosting import MacheteClientWithCodeHosting
8-
from git_machete.code_hosting import PullRequest
9+
from git_machete.code_hosting import CodeHostingSpec, PullRequest
910
from git_machete.config import (SquashMergeDetection,
1011
TraverseWhenBranchNotCheckedOutInAnyWorktree)
1112
from git_machete.git_operations import (GitContext, LocalBranchShortName,
@@ -49,6 +50,13 @@ def from_string_or_branch(cls: Type['TraverseStartFrom'], value: str,
4950

5051

5152
class TraverseMacheteClient(MacheteClientWithCodeHosting):
53+
54+
def __init__(self, git: GitContext, spec: CodeHostingSpec):
55+
super().__init__(git, spec)
56+
self.__temporary_worktree_path: Optional[str] = None
57+
self.__dir_before_temporary_worktree: Optional[str] = None
58+
self.__worktree_root_dir_for_branch: Dict[LocalBranchShortName, str] = {}
59+
5260
def _update_worktrees_cache_after_checkout(self, checked_out_branch: LocalBranchShortName) -> None:
5361
"""
5462
Update the worktrees cache after a checkout operation in the current worktree.
@@ -60,45 +68,71 @@ def _update_worktrees_cache_after_checkout(self, checked_out_branch: LocalBranch
6068
current_worktree_root_dir = self._git.get_current_worktree_root_dir()
6169

6270
for branch, path in self.__worktree_root_dir_for_branch.items():
63-
if path == current_worktree_root_dir: # pragma: no branch
71+
if path == current_worktree_root_dir:
6472
del self.__worktree_root_dir_for_branch[branch]
6573
break
6674

6775
self.__worktree_root_dir_for_branch[checked_out_branch] = current_worktree_root_dir
6876

77+
def _remove_temporary_worktree(self) -> None:
78+
if self.__temporary_worktree_path is not None:
79+
temp_path = self.__temporary_worktree_path
80+
prev_dir = self.__dir_before_temporary_worktree
81+
self.__temporary_worktree_path = None
82+
self.__dir_before_temporary_worktree = None
83+
for branch, path in list(self.__worktree_root_dir_for_branch.items()): # pragma: no branch; break is always hit
84+
if path == temp_path:
85+
del self.__worktree_root_dir_for_branch[branch]
86+
break
87+
assert prev_dir is not None
88+
print(f"Removing the temporary worktree; changing directory back to {bold(prev_dir)}")
89+
self._git.chdir(prev_dir)
90+
self._git.worktree_remove(temp_path)
91+
6992
def _switch_branch(
7093
self,
7194
target_branch: LocalBranchShortName,
7295
custom_checkout_message: Optional[str] = None) -> None:
7396
"""
7497
Switch to the given branch, doing whatever is needed:
7598
- If branch is already checked out in a worktree, cd there
76-
- If branch is not checked out anywhere, checkout the branch (possibly after cd'ing to main worktree)
99+
- If branch is not checked out anywhere, checkout the branch (possibly after cd'ing to main or temp worktree)
77100
- If already on the branch in the correct worktree, do nothing
78101
79102
Updates the worktrees cache after checkout.
80103
Handles all user-facing messaging including directory changes and checkout messages with "OK".
81104
"""
105+
# Clean up temporary worktree from previous branch before switching
106+
self._remove_temporary_worktree()
107+
82108
target_worktree_root_dir = self.__worktree_root_dir_for_branch.get(target_branch)
83109
current_worktree_root_dir = self._git.get_current_worktree_root_dir()
84110

85111
if target_worktree_root_dir is None:
86112
# Branch is not checked out anywhere
87113
config_value = self._config.traverse_when_branch_not_checked_out_in_any_worktree()
88114

89-
# Default behavior: cd to main worktree if we're in a linked worktree
90-
# Otherwise (STAY_IN_THE_CURRENT_WORKTREE), stay in the current worktree and checkout the branch there
91-
if config_value == TraverseWhenBranchNotCheckedOutInAnyWorktree.CD_INTO_MAIN_WORKTREE:
92-
main_worktree_root_dir = self._git.get_main_worktree_root_dir()
93-
if current_worktree_root_dir != main_worktree_root_dir:
94-
print(f"Changing directory to main worktree at {bold(main_worktree_root_dir)}")
95-
self._git.chdir(main_worktree_root_dir)
96-
97-
checkout_msg = custom_checkout_message if custom_checkout_message else f"Checking out {bold(target_branch)}"
98-
print_no_newline(f"{checkout_msg}... ")
99-
self._git.checkout(target_branch)
100-
print(green_ok())
101-
self._update_worktrees_cache_after_checkout(target_branch)
115+
if config_value == TraverseWhenBranchNotCheckedOutInAnyWorktree.CD_INTO_TEMPORARY_WORKTREE:
116+
temp_worktree_path = tempfile.mkdtemp(prefix="git-machete-")
117+
print_no_newline(f"Creating a temporary worktree to check out {bold(target_branch)}... ")
118+
self._git.worktree_add(temp_worktree_path, target_branch)
119+
print(green_ok())
120+
self.__dir_before_temporary_worktree = current_worktree_root_dir
121+
self._git.chdir(temp_worktree_path)
122+
self.__temporary_worktree_path = temp_worktree_path
123+
self.__worktree_root_dir_for_branch[target_branch] = temp_worktree_path
124+
else:
125+
if config_value == TraverseWhenBranchNotCheckedOutInAnyWorktree.CD_INTO_MAIN_WORKTREE:
126+
main_worktree_root_dir = self._git.get_main_worktree_root_dir()
127+
if current_worktree_root_dir != main_worktree_root_dir:
128+
print(f"Changing directory to main worktree at {bold(main_worktree_root_dir)}")
129+
self._git.chdir(main_worktree_root_dir)
130+
131+
checkout_msg = custom_checkout_message if custom_checkout_message else f"Checking out {bold(target_branch)}"
132+
print_no_newline(f"{checkout_msg}... ")
133+
self._git.checkout(target_branch)
134+
print(green_ok())
135+
self._update_worktrees_cache_after_checkout(target_branch)
102136
else:
103137
# Branch is already checked out in a worktree - no need to checkout, just cd there
104138
if current_worktree_root_dir != target_worktree_root_dir:
@@ -151,7 +185,8 @@ def traverse(
151185
initial_worktree_root = self._git.get_current_worktree_root_dir()
152186

153187
# Fetch worktrees once at the start to avoid repeated git worktree list calls
154-
self.__worktree_root_dir_for_branch: Dict[LocalBranchShortName, str] = self._git.get_worktree_root_dirs_by_branch()
188+
self.__temporary_worktree_path = None
189+
self.__worktree_root_dir_for_branch = self._git.get_worktree_root_dirs_by_branch()
155190

156191
try:
157192
if opt_start_from == TraverseStartFrom.ROOT:
@@ -515,6 +550,8 @@ def traverse(
515550
f"The initial branch {bold(initial_branch)} has been slid out. "
516551
f"Returned to nearest remaining managed branch {bold(nearest_remaining_branch)}")
517552
finally:
553+
self._remove_temporary_worktree()
554+
518555
# Warn if the initial directory doesn't correspond to the final checked out branch's worktree
519556
final_branch = self._git.get_current_branch()
520557
final_worktree_path = self.__worktree_root_dir_for_branch.get(final_branch)

git_machete/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def _traverse_remote_fetch_key(remote: str) -> str:
2323

2424
class TraverseWhenBranchNotCheckedOutInAnyWorktree(ParsableEnum):
2525
CD_INTO_MAIN_WORKTREE = auto()
26+
CD_INTO_TEMPORARY_WORKTREE = auto()
2627
STAY_IN_THE_CURRENT_WORKTREE = auto() # noqa: F841
2728

2829

0 commit comments

Comments
 (0)