Repository: git-town/git-town

Issue:
sync fail with  ambiguous argument
### Steps to reproduce

I made a test repo to try out the stacked changes and trying to understand what it does, background: I use jujutsu and the mega-merge workflow.

Here is what i did:

branches: feature-a, feature-b, wip

1. create a branch from master named `wip`
2. git town switch and select feature-a
3. git town set-parent and select wip
4. git town switch and select feature-b
5. git town set-parent and select feature-a

Now the stack is like wip -> feature-a -> feature-b

I noticed that feature-b not contain changes in feature-a, and after read the doc, I know that i have to run git town sync in feature-b to pull the changes from parents, so i can work on feature-b but with all changes from other branches, this is kind like the mega-merge workflow that i need.

So here is what i did next:

1. git town switch and select feature-a
2. git town sync, success, now feature-a contain changes from wip, a file named "wip", *the file name is same with the branch name*.
3. git town switch and select feature-b
4. git town sync, it fails.


fail message:




### Command output

```shell
git-town.main-branch
master

git-town-branch.towry/feat-word-count.parent
wip

git-town-branch.towry/test-workers.parent
towry/feat-word-count

git-town-branch.towry/feat-build-awesome-app-0527.parent
master

git-town-branch.wip.parent
master


(verbose) git for-each-ref "--format=refname:%(refname) branchname:%(refname:lstrip=2) sha:%(objectname) head:%(if)%(HEAD)%(then)Y%(else)N%(
end) worktree:%(if)%(worktreepath)%(then)Y%(else)N%(end) symref:%(if)%(symref)%(then)Y%(else)N%(end) upstream:%(upstream:lstrip=2) track:%(u
pstream:track,nobracket)" --sort=refname refs/heads/ refs/remotes/
refname:refs/heads/master branchname:master sha:ac01ec2d38d7d122d536646f47d6fee11f67aab3 head:N worktree:N symref:N upstream: track:
refname:refs/heads/next branchname:next sha:e7aec274cc0d2a419d8254bb84b39e09c1e24f4e head:N worktree:N symref:N upstream: track:
refname:refs/heads/towry/feat-build-awesome-app-0527 branchname:towry/feat-build-awesome-app-0527 sha:2b1540556ab0ccb1025be6806c0fec10de7567
d0 head:N worktree:N symref:N upstream: track:
refname:refs/heads/towry/feat-word-count branchname:towry/feat-word-count sha:a5adfce8b7119227bc0dca3a45392d4079d6f6e7 head:N worktree:N sym
ref:N upstream: track:
refname:refs/heads/towry/test-workers branchname:towry/test-workers sha:86f8dc8c29624c2d326455c8871d95dd05291ed8 head:Y worktree:Y symref:N
upstream: track:
refname:refs/heads/wip branchname:wip sha:2c7dccba6eafa48a3424f796c7bbfea8757d2c0a head:N worktree:N symref:N upstream: track:

(verbose) git status -z --ignore-submodules

(verbose) git rev-parse --verify -q MERGE_HEAD

(verbose) git rev-parse --absolute-git-dir
/private/tmp/test/.git

(verbose) git remote

(verbose) git stash list

(verbose) git for-each-ref "--format=refname:%(refname) branchname:%(refname:lstrip=2) sha:%(objectname) head:%(if)%(HEAD)%(then)Y%(else)N%(
end) worktree:%(if)%(worktreepath)%(then)Y%(else)N%(end) symref:%(if)%(symref)%(then)Y%(else)N%(end) upstream:%(upstream:lstrip=2) track:%(u
pstream:track,nobracket)" --sort=refname refs/heads/ refs/remotes/
refname:refs/heads/master branchname:master sha:ac01ec2d38d7d122d536646f47d6fee11f67aab3 head:N worktree:N symref:N upstream: track:
refname:refs/heads/next branchname:next sha:e7aec274cc0d2a419d8254bb84b39e09c1e24f4e head:N worktree:N symref:N upstream: track:
refname:refs/heads/towry/feat-build-awesome-app-0527 branchname:towry/feat-build-awesome-app-0527 sha:2b1540556ab0ccb1025be6806c0fec10de7567
d0 head:N worktree:N symref:N upstream: track:
refname:refs/heads/towry/feat-word-count branchname:towry/feat-word-count sha:a5adfce8b7119227bc0dca3a45392d4079d6f6e7 head:N worktree:N sym
ref:N upstream: track:
refname:refs/heads/towry/test-workers branchname:towry/test-workers sha:86f8dc8c29624c2d326455c8871d95dd05291ed8 head:Y worktree:Y symref:N
upstream: track:
refname:refs/heads/wip branchname:wip sha:2c7dccba6eafa48a3424f796c7bbfea8757d2c0a head:N worktree:N symref:N upstream: track:

(verbose) git rev-parse --verify --abbrev-ref @{-1}
towry/feat-word-count

(verbose) git remote get-url origin
error: No such remote 'origin'

(verbose) git log master..wip --format=%s --reverse
add wip

(verbose) git log wip..towry/feat-word-count --format=%s --reverse
feat: add word count feature placeholder
Merge branch 'wip' into towry/feat-word-count

(verbose) git log towry/feat-word-count..towry/test-workers --format=%s --reverse
chore: add empty test workers file

[towry/test-workers] git checkout towry/feat-word-count
Switched to branch 'towry/feat-word-count'

(verbose) git log --no-merges --format=%H wip ^towry/feat-word-count
fatal: ambiguous argument 'wip': both revision and filename
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

(verbose) git for-each-ref "--format=refname:%(refname) branchname:%(refname:lstrip=2) sha:%(objectname) head:%(if)%(HEAD)%(then)Y%(else)N%(
end) worktree:%(if)%(worktreepath)%(then)Y%(else)N%(end) symref:%(if)%(symref)%(then)Y%(else)N%(end) upstream:%(upstream:lstrip=2) track:%(u
pstream:track,nobracket)" --sort=refname refs/heads/ refs/remotes/
refname:refs/heads/master branchname:master sha:ac01ec2d38d7d122d536646f47d6fee11f67aab3 head:N worktree:N symref:N upstream: track:
refname:refs/heads/next branchname:next sha:e7aec274cc0d2a419d8254bb84b39e09c1e24f4e head:N worktree:N symref:N upstream: track:
refname:refs/heads/towry/feat-build-awesome-app-0527 branchname:towry/feat-build-awesome-app-0527 sha:2b1540556ab0ccb1025be6806c0fec10de7567
d0 head:N worktree:N symref:N upstream: track:
refname:refs/heads/towry/feat-word-count branchname:towry/feat-word-count sha:a5adfce8b7119227bc0dca3a45392d4079d6f6e7 head:Y worktree:Y sym
ref:N upstream: track:
refname:refs/heads/towry/test-workers branchname:towry/test-workers sha:86f8dc8c29624c2d326455c8871d95dd05291ed8 head:N worktree:N symref:N
upstream: track:
refname:refs/heads/wip branchname:wip sha:2c7dccba6eafa48a3424f796c7bbfea8757d2c0a head:N worktree:N symref:N upstream: track:

(verbose) git config -lz --global
gitbutler.aimodelprovider
openai


(verbose) git config -lz --local
core.repositoryformatversion
0

core.filemode
true

core.bare
false

core.logallrefupdates
true

core.ignorecase
true

core.precomposeunicode
true

git-town.main-branch
master

git-town-branch.towry/feat-word-count.parent
wip

git-town-branch.towry/test-workers.parent
towry/feat-word-count

git-town-branch.towry/feat-build-awesome-app-0527.parent
master

git-town-branch.wip.parent
master


(verbose) git stash list

(verbose) git status -z --ignore-submodules

(verbose) git rev-parse --verify -q MERGE_HEAD

(verbose) git rev-parse --absolute-git-dir
/private/tmp/test/.git

Ran 26 shell commands.
Error:
----------------------------------------
Diagnostic information of failed command

COMMAND: git log --no-merges --format=%H wip ^towry/feat-word-count
ERROR: exit status 128
OUTPUT START
fatal: ambiguous argument 'wip': both revision and filename
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

OUTPUT END
----------------------------------------

To continue after having resolved conflicts, run "git town continue".
To go back to where you started, run "git town undo".
To continue by skipping the current branch, run "git town skip".



/tmp/test [towry/feat-word-count]
✗                                                                                                                                  17:46:11
```

### Additional information

git-town version: 21.0.0

Discussion:
@towry: I thought it was because the branch name and file name is the same, it turns out the command is not correctly quoted.

```
✗  git log --no-merges --format=%H master ^piw                                                                                     17:53:16
fatal: ambiguous argument 'piw': both revision and filename
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
```
Wrap the format value in quotes:

```
/tmp/test [piw]
✗  git log --no-merges --format="%H master ^piw"                                                                                   17:53:33
e95d077dfde06fe32f1b3abff12ed49057366deb master ^piw
ac01ec2d38d7d122d536646f47d6fee11f67aab3 master ^piw
```


Add other additional info:

- macos
- fish shell
- ghostty
- tmux@towry: This test repo has not pushed to remote yet, not a single branch.@towry: Testing with repo has remote configured, seems to work fine, and I quite like it :)@kevgo: Thanks for reaching out about this! First, some tips to use Git Town more efficiently.

### Create your branch stack the Git Town way

To quickly set up a branch stack like `wip` → `feature-a` → `feature-b`, you can use the following commands:

```
git town hack wip
git town append feature-a
git town append feature-b
```

This approach ensures your branch relationships are properly configured, i.e. no need to run `git town set-parent`. For more details, check out the [hack](https://www.git-town.com/commands/hack.html) and [append](https://www.git-town.com/commands/append.html) command docs.

### Sync the entire stack in one command

When you run `git sync`, it will automatically sync that branch and all of its ancestors. For example, if you run it from a branch like `feature-b`, it also syncs `feature-a`, `wip`, and `master`.

If you are on a different branch, like `wip`, and want to sync the entire stack:

```
git town sync --stack
```

But the easiest way is to always sync all local branches:

```
git town sync --all
```
@kevgo: You also talk about a bug. Thanks for reporting it! 🙏 

The issue appears to be that your workspace contains both a branch named `wip` and a file named `wip`. This causes the following command to fail:

```
git log --no-merges --format=%H wip ^towry/feat-word-count
```

Git complains that it isn't sure whether we talk about the branch or the file:

```
ambiguous argument 'wip': both revision and filename
```

To refer to the branch, we should use this syntax:

```
git log --no-merges --format=%H refs/heads/wip ^refs/heads/towry/feat-word-count
```

A fix is on the way. Thanks again for reporting this!
@kevgo: > This test repo has not pushed to remote yet, not a single branch.

This happens because Git Town gets stuck on the bug we discuss above before it gets a chance to push your branches to the remote. Fixing this bug will solve all other problems you encounter in this ticket as well. @kevgo: This is now available as part of [Git Town 21.1](https://github.com/git-town/git-town/releases/tag/v21.1.0). Enjoy!

Solution patch:
diff --git a/features/append/on_feature_branch/verbose/enabled.feature b/features/append/on_feature_branch/verbose/enabled.feature
index cc9096685..2775466de 100644
--- a/features/append/on_feature_branch/verbose/enabled.feature
+++ b/features/append/on_feature_branch/verbose/enabled.feature
@@ -30,7 +30,7 @@ Feature: display all executed Git commands
       |          | backend  | git rev-parse --verify --abbrev-ref @{-1}                                                                                                                                                                                                                                                                                                        |
       |          | backend  | git remote get-url origin                                                                                                                                                                                                                                                                                                                        |
       |          | backend  | git log main..existing --format=%s --reverse                                                                                                                                                                                                                                                                                                     |
-      |          | backend  | git log --no-merges --format=%H main ^existing                                                                                                                                                                                                                                                                                                   |
+      |          | backend  | git log --no-merges --format=%H refs/heads/main ^refs/heads/existing                                                                                                                                                                                                                                                                             |
       |          | backend  | git rev-list --left-right existing...origin/existing                                                                                                                                                                                                                                                                                             |
       |          | backend  | git rev-parse --verify -q refs/heads/existing                                                                                                                                                                                                                                                                                                    |
       |          | backend  | git rev-list --left-right existing...origin/existing                                                                                                                                                                                                                                                                                             |
diff --git a/features/compress/current_branch/verbose/verbose.feature b/features/compress/current_branch/verbose/verbose.feature
index 1d60b15ca..2b495ac91 100644
--- a/features/compress/current_branch/verbose/verbose.feature
+++ b/features/compress/current_branch/verbose/verbose.feature
@@ -33,7 +33,7 @@ Feature: compress the commits on a feature branch verbosely
       |         | git remote get-url origin                                                                                                                                                                                                                                                                                                                        |
       |         | git log "--format=%H %s" main..feature                                                                                                                                                                                                                                                                                                           |
       |         | git show --no-patch --format=%B {{ sha-initial 'commit 1' }}                                                                                                                                                                                                                                                                                     |
-      |         | git log --no-merges --format=%H main ^feature                                                                                                                                                                                                                                                                                                    |
+      |         | git log --no-merges --format=%H refs/heads/main ^refs/heads/feature                                                                                                                                                                                                                                                                              |
       |         | git reset --soft main                                                                                                                                                                                                                                                                                                                            |
       |         | git commit -m "commit 1"                                                                                                                                                                                                                                                                                                                         |
       |         | git rev-list --left-right feature...origin/feature                                                                                                                                                                                                                                                                                               |
diff --git a/features/merge/verbose/enabled.feature b/features/merge/verbose/enabled.feature
index a94afb3b1..b71d7035c 100644
--- a/features/merge/verbose/enabled.feature
+++ b/features/merge/verbose/enabled.feature
@@ -36,7 +36,7 @@ Feature: merging a branch verbosely
       |        | git for-each-ref "--format=refname:%(refname) branchname:%(refname:lstrip=2) sha:%(objectname) head:%(if)%(HEAD)%(then)Y%(else)N%(end) worktree:%(if)%(worktreepath)%(then)Y%(else)N%(end) symref:%(if)%(symref)%(then)Y%(else)N%(end) upstream:%(upstream:lstrip=2) track:%(upstream:track,nobracket)" --sort=refname refs/heads/ refs/remotes/ |
       |        | git remote get-url origin                                                                                                                                                                                                                                                                                                                        |
       |        | git rev-parse --verify --abbrev-ref @{-1}                                                                                                                                                                                                                                                                                                        |
-      |        | git log --no-merges --format=%H alpha ^beta                                                                                                                                                                                                                                                                                                      |
+      |        | git log --no-merges --format=%H refs/heads/alpha ^refs/heads/beta                                                                                                                                                                                                                                                                                |
       |        | git checkout alpha                                                                                                                                                                                                                                                                                                                               |
       | alpha  | git reset --hard {{ sha 'beta commit' }}                                                                                                                                                                                                                                                                                                         |
       |        | git config --unset git-town-branch.beta.parent                                                                                                                                                                                                                                                                                                   |
diff --git a/features/prepend/verbose/enabled.feature b/features/prepend/verbose/enabled.feature
index 1dd6a885b..369be48bf 100644
--- a/features/prepend/verbose/enabled.feature
+++ b/features/prepend/verbose/enabled.feature
@@ -30,7 +30,7 @@ Feature: display all executed Git commands
       |        | backend  | git rev-parse --verify --abbrev-ref @{-1}                                                                                                                                                                                                                                                                                                        |
       |        | backend  | git remote get-url origin                                                                                                                                                                                                                                                                                                                        |
       |        | backend  | git log main..old --format=%s --reverse                                                                                                                                                                                                                                                                                                          |
-      |        | backend  | git log --no-merges --format=%H main ^old                                                                                                                                                                                                                                                                                                        |
+      |        | backend  | git log --no-merges --format=%H refs/heads/main ^refs/heads/old                                                                                                                                                                                                                                                                                  |
       |        | backend  | git rev-list --left-right old...origin/old                                                                                                                                                                                                                                                                                                       |
       |        | backend  | git rev-parse --verify -q refs/heads/old                                                                                                                                                                                                                                                                                                         |
       |        | backend  | git rev-list --left-right old...origin/old                                                                                                                                                                                                                                                                                                       |
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/file_matches_branch_name/current_branch.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/file_matches_branch_name/current_branch.feature
new file mode 100644
index 000000000..bee502125
--- /dev/null
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/file_matches_branch_name/current_branch.feature
@@ -0,0 +1,50 @@
+Feature: sync a branch that contains a file with the same name
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME    | TYPE    | PARENT | LOCATIONS     |
+      | feature | feature | main   | local, origin |
+    And the commits
+      | BRANCH  | LOCATION | MESSAGE               | FILE NAME           |
+      | main    | local    | local main commit     | main                |
+      |         | origin   | origin main commit    | remote main file    |
+      | feature | local    | local feature commit  | feature             |
+      |         | origin   | origin feature commit | remote feature file |
+    And the current branch is "feature"
+    When I run "git-town sync"
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                                           |
+      | feature | git fetch --prune --tags                          |
+      |         | git checkout main                                 |
+      | main    | git -c rebase.updateRefs=false rebase origin/main |
+      |         | git push                                          |
+      |         | git checkout feature                              |
+      | feature | git merge --no-edit --ff main                     |
+      |         | git merge --no-edit --ff origin/feature           |
+      |         | git push                                          |
+    And all branches are now synchronized
+    And these commits exist now
+      | BRANCH  | LOCATION      | MESSAGE                                                    |
+      | main    | local, origin | origin main commit                                         |
+      |         |               | local main commit                                          |
+      | feature | local, origin | local feature commit                                       |
+      |         |               | Merge branch 'main' into feature                           |
+      |         |               | origin feature commit                                      |
+      |         |               | Merge remote-tracking branch 'origin/feature' into feature |
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                                                                                |
+      | feature | git reset --hard {{ sha 'local feature commit' }}                                      |
+      |         | git push --force-with-lease origin {{ sha-in-origin 'origin feature commit' }}:feature |
+    And these commits exist now
+      | BRANCH  | LOCATION      | MESSAGE               |
+      | main    | local, origin | origin main commit    |
+      |         |               | local main commit     |
+      | feature | local         | local feature commit  |
+      |         | origin        | origin feature commit |
+    And the initial branches and lineage exist now
diff --git a/features/sync/features/verbose.feature b/features/sync/features/verbose.feature
index 1dcbd57c3..5f9ec5ed2 100644
--- a/features/sync/features/verbose.feature
+++ b/features/sync/features/verbose.feature
@@ -39,7 +39,7 @@ Feature: display all executed Git commands
       |         | backend  | git rev-list --left-right main...origin/main                                                                                                                                                                                                                                                                                                     |
       |         | frontend | git push                                                                                                                                                                                                                                                                                                                                         |
       |         | frontend | git checkout feature                                                                                                                                                                                                                                                                                                                             |
-      |         | backend  | git log --no-merges --format=%H main ^feature                                                                                                                                                                                                                                                                                                    |
+      |         | backend  | git log --no-merges --format=%H refs/heads/main ^refs/heads/feature                                                                                                                                                                                                                                                                              |
       |         | backend  | git rev-list --left-right feature...origin/feature                                                                                                                                                                                                                                                                                               |
       | feature | frontend | git merge --no-edit --ff main                                                                                                                                                                                                                                                                                                                    |
       |         | frontend | git merge --no-edit --ff origin/feature                                                                                                                                                                                                                                                                                                          |
diff --git a/internal/git/commands.go b/internal/git/commands.go
index 48d4f6add..e1a7a3acf 100644
--- a/internal/git/commands.go
+++ b/internal/git/commands.go
@@ -77,7 +77,7 @@ func (self *Commands) BranchHasUnmergedChanges(querier gitdomain.Querier, branch
 }
 
 func (self *Commands) BranchInSyncWithParent(querier gitdomain.Querier, branch gitdomain.LocalBranchName, parent gitdomain.BranchName) (bool, error) {
-	output, err := querier.QueryTrim("git", "log", "--no-merges", "--format=%H", parent.String(), "^"+branch.String())
+	output, err := querier.QueryTrim("git", "log", "--no-merges", "--format=%H", parent.RefName(), "^"+branch.RefName())
 	return len(output) == 0, err
 }
 
diff --git a/internal/git/gitdomain/branch_name.go b/internal/git/gitdomain/branch_name.go
index 6b2005339..adf6cdd67 100644
--- a/internal/git/gitdomain/branch_name.go
+++ b/internal/git/gitdomain/branch_name.go
@@ -29,6 +29,14 @@ func (self BranchName) Location() Location {
 	return NewLocation(self.String())
 }
 
+// RefName provides the fully qualified reference name for this branch.
+func (self BranchName) RefName() string {
+	if self.IsLocal() {
+		return "refs/heads/" + self.String()
+	}
+	return self.String()
+}
+
 // RemoteName provides the remote version of this branch name.
 func (self BranchName) RemoteName() RemoteBranchName {
 	if strings.HasPrefix(string(self), "origin/") {
diff --git a/internal/git/gitdomain/local_branch_name.go b/internal/git/gitdomain/local_branch_name.go
index 22873d6db..0213c9bac 100644
--- a/internal/git/gitdomain/local_branch_name.go
+++ b/internal/git/gitdomain/local_branch_name.go
@@ -40,6 +40,11 @@ func (self LocalBranchName) Location() Location {
 	return NewLocation(string(self))
 }
 
+// RefName provides the fully qualified reference name for this branch.
+func (self LocalBranchName) RefName() string {
+	return "refs/heads/" + self.String()
+}
+
 // Implementation of the fmt.Stringer interface.
 func (self LocalBranchName) String() string { return string(self) }
 



Test patch:
diff --git a/internal/git/gitdomain/branch_name_test.go b/internal/git/gitdomain/branch_name_test.go
index 6070495cd..ef2da881b 100644
--- a/internal/git/gitdomain/branch_name_test.go
+++ b/internal/git/gitdomain/branch_name_test.go
@@ -62,6 +62,17 @@ func TestBranchName(t *testing.T) {
 		})
 	})
 
+	t.Run("RefName", func(t *testing.T) {
+		t.Parallel()
+		tests := map[gitdomain.BranchName]string{
+			"main":        "refs/heads/main",
+			"origin/main": "origin/main",
+		}
+		for give, want := range tests {
+			must.EqOp(t, want, give.RefName())
+		}
+	})
+
 	t.Run("RemoteName", func(t *testing.T) {
 		t.Run("local branch name", func(t *testing.T) {
 			t.Parallel()
diff --git a/internal/test/commands/test_commands.go b/internal/test/commands/test_commands.go
index 34b66397c..b6e5f1a2d 100644
--- a/internal/test/commands/test_commands.go
+++ b/internal/test/commands/test_commands.go
@@ -109,9 +109,9 @@ func (self *TestCommands) Commits(fields []string, mainBranch gitdomain.BranchNa
 func (self *TestCommands) CommitsInBranch(branch gitdomain.LocalBranchName, parentOpt Option[gitdomain.BranchName], fields []string) []testgit.Commit {
 	args := []string{"log", "--format=%H%x00%s%x00%an <%ae>", "--topo-order", "--reverse"}
 	if parent, hasParent := parentOpt.Get(); hasParent {
-		args = append(args, fmt.Sprintf("%s..%s", parent, branch))
+		args = append(args, fmt.Sprintf("%s..%s", parent.RefName(), branch.RefName()))
 	} else {
-		args = append(args, branch.String())
+		args = append(args, branch.RefName())
 	}
 	output := self.MustQuery("git", args...)
 	lines := strings.Split(output, "\n")