close
An unofficial terminal client for Tangled, optimized for humans and agents. tgcli.wisp.place
cli atproto go tangled
164

Configure Feed

Select the types of activity you want to include in your feed.

gitutil: detect repositories from URL-valued branch remote settings #34

Merged opened by okami.mom targeting master from okami.mom/tg-2: detect-url-valued-branch-remote

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.

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3rwz3xfw2crswgifqgc3g7zh/sh.tangled.repo.pull/3mwa6m2xluf22
+135 -1
Diff #0
+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
··· 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
Sign up or Login to add to the discussion
okami.mom submitted #0
1 commit
Expand
gitutil: detect repositories from URL-valued branch remote settings
3/3 success
Expand
Expand 0 comments
Pull request successfully merged