Detection only listed named remotes, so a checkout whose branch points straight at a URL (e.g. branch.main.remote=git@tangled.org:alice/project) failed with "0 remote(s)". Now URL values of the current branch's remote/pushRemote and remote.pushDefault are also checked, after named remotes, so existing setups are unchanged. Tested against temp repos, and verified live with the patched binary: tg issue view now resolves in such a checkout where the released binary fails.
+135
-1
Diff
Round #0
+45
-1
internal/gitutil/repo_context.go
+45
-1
internal/gitutil/repo_context.go
···
89
89
}
90
90
}
91
91
}
92
+
urls, err := c.branchRemoteURLs(ctx, remotes)
93
+
if err != nil {
94
+
return nil, err
95
+
}
96
+
for _, raw := range urls {
97
+
if candidate, ok := parseRepoCandidate(raw); ok {
98
+
candidates = append(candidates, *candidate)
99
+
}
100
+
}
92
101
if len(candidates) == 0 {
93
-
return nil, fmt.Errorf("no Tangled remote candidate found among %d remote(s) %q; pass the repository as handle/repo", len(remotes), remotes)
102
+
return nil, fmt.Errorf("no Tangled remote candidate found among %d remote(s) %q or the current branch's remote settings; pass the repository as handle/repo", len(remotes), remotes)
94
103
}
95
104
return candidates, nil
96
105
}
97
106
107
+
// branchRemoteURLs returns the current branch's remote settings that are URLs rather than remote names.
108
+
func (c *Client) branchRemoteURLs(ctx context.Context, remotes []string) ([]string, error) {
109
+
keys := []string{"remote.pushDefault"}
110
+
branch, err := c.gitOptionalLines(ctx, "symbolic-ref", "--quiet", "--short", "HEAD")
111
+
if err != nil {
112
+
return nil, fmt.Errorf("read current branch: %w", err)
113
+
}
114
+
if len(branch) == 1 {
115
+
keys = []string{"branch." + branch[0] + ".remote", "branch." + branch[0] + ".pushRemote", "remote.pushDefault"}
116
+
}
117
+
var urls []string
118
+
for _, key := range keys {
119
+
values, err := c.gitOptionalLines(ctx, "config", "--get-all", key)
120
+
if err != nil {
121
+
return nil, fmt.Errorf("read git config %s: %w", key, err)
122
+
}
123
+
for _, value := range values {
124
+
if !slices.Contains(remotes, value) && !slices.Contains(urls, value) {
125
+
urls = append(urls, value)
126
+
}
127
+
}
128
+
}
129
+
return urls, nil
130
+
}
131
+
98
132
func DetectRepoCandidatesFromCWD(ctx context.Context) ([]RepoContext, error) {
99
133
return defaultClient.DetectRepoCandidatesFromCWD(ctx)
100
134
}
···
206
240
}, true
207
241
}
208
242
243
+
// gitOptionalLines is gitLines that treats exit status 1 (unset key, detached HEAD) as no lines.
244
+
func (c *Client) gitOptionalLines(ctx context.Context, args ...string) ([]string, error) {
245
+
lines, err := c.gitLines(ctx, args...)
246
+
var exitErr *exec.ExitError
247
+
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
248
+
return nil, nil
249
+
}
250
+
return lines, err
251
+
}
252
+
209
253
// gitLines runs git with the given args and returns non-empty output lines.
210
254
func (c *Client) gitLines(ctx context.Context, args ...string) ([]string, error) {
211
255
// Output is intentionally captured; diagnostics still go to the client's sink.
+90
internal/gitutil/repo_context_test.go
+90
internal/gitutil/repo_context_test.go
···
1
1
package gitutil
2
2
3
3
import (
4
+
"context"
5
+
"reflect"
4
6
"slices"
5
7
"strings"
6
8
"testing"
···
167
169
})
168
170
}
169
171
}
172
+
173
+
func TestDetectRepoCandidatesFromBranchRemoteSettings(t *testing.T) {
174
+
tests := []struct {
175
+
name string
176
+
setup func(t *testing.T, repoDir string)
177
+
want []RepoContext
178
+
}{
179
+
{
180
+
// No named remotes: the branch fetches from and pushes to a URL.
181
+
name: "branch remote is a URL",
182
+
setup: func(t *testing.T, repoDir string) {
183
+
runGit(t, repoDir, "config", "branch.main.remote", "git@tangled.org:alice.example.com/project")
184
+
},
185
+
want: []RepoContext{{Handle: "alice.example.com", Repo: "project"}},
186
+
},
187
+
{
188
+
name: "branch pushRemote is a URL",
189
+
setup: func(t *testing.T, repoDir string) {
190
+
runGit(t, repoDir, "config", "branch.main.pushRemote", "https://tangled.org/alice.example.com/project")
191
+
},
192
+
want: []RepoContext{{Handle: "alice.example.com", Repo: "project"}},
193
+
},
194
+
{
195
+
name: "detached HEAD uses remote.pushDefault",
196
+
setup: func(t *testing.T, repoDir string) {
197
+
runGit(t, repoDir, "config", "branch.main.remote", "git@tangled.org:alice.example.com/ignored")
198
+
runGit(t, repoDir, "config", "remote.pushDefault", "git@tangled.org:alice.example.com/project")
199
+
runGit(t, repoDir, "checkout", "--detach")
200
+
},
201
+
want: []RepoContext{{Handle: "alice.example.com", Repo: "project"}},
202
+
},
203
+
{
204
+
// Named remotes keep precedence; a URL setting is appended once.
205
+
name: "named remotes first, repeated URL once",
206
+
setup: func(t *testing.T, repoDir string) {
207
+
runGit(t, repoDir, "remote", "add", "origin", "git@tangled.org:alice.example.com/origin")
208
+
runGit(t, repoDir, "config", "branch.main.remote", "git@tangled.org:alice.example.com/project")
209
+
runGit(t, repoDir, "config", "branch.main.pushRemote", "git@tangled.org:alice.example.com/project")
210
+
},
211
+
want: []RepoContext{
212
+
{Handle: "alice.example.com", Repo: "origin"},
213
+
{Handle: "alice.example.com", Repo: "project"},
214
+
},
215
+
},
216
+
{
217
+
// A setting that names a remote was already scanned via that remote.
218
+
name: "setting naming a remote is not scanned twice",
219
+
setup: func(t *testing.T, repoDir string) {
220
+
runGit(t, repoDir, "remote", "add", "origin", "git@tangled.org:alice.example.com/project")
221
+
runGit(t, repoDir, "config", "branch.main.remote", "origin")
222
+
runGit(t, repoDir, "config", "remote.pushDefault", "origin")
223
+
},
224
+
want: []RepoContext{{Handle: "alice.example.com", Repo: "project"}},
225
+
},
226
+
{
227
+
name: "multi-valued key does not fail detection",
228
+
setup: func(t *testing.T, repoDir string) {
229
+
runGit(t, repoDir, "config", "--add", "branch.main.pushRemote", "git@tangled.org:alice.example.com/project")
230
+
runGit(t, repoDir, "config", "--add", "branch.main.pushRemote", "https://example.com/not-tangled.git")
231
+
},
232
+
want: []RepoContext{{Handle: "alice.example.com", Repo: "project"}},
233
+
},
234
+
}
235
+
for _, tt := range tests {
236
+
t.Run(tt.name, func(t *testing.T) {
237
+
repoDir := newPullBaseTestRepo(t)
238
+
tt.setup(t, repoDir)
239
+
t.Chdir(repoDir)
240
+
got, err := NewClient(nil, nil).DetectRepoCandidatesFromCWD(context.Background())
241
+
if err != nil {
242
+
t.Fatalf("DetectRepoCandidatesFromCWD() error = %v", err)
243
+
}
244
+
if !reflect.DeepEqual(got, tt.want) {
245
+
t.Fatalf("DetectRepoCandidatesFromCWD() = %+v, want %+v", got, tt.want)
246
+
}
247
+
})
248
+
}
249
+
}
250
+
251
+
func TestDetectRepoCandidatesWithoutAnyTangledRemote(t *testing.T) {
252
+
repoDir := newPullBaseTestRepo(t)
253
+
runGit(t, repoDir, "config", "branch.main.remote", "https://example.com/not-tangled.git")
254
+
t.Chdir(repoDir)
255
+
_, err := NewClient(nil, nil).DetectRepoCandidatesFromCWD(context.Background())
256
+
if err == nil || !strings.Contains(err.Error(), "current branch's remote settings") {
257
+
t.Fatalf("DetectRepoCandidatesFromCWD() error = %v, want a no-candidate error naming branch settings", err)
258
+
}
259
+
}
History
1 round
0 comments
okami.mom
submitted
#0
1 commit
Expand
Collapse
gitutil: detect repositories from URL-valued branch remote settings
3/3 success
Expand
Collapse
Expand 0 comments
Pull request successfully merged