Skip to content

Commit 78b72c0

Browse files
committed
feat(git_work): enabled upstream tracking for worktrees
- Centralized bare repo config and upstream helpers in GitWork.Project. - Set push.autoSetupRemote during clone and init repair. - Set upstream tracking when checkout auto-created a worktree from origin. - Added tests for clone, init repair, and checkout auto-create flows.
1 parent 46cb2c2 commit 78b72c0

7 files changed

Lines changed: 157 additions & 95 deletions

File tree

lib/git_work/commands/checkout.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ defmodule GitWork.Commands.Checkout do
302302

303303
case Hooks.run(:post_worktree_create, ctx) do
304304
:ok ->
305+
Project.ensure_upstream(worktree_dir, branch)
305306
{:ok, worktree_dir}
306307

307308
{:error, msg} ->

lib/git_work/commands/clone.ex

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ defmodule GitWork.Commands.Clone do
4848
{:ok, branch} <- detect_head_branch(bare_dir),
4949
:ok <- fix_head(bare_dir, branch),
5050
:ok <- add_main_worktree(dir, branch),
51-
:ok <- configure_bare(bare_dir),
51+
:ok <- Project.configure_bare(bare_dir),
5252
:ok <- fetch_refs(bare_dir),
53-
:ok <- ensure_upstream(dir, branch) do
53+
:ok <- Project.ensure_upstream(Path.join(dir, branch), branch) do
5454
{:ok, Path.join(dir, branch)}
5555
end
5656
end
@@ -67,16 +67,6 @@ defmodule GitWork.Commands.Clone do
6767
File.write(Path.join(dir, ".git"), "gitdir: ./.bare\n")
6868
end
6969

70-
defp configure_bare(bare_dir) do
71-
with {:ok, _} <- Git.cmd(["config", "core.bare", "true"], cd: bare_dir),
72-
{:ok, _} <-
73-
Git.cmd(
74-
["config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"],
75-
cd: bare_dir
76-
) do
77-
:ok
78-
end
79-
end
8070

8171
defp fetch_refs(bare_dir) do
8272
case Git.cmd(["fetch", "--all"], cd: bare_dir) do
@@ -135,32 +125,4 @@ defmodule GitWork.Commands.Clone do
135125
end
136126
end
137127

138-
defp ensure_upstream(dir, branch) do
139-
worktree_dir = Path.join(dir, branch)
140-
141-
case Git.cmd(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cd: worktree_dir) do
142-
{:ok, _} ->
143-
:ok
144-
145-
{:error, _} ->
146-
case Git.cmd(["show-ref", "--verify", "--quiet", "refs/remotes/origin/#{branch}"],
147-
cd: worktree_dir
148-
) do
149-
{:ok, _} ->
150-
case Git.cmd(["branch", "--set-upstream-to=origin/#{branch}", branch],
151-
cd: worktree_dir
152-
) do
153-
{:ok, _} ->
154-
:ok
155-
156-
{:error, msg} ->
157-
IO.write(:stderr, "warning: failed to set upstream for #{branch}: #{msg}\n")
158-
:ok
159-
end
160-
161-
{:error, _} ->
162-
:ok
163-
end
164-
end
165-
end
166128
end

lib/git_work/commands/init.ex

Lines changed: 3 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ defmodule GitWork.Commands.Init do
6565

6666
defp do_repair(dir, git_dir, bare_dir) do
6767
with :ok <- ensure_gitdir_pointer(dir, git_dir),
68-
:ok <- configure_bare(bare_dir),
68+
:ok <- Project.configure_bare(bare_dir),
6969
{:ok, branch} <- Project.head_branch(dir),
7070
:ok <- ensure_worktree_dir(dir, branch) do
7171
{:ok, Project.worktree_path(dir, branch)}
@@ -119,12 +119,12 @@ defmodule GitWork.Commands.Init do
119119

120120
defp do_init_steps(dir, branch, stashed?, git_dir, bare_dir) do
121121
with :ok <- move_git_to_bare(git_dir, bare_dir),
122-
:ok <- configure_bare(bare_dir),
122+
:ok <- Project.configure_bare(bare_dir),
123123
:ok <- move_files_to_worktree(dir, branch),
124124
:ok <- setup_worktree_linkage(dir, branch),
125125
:ok <- write_gitdir_pointer(dir),
126126
:ok <- reset_worktree_index(dir, branch),
127-
:ok <- ensure_upstream(dir, branch),
127+
:ok <- Project.ensure_upstream(Path.join(dir, branch), branch),
128128
:ok <- validate_init(dir, branch, bare_dir),
129129
:ok <- maybe_pop_stash(dir, branch, stashed?) do
130130
:ok
@@ -168,27 +168,6 @@ defmodule GitWork.Commands.Init do
168168
end
169169
end
170170

171-
defp configure_bare(bare_dir) do
172-
with {:ok, _} <- Git.cmd(["config", "core.bare", "true"], cd: bare_dir) do
173-
# Only set fetch refspec if remote origin exists
174-
case Git.cmd(["remote", "get-url", "origin"], cd: bare_dir) do
175-
{:ok, _} ->
176-
Git.cmd(
177-
["config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"],
178-
cd: bare_dir
179-
)
180-
|> case do
181-
{:ok, _} -> :ok
182-
{:error, msg} -> {:error, "failed to configure fetch: #{msg}"}
183-
end
184-
185-
{:error, _} ->
186-
IO.write(:stderr, "warning: no remote 'origin' configured\n")
187-
:ok
188-
end
189-
end
190-
end
191-
192171
defp move_files_to_worktree(dir, branch) do
193172
worktree_dir = Path.join(dir, branch)
194173

@@ -249,36 +228,6 @@ defmodule GitWork.Commands.Init do
249228
end
250229
end
251230

252-
defp ensure_upstream(dir, branch) do
253-
worktree_dir = Path.join(dir, branch)
254-
255-
case Git.cmd(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
256-
cd: worktree_dir
257-
) do
258-
{:ok, _} ->
259-
:ok
260-
261-
{:error, _} ->
262-
case Git.cmd(["show-ref", "--verify", "--quiet", "refs/remotes/origin/#{branch}"],
263-
cd: worktree_dir
264-
) do
265-
{:ok, _} ->
266-
case Git.cmd(["branch", "--set-upstream-to=origin/#{branch}", branch],
267-
cd: worktree_dir
268-
) do
269-
{:ok, _} ->
270-
:ok
271-
272-
{:error, msg} ->
273-
IO.write(:stderr, "warning: failed to set upstream for #{branch}: #{msg}\n")
274-
:ok
275-
end
276-
277-
{:error, _} ->
278-
:ok
279-
end
280-
end
281-
end
282231

283232
defp validate_init(dir, branch, bare_dir) do
284233
worktree_dir = Path.join(dir, branch)

lib/git_work/project.ex

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
defmodule GitWork.Project do
22
@moduledoc """
3-
Project root discovery, path helpers, and branch name sanitization.
3+
Project root discovery, path helpers, branch name sanitization,
4+
and shared configuration for bare repos and worktrees.
45
"""
56

7+
alias GitWork.Git
8+
69
@doc """
710
Find the project root by walking up from `start_dir` looking for `.bare/`.
811
Returns {:ok, path} or {:error, message}.
@@ -76,4 +79,64 @@ defmodule GitWork.Project do
7679
{:error, _} -> {:error, "could not determine HEAD branch"}
7780
end
7881
end
82+
83+
@doc """
84+
Configure the bare repo: mark as bare, enable push.autoSetupRemote,
85+
and set the fetch refspec for origin (if origin exists).
86+
"""
87+
def configure_bare(bare_dir) do
88+
with {:ok, _} <- Git.cmd(["config", "core.bare", "true"], cd: bare_dir),
89+
{:ok, _} <- Git.cmd(["config", "push.autoSetupRemote", "true"], cd: bare_dir) do
90+
# Only set fetch refspec if remote origin exists
91+
case Git.cmd(["remote", "get-url", "origin"], cd: bare_dir) do
92+
{:ok, _} ->
93+
case Git.cmd(
94+
["config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"],
95+
cd: bare_dir
96+
) do
97+
{:ok, _} -> :ok
98+
{:error, msg} -> {:error, "failed to configure fetch: #{msg}"}
99+
end
100+
101+
{:error, _} ->
102+
IO.write(:stderr, "warning: no remote 'origin' configured\n")
103+
:ok
104+
end
105+
end
106+
end
107+
108+
@doc """
109+
Set upstream tracking for a branch if its remote counterpart exists.
110+
No-ops when the branch has no remote ref (e.g. brand-new, not yet pushed).
111+
"""
112+
def ensure_upstream(worktree_dir, branch) do
113+
case Git.cmd(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
114+
cd: worktree_dir
115+
) do
116+
{:ok, _} ->
117+
# Already has upstream tracking
118+
:ok
119+
120+
{:error, _} ->
121+
case Git.cmd(["show-ref", "--verify", "--quiet", "refs/remotes/origin/#{branch}"],
122+
cd: worktree_dir
123+
) do
124+
{:ok, _} ->
125+
case Git.cmd(["branch", "--set-upstream-to=origin/#{branch}", branch],
126+
cd: worktree_dir
127+
) do
128+
{:ok, _} ->
129+
:ok
130+
131+
{:error, msg} ->
132+
IO.write(:stderr, "warning: failed to set upstream for #{branch}: #{msg}\n")
133+
:ok
134+
end
135+
136+
{:error, _} ->
137+
# No remote ref — nothing to track (will be set on first push via autoSetupRemote)
138+
:ok
139+
end
140+
end
141+
end
79142
end

test/git_work/commands/checkout_test.exs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,4 +351,73 @@ defmodule GitWork.Commands.CheckoutTest do
351351
assert {:ok, path} = Checkout.run(["feature-trust-remote"], :text)
352352
assert File.regular?(Path.join(path, ".trusted"))
353353
end
354+
355+
test "checkout auto-create from remote sets upstream tracking", %{tmp: tmp} do
356+
origin = GitWork.TestHelper.create_origin_repo(tmp)
357+
project = Path.join(tmp, "project")
358+
{:ok, _} = GitWork.Commands.Clone.run([origin, project], :text)
359+
360+
{_, 0} =
361+
System.cmd("git", ["config", "git-work.hooks.mise.task", ""],
362+
cd: Path.join(project, ".bare")
363+
)
364+
365+
GitWork.TestHelper.create_remote_branch(origin, "feature-tracking")
366+
System.cmd("git", ["fetch", "--all"], cd: Path.join(project, ".bare"))
367+
368+
File.cd!(Path.join(project, "main"))
369+
370+
# Auto-create worktree from remote branch (no -b)
371+
assert {:ok, path} = Checkout.run(["feature-tracking"], :text)
372+
assert File.dir?(path)
373+
374+
# Upstream tracking should be set to the remote branch
375+
{upstream, 0} =
376+
System.cmd("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
377+
cd: path
378+
)
379+
380+
assert String.trim(upstream) == "origin/feature-tracking"
381+
end
382+
383+
test "checkout -b new branch has push.autoSetupRemote configured", %{tmp: tmp} do
384+
origin = GitWork.TestHelper.create_origin_repo(tmp)
385+
project = Path.join(tmp, "project")
386+
{:ok, _} = GitWork.Commands.Clone.run([origin, project], :text)
387+
388+
{_, 0} =
389+
System.cmd("git", ["config", "git-work.hooks.mise.task", ""],
390+
cd: Path.join(project, ".bare")
391+
)
392+
393+
File.cd!(Path.join(project, "main"))
394+
395+
assert {:ok, path} = Checkout.run(["-b", "feature-fresh"], :text)
396+
assert File.dir?(path)
397+
398+
# push.autoSetupRemote should be set on the bare repo so that
399+
# `git push` from any worktree automatically sets upstream tracking
400+
{auto_setup, 0} =
401+
System.cmd("git", ["config", "push.autoSetupRemote"], cd: Path.join(project, ".bare"))
402+
403+
assert String.trim(auto_setup) == "true"
404+
405+
# New branch has no upstream yet (nothing on remote)
406+
{_, exit_code} =
407+
System.cmd("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
408+
cd: path
409+
)
410+
411+
assert exit_code != 0
412+
413+
# Default push with autoSetupRemote — should set upstream automatically
414+
{_, 0} = System.cmd("git", ["push"], cd: path)
415+
416+
{upstream, 0} =
417+
System.cmd("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
418+
cd: path
419+
)
420+
421+
assert String.trim(upstream) == "origin/feature-fresh"
422+
end
354423
end

test/git_work/commands/clone_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,16 @@ defmodule GitWork.Commands.CloneTest do
7474
assert {:error, msg} = Clone.run([origin, project], :text)
7575
assert msg =~ "already exists"
7676
end
77+
78+
test "clone sets push.autoSetupRemote on bare repo", %{tmp: tmp} do
79+
origin = GitWork.TestHelper.create_origin_repo(tmp)
80+
project = Path.join(tmp, "project")
81+
82+
assert {:ok, _} = Clone.run([origin, project], :text)
83+
84+
{auto_setup, 0} =
85+
System.cmd("git", ["config", "push.autoSetupRemote"], cd: Path.join(project, ".bare"))
86+
87+
assert String.trim(auto_setup) == "true"
88+
end
7789
end

test/git_work/commands/init_test.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ defmodule GitWork.Commands.InitTest do
6969
System.cmd("git", ["config", "--bool", "core.bare"], cd: Path.join(repo, ".bare"))
7070

7171
assert String.trim(value) == "true"
72+
73+
# push.autoSetupRemote should be set by repair
74+
{auto_setup, 0} =
75+
System.cmd("git", ["config", "push.autoSetupRemote"], cd: Path.join(repo, ".bare"))
76+
77+
assert String.trim(auto_setup) == "true"
7278
end
7379

7480
test "recreates missing HEAD worktree on rerun", %{tmp: tmp} do

0 commit comments

Comments
 (0)