Skip to content

Commit 9f94143

Browse files
authored
feat: pull base branch before worktree creation (#4)
* feat: pull base branch before creating new worktree * chore: update CHANGELOG.md * chore: fix formatting
1 parent c75d057 commit 9f94143

5 files changed

Lines changed: 63 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
99

1010
### Changed
1111
- Increased `prune` unit test coverage
12+
- Base branch is pulled from origin (best-effort) before creating a new worktree
1213

1314
### Fixed
1415
- Removed line breaks from `prune`, `reset`, `root` and `up` commands' help messages

src/git_workspace/git.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ def fetch_origin(cwd: Path) -> None:
111111
raise GitFetchError(f"Failed to fetch from origin: {result.stderr.strip()}")
112112

113113

114+
def pull_branch(branch: str, cwd: Path) -> None:
115+
"""
116+
Fast-forwards a local branch to match origin without checking it out.
117+
118+
Best-effort: failures are logged as warnings and silently ignored so that
119+
offline or no-remote scenarios don't block worktree creation.
120+
"""
121+
logger.debug("pulling branch %r in %s", branch, cwd)
122+
cmd = ["git", "fetch", "origin", f"{branch}:{branch}"]
123+
result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
124+
if result.returncode != 0:
125+
logger.warning("failed to pull branch %r in %s: %s", branch, cwd, result.stderr.strip())
126+
127+
114128
def local_branch_exists(branch: str, cwd: Path) -> bool:
115129
"""
116130
Returns whether a local branch exists

src/git_workspace/worktree.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def _create_new(
142142
branch,
143143
resolved_base_branch,
144144
)
145+
git.pull_branch(resolved_base_branch, cwd=workspace.dir)
145146
dir = workspace.paths.worktree(branch)
146147
git.create_worktree_new(
147148
dir,

tests/unit/test_git.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,24 @@ def test_raises_git_fetch_error_on_failure(self, mock_subprocess_run: MagicMock)
129129
git.fetch_origin(CWD)
130130

131131

132+
class TestPullBranch:
133+
def test_builds_correct_command(self, mock_subprocess_run: MagicMock) -> None:
134+
git.pull_branch(BASE_BRANCH, CWD)
135+
136+
assert mock_subprocess_run.call_args.args[0] == [
137+
"git",
138+
"fetch",
139+
"origin",
140+
f"{BASE_BRANCH}:{BASE_BRANCH}",
141+
]
142+
assert mock_subprocess_run.call_args.kwargs["cwd"] == CWD
143+
144+
def test_does_not_raise_on_failure(self, mock_subprocess_run: MagicMock) -> None:
145+
mock_subprocess_run.return_value.returncode = 1
146+
147+
git.pull_branch(BASE_BRANCH, CWD) # should not raise
148+
149+
132150
class TestLocalBranchExists:
133151
def test_builds_correct_command(self, mock_subprocess_run: MagicMock) -> None:
134152
git.local_branch_exists(BRANCH, CWD)

tests/unit/test_worktree.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,35 @@ def test_returns_cwd_result_when_no_branch(
167167
assert result is worktree
168168

169169

170+
class TestCreateNew:
171+
@pytest.fixture
172+
def mock_git(self, mocker: MockerFixture) -> MagicMock:
173+
mocker.patch("git_workspace.worktree.git.pull_branch")
174+
mocker.patch("git_workspace.worktree.git.create_worktree_new")
175+
return mocker.MagicMock()
176+
177+
def test_pulls_base_branch_before_creating_worktree(
178+
self, mocker: MockerFixture, workspace: MagicMock, mock_git: MagicMock
179+
) -> None:
180+
mock_pull = mocker.patch("git_workspace.worktree.git.pull_branch")
181+
mocker.patch("git_workspace.worktree.git.create_worktree_new")
182+
183+
Worktree._create_new(workspace, BRANCH, BASE_BRANCH)
184+
185+
mock_pull.assert_called_once_with(BASE_BRANCH, cwd=workspace.dir)
186+
187+
def test_uses_manifest_base_branch_when_none_provided(
188+
self, mocker: MockerFixture, workspace: MagicMock, mock_git: MagicMock
189+
) -> None:
190+
mock_pull = mocker.patch("git_workspace.worktree.git.pull_branch")
191+
mocker.patch("git_workspace.worktree.git.create_worktree_new")
192+
workspace.manifest.base_branch = BASE_BRANCH
193+
194+
Worktree._create_new(workspace, BRANCH, None)
195+
196+
mock_pull.assert_called_once_with(BASE_BRANCH, cwd=workspace.dir)
197+
198+
170199
class TestDelete:
171200
@pytest.fixture
172201
def mock_git_remove_worktree(self, mocker: MockerFixture) -> MagicMock:

0 commit comments

Comments
 (0)