Skip to content

Commit d0d5d69

Browse files
Add Gerrit forge adapter (#119)
* Add Gerrit forge adapter * Address review comments on gerrit adapter * fix: format gerrit.go to pass gofmt lint check * fix: address review comments on gerrit URLs and default domain * Address Gerrit review feedback
1 parent 83cc189 commit d0d5d69

21 files changed

Lines changed: 1650 additions & 5 deletions

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# forge
22

3-
Go library and CLI for working with git forges. Supports GitHub, GitLab, Gitea/Forgejo, Bitbucket Cloud, and Tangled through a single interface.
3+
Go library and CLI for working with git forges. Supports GitHub, GitLab, Gitea/Forgejo, Bitbucket Cloud, Gerrit, and Tangled through a single interface.
44

55
## CLI
66

@@ -42,6 +42,7 @@ Store tokens with `forge auth login`:
4242
forge auth login # interactive: asks domain + token
4343
forge auth login --domain github.com --token ghp_abc123
4444
forge auth login --domain gitea.example.com --token abc123 --type gitea
45+
forge auth login --domain gerrit.example.com --token user:http_password --type gerrit
4546
forge auth login --domain github.com --token-cmd 'rbw get github-token'
4647
```
4748

@@ -76,6 +77,10 @@ token = ghp_abc123
7677
[gitea.example.com]
7778
type = gitea
7879
token = abc123
80+
81+
[gerrit.example.com]
82+
type = gerrit
83+
token = user:http_password
7984
```
8085

8186
### Token commands

detect.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package forges
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"fmt"
8+
"io"
79
"net/http"
810
"strings"
911
)
@@ -68,6 +70,11 @@ func detectFromAPI(ctx context.Context, client *http.Client, baseURL string) (Fo
6870
return GitLab, nil
6971
}
7072

73+
// Try Gerrit /config/server/version
74+
if ok, err := probeGerritAPI(ctx, client, baseURL); err == nil && ok {
75+
return Gerrit, nil
76+
}
77+
7178
// Try GitHub Enterprise /api/v3/meta
7279
if ok, err := probeURL(ctx, client, baseURL+"/api/v3/meta"); err == nil && ok {
7380
return GitHub, nil
@@ -110,6 +117,32 @@ func probeGiteaAPI(ctx context.Context, client *http.Client, baseURL string) (Fo
110117
return Gitea, nil
111118
}
112119

120+
func probeGerritAPI(ctx context.Context, client *http.Client, baseURL string) (bool, error) {
121+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/config/server/version", nil)
122+
if err != nil {
123+
return false, err
124+
}
125+
126+
resp, err := client.Do(req)
127+
if err != nil {
128+
return false, err
129+
}
130+
defer func() { _ = resp.Body.Close() }()
131+
132+
if resp.StatusCode != http.StatusOK {
133+
return false, fmt.Errorf("status %d", resp.StatusCode)
134+
}
135+
136+
body, err := io.ReadAll(resp.Body)
137+
if err != nil {
138+
return false, err
139+
}
140+
body = bytes.TrimPrefix(body, []byte(")]}'\n"))
141+
142+
var version string
143+
return json.Unmarshal(body, &version) == nil && version != "", nil
144+
}
145+
113146
func probeURL(ctx context.Context, client *http.Client, url string) (bool, error) {
114147
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
115148
if err != nil {

forge.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ func (c *Client) RegisterDomain(ctx context.Context, domain, token string, build
147147
c.forges[domain] = builders.GitLab(baseURL, token, c.httpClient)
148148
case Gitea, Forgejo:
149149
c.forges[domain] = builders.Gitea(baseURL, token, c.httpClient)
150+
case Gerrit:
151+
if builders.Gerrit == nil {
152+
return fmt.Errorf("no builder registered for forge type %q at %s", ft, domain)
153+
}
154+
c.forges[domain] = builders.Gerrit(baseURL, token, c.httpClient)
150155
case Tangled:
151156
if builders.Tangled == nil {
152157
return fmt.Errorf("no builder registered for forge type %q at %s", ft, domain)
@@ -164,6 +169,7 @@ type ForgeBuilders struct {
164169
GitHub func(baseURL, token string, hc *http.Client) Forge
165170
GitLab func(baseURL, token string, hc *http.Client) Forge
166171
Gitea func(baseURL, token string, hc *http.Client) Forge
172+
Gerrit func(baseURL, token string, hc *http.Client) Forge
167173
Tangled func(baseURL, token string, hc *http.Client) Forge
168174
}
169175

forges_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,30 @@ func TestDetectForgeTypeGitHubAPI(t *testing.T) {
324324
}
325325
}
326326

327+
func TestDetectForgeTypeGerritAPI(t *testing.T) {
328+
mux := http.NewServeMux()
329+
mux.HandleFunc("GET /api/v1/version", func(w http.ResponseWriter, r *http.Request) {
330+
w.WriteHeader(http.StatusNotFound)
331+
})
332+
mux.HandleFunc("GET /api/v4/version", func(w http.ResponseWriter, r *http.Request) {
333+
w.WriteHeader(http.StatusNotFound)
334+
})
335+
mux.HandleFunc("GET /config/server/version", func(w http.ResponseWriter, r *http.Request) {
336+
_, _ = fmt.Fprintf(w, ")]}'\n\"3.9.0\"")
337+
})
338+
339+
srv := httptest.NewServer(mux)
340+
defer srv.Close()
341+
342+
ft, err := detectFromAPI(context.Background(), http.DefaultClient, srv.URL)
343+
if err != nil {
344+
t.Fatalf("unexpected error: %v", err)
345+
}
346+
if ft != Gerrit {
347+
t.Errorf("want Gerrit, got %s", ft)
348+
}
349+
}
350+
327351
func TestDetectForgeTypeTangledAPI(t *testing.T) {
328352
mux := http.NewServeMux()
329353
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
@@ -354,6 +378,47 @@ func TestDetectForgeTypeTangledAPI(t *testing.T) {
354378
}
355379
}
356380

381+
func TestRegisterDomainGerritMissingBuilderReturnsError(t *testing.T) {
382+
mux := http.NewServeMux()
383+
mux.HandleFunc("GET /api/v1/version", func(w http.ResponseWriter, r *http.Request) {
384+
w.WriteHeader(http.StatusNotFound)
385+
})
386+
mux.HandleFunc("GET /api/v4/version", func(w http.ResponseWriter, r *http.Request) {
387+
w.WriteHeader(http.StatusNotFound)
388+
})
389+
mux.HandleFunc("GET /config/server/version", func(w http.ResponseWriter, r *http.Request) {
390+
_, _ = fmt.Fprintf(w, ")]}'\n\"3.9.0\"")
391+
})
392+
srv := httptest.NewServer(mux)
393+
defer srv.Close()
394+
395+
base, err := url.Parse(srv.URL)
396+
if err != nil {
397+
t.Fatal(err)
398+
}
399+
client := NewClient(WithHTTPClient(&http.Client{
400+
Transport: rewriteHostTransport{base: base},
401+
}))
402+
403+
err = client.RegisterDomain(context.Background(), "gerrit.example", "", ForgeBuilders{
404+
GitHub: func(baseURL, token string, hc *http.Client) Forge {
405+
return &mockForge{}
406+
},
407+
GitLab: func(baseURL, token string, hc *http.Client) Forge {
408+
return &mockForge{}
409+
},
410+
Gitea: func(baseURL, token string, hc *http.Client) Forge {
411+
return &mockForge{}
412+
},
413+
})
414+
if err == nil {
415+
t.Fatal("expected missing Gerrit builder error")
416+
}
417+
if !strings.Contains(err.Error(), "no builder registered") || !strings.Contains(err.Error(), "gerrit") {
418+
t.Fatalf("unexpected error: %v", err)
419+
}
420+
}
421+
357422
func TestRegisterDomainTangledMissingBuilderReturnsError(t *testing.T) {
358423
mux := http.NewServeMux()
359424
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {

gerrit/branches.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package gerrit
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
forge "github.com/git-pkgs/forge"
8+
)
9+
10+
type gerritBranchService struct {
11+
forge *gerritForge
12+
}
13+
14+
func (f *gerritForge) Branches() forge.BranchService {
15+
return &gerritBranchService{forge: f}
16+
}
17+
18+
func (s *gerritBranchService) List(ctx context.Context, owner, repo string, opts forge.ListBranchOpts) ([]forge.Branch, error) {
19+
project := projectName(owner, repo)
20+
var infos []struct {
21+
Ref string `json:"ref"`
22+
Revision string `json:"revision"`
23+
CanDelete bool `json:"can_delete"`
24+
}
25+
if err := s.forge.doJSON(ctx, http.MethodGet, "/projects/"+encodeID(project)+"/branches/", nil, nil, &infos); err != nil {
26+
return nil, err
27+
}
28+
29+
branches := make([]forge.Branch, 0, len(infos))
30+
for _, info := range infos {
31+
branches = append(branches, forge.Branch{
32+
Name: trimRefPrefix(info.Ref),
33+
SHA: info.Revision,
34+
})
35+
if opts.Limit > 0 && len(branches) >= opts.Limit {
36+
break
37+
}
38+
}
39+
return branches, nil
40+
}
41+
42+
func (s *gerritBranchService) Create(ctx context.Context, owner, repo, name, from string) (*forge.Branch, error) {
43+
project := projectName(owner, repo)
44+
body := map[string]string{}
45+
if from != "" {
46+
body["revision"] = from
47+
}
48+
49+
var info struct {
50+
Ref string `json:"ref"`
51+
Revision string `json:"revision"`
52+
}
53+
if err := s.forge.doJSON(ctx, http.MethodPut, "/projects/"+encodeID(project)+"/branches/"+encodeID(name), nil, body, &info); err != nil {
54+
return nil, err
55+
}
56+
57+
return &forge.Branch{Name: trimRefPrefix(info.Ref), SHA: info.Revision}, nil
58+
}
59+
60+
func (s *gerritBranchService) Delete(ctx context.Context, owner, repo, name string) error {
61+
project := projectName(owner, repo)
62+
return s.forge.doJSON(ctx, http.MethodDelete, "/projects/"+encodeID(project)+"/branches/"+encodeID(name), nil, nil, nil)
63+
}

0 commit comments

Comments
 (0)