useragent: identify git-sync in outbound User-Agent · Entire

useragent: identify git-sync in outbound User-Agent

40abe29→main·

pjbgf·1mo ago·7 files·+86 added/-9 removed

Replace the bare go-git User-Agent on outbound requests with a git-sync-flavoured string. Git wire-protocol traffic (smart-HTTP info-refs and upload-pack/receive-pack RPCs, plus the protocol v2 "agent=" capability) sends "git-sync/ go-git/" to preserve go-git attribution servers and operators key off; non-git HTTP (the GitHub repo metadata call in the bootstrap strategy) sends just "git-sync/".

A new internal/useragent package centralises the format and holds a package-level Version var. The CLI wires this from versioninfo.Version at startup so --version and the User-Agent agree; SDK consumers can override it before issuing requests.

Assisted-by: Claude Opus 4.7 noreply@anthropic.com Signed-off-by: Paulo Gomes paulo@entire.io

Sessions

fb4821afd3f1View transcript

Changes

7

6 unmodified lines

7
8
9
10
11
12
13
14
15
16
17
18
19

6 unmodified lines

"os"
    "strings"

"entire.io/entire/git-sync/cmd/git-sync/internal/versioninfo"
    "entire.io/entire/git-sync/internal/useragent"
    "github.com/spf13/cobra"
)

func main() {
    useragent.Version = versioninfo.Version
    err := run(context.Background(), os.Args[1:])
    if err == nil {
        return
    }

Mcmd/git-sync/main.go+3

5 unmodified lines

6
7
8
9
10
11
12
54 unmodified lines

67
68
69
69
70
71
72
73

5 unmodified lines

"sort"
    "strings"

"entire.io/entire/git-sync/internal/useragent"
    "github.com/go-git/go-git/v6/plumbing/protocol/capability";

54 unmodified lines

func (c *V2Capabilities) RequestCapabilities() []string {
    var caps []string
    if agent := c.Value("agent"); agent != "" {
        caps = append(caps, "agent="+capability.DefaultAgent())
        caps = append(caps, "agent="+useragent.GoGit())
    }
    return caps
}

Minternal/gitproto/capability.go+2/-1

2 unmodified lines

3
4
5
6
7
8
9
159 unmodified lines

169
170
171
171
172
172
173
174
175
176

2 unmodified lines

import (
    "testing"

"entire.io/entire/git-sync/internal/useragent"
    "github.com/go-git/go-git/v6/plumbing/protocol/capability"
)

159 unmodified lines

if len(got) != 1 {
        t.Fatalf("RequestCapabilities() returned %d items, want 1", len(got))
    }
    if got[0] != "agent="+capability.DefaultAgent() {
        t.Errorf("RequestCapabilities()[0] = %q, want %q", got[0], "agent="+capability.DefaultAgent())
    }
    if got[0] != "agent="+useragent.GoGit() {
        t.Errorf("RequestCapabilities()[0] = %q, want %q", got[0], "agent="+useragent.GoGit())
    }

// Without agent, RequestCapabilities should return empty.

Minternal/gitproto/capability_test.go+3/-2

13 unmodified lines

14
15
16
17
17
18
19
20
507 unmodified lines

528
529
530
531
531
532
533
534
128 unmodified lines

663
664
665
666
666
667
668
669
153 unmodified lines

823
824
825
826
826
827
828
829

13 unmodified lines

"os"
    "strings"

"github.com/go-git/go-git/v6/plumbing/protocol/capability"
    "entire.io/entire/git-sync/internal/useragent"
    transporthttp "github.com/go-git/go-git/v6/plumbing/transport/http"
)

507 unmodified lines

}
    req.Header.Set("Content-Type", fmt.Sprintf("application/x-%s-request", service))
    req.Header.Set("Accept", fmt.Sprintf("application/x-%s-result", service))
    req.Header.Set("User-Agent", capability.DefaultAgent())
    req.Header.Set("User-Agent", useragent.GoGit())
    req.Header.Set(StatsPhaseHeader, phase)
    if v2 {
        rq.Header.Set("Git-Protocol", GitProtocolV2)
    }

Minternal/gitproto/smarthttp.go+4/-4

17 unmodified lines

18
19
20
21
21
22
23
24
25
26
27
28
1309 unmodified lines

1338
1339
1340
1341
1341
1342
1343
1344

17 unmodified lines

git "github.com/go-git/go-git/v6"
    "github.com/go-git/go-git/v6/plumbing"
    "github.com/go-git/go-git/v6/plumbing/protocol/capability"

"entire.io/entire/git-sync/internal/convert"
    "entire.io/entire/git-sync/internal/gitproto"
    "entire.io/entire/git-sync/internal/planner"
    "entire.io/entire/git-sync/internal/useragent"

const (
1309 unmodified lines

}
    req.Header.Set("Accept", "application/vnd.github+json")
    req.Header.Set("X-Github-Api-Version", "2022-11-28")
    req.Header.Set("User-Agent", capability.DefaultAgent())
    req.Header.Set("User-Agent", useragent.Plain())
    req.Header.Set(gitproto.StatsPhaseHeader, "github repo metadata")
    resp, err := httpConn.HTTP.Do(req)
    if err != nil {

Minternal/strategy/bootstrap/bootstrap.go+2/-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

// Package useragent builds the User-Agent strings git-sync advertises to
// remote services. Two flavours: GoGit for git wire-protocol traffic
// (HTTP smart-protocol requests and the protocol-level "agent="
// capability), and Plain for non-git HTTP requests such as provider
// metadata APIs.
package useragent

import "github.com/go-git/go-git/v6/plumbing/protocol/capability"

// Version is the git-sync version advertised in User-Agent strings.
// CLI builds set this from versioninfo.Version; SDK consumers may
// overwrite it before issuing any requests if they want to identify a
// different version.
var Version = "dev"

// GoGit returns the User-Agent for git wire-protocol traffic. Format:
// "git-sync/<version> go-git/<go-git-version>". The go-git suffix is
// preserved because servers and operators commonly key off it.
func GoGit() string {
    return "git-sync/" + Version + " " + capability.DefaultAgent()
}

// Plain returns the User-Agent for non-git HTTP requests (e.g. provider
// REST APIs). Format: "git-sync/<version>".
func Plain() string {
    return "git-sync/" + Version
}

Ainternal/useragent/useragent.go+27

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

package useragent

import (
    "strings"
    "testing"

"github.com/go-git/go-git/v6/plumbing/protocol/capability"
)

func TestGoGit(t *testing.T) {

t.Parallel()

got := GoGit()
    wantPrefix := "git-sync/" + Version + " "
    if !strings.HasPrefix(got, wantPrefix) {
        t.Errorf("GoGit() = %q, want prefix %q", got, wantPrefix)
    }
    if !strings.Contains(got, capability.DefaultAgent()) {
        t.Errorf("GoGit() = %q, want it to contain %q", got, capability.DefaultAgent())
    }
}

func TestPlain(t *testing.T) {

t.Parallel()

got := Plain()
    want := "git-sync/" + Version
    if got != want {
        t.Errorf("Plain() = %q, want %q", got, want)
    }
}

func TestVersionOverride(t *testing.T) {
    // Not parallel — mutates package-level Version.
    orig := Version
    t.Cleanup(func() { Version = orig })

Version = "1.2.3"
    if got, want := Plain(), "git-sync/1.2.3"; got != want {
        t.Errorf("Plain() with overridden Version = %q, want %q", got, want)
    }
    if got, want := GoGit(), "git-sync/1.2.3 "+capability.DefaultAgent(); got != want {
        t.Errorf("GoGit() with overridden Version = %q, want %q", got, want)
    }
}
}