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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
package stats
import (
"bytes"
"compress/gzip"
"context"
"fmt"
"net/http"
"strings"
"time"
"gitlab.com/gitlab-org/gitaly/v15/internal/git/pktline"
)
// HTTPFetchPack is a FetchPack obtained via a clone of a target repository via HTTP. It contains
// additional information about the cloning process like status codes and timings.
type HTTPFetchPack struct {
start time.Time
responseHeader time.Duration
httpStatus int
stats FetchPack
wantedRefs []string
}
// ResponseHeader returns how long it took to receive the response header.
func (p *HTTPFetchPack) ResponseHeader() time.Duration { return p.responseHeader }
// HTTPStatus returns the HTTP status code.
func (p *HTTPFetchPack) HTTPStatus() int { return p.httpStatus }
// NAK returns how long it took to receive the NAK which signals that negotiation has concluded.
func (p *HTTPFetchPack) NAK() time.Duration { return p.stats.nak.Sub(p.start) }
// ResponseBody returns how long it took to receive the first bytes of the response body.
func (p *HTTPFetchPack) ResponseBody() time.Duration { return p.stats.responseBody.Sub(p.start) }
// Packets returns the number of Git packets received.
func (p *HTTPFetchPack) Packets() int { return p.stats.packets }
// LargestPacketSize returns the largest packet size received.
func (p *HTTPFetchPack) LargestPacketSize() int { return p.stats.largestPacketSize }
// RefsWanted returns the number of references sent to the remote repository as "want"s.
func (p *HTTPFetchPack) RefsWanted() int { return len(p.wantedRefs) }
// BandPackets returns how many packets were received on a specific sideband.
func (p *HTTPFetchPack) BandPackets(b string) int { return p.stats.multiband[b].packets }
// BandPayloadSize returns how many bytes were received on a specific sideband.
func (p *HTTPFetchPack) BandPayloadSize(b string) int64 { return p.stats.multiband[b].size }
// BandFirstPacket returns how long it took to receive the first packet on a specific sideband.
func (p *HTTPFetchPack) BandFirstPacket(b string) time.Duration {
return p.stats.multiband[b].firstPacket.Sub(p.start)
}
// See https://github.com/git/git/blob/v2.25.0/Documentation/technical/http-protocol.txt#L351
// for background information.
func buildFetchPackRequest(
ctx context.Context,
url, user, password string,
announcedRefs []Reference,
) (*http.Request, []string, error) {
var wants []string
for _, ref := range announcedRefs {
if strings.HasPrefix(ref.Name, "refs/heads/") || strings.HasPrefix(ref.Name, "refs/tags/") {
wants = append(wants, ref.Oid)
}
}
reqBodyRaw := &bytes.Buffer{}
reqBodyGzip := gzip.NewWriter(reqBodyRaw)
for i, oid := range wants {
if i == 0 {
oid += " multi_ack_detailed no-done side-band-64k thin-pack ofs-delta deepen-since deepen-not agent=git/2.21.0"
}
if _, err := pktline.WriteString(reqBodyGzip, "want "+oid+"\n"); err != nil {
return nil, nil, err
}
}
if err := pktline.WriteFlush(reqBodyGzip); err != nil {
return nil, nil, err
}
if _, err := pktline.WriteString(reqBodyGzip, "done\n"); err != nil {
return nil, nil, err
}
if err := reqBodyGzip.Close(); err != nil {
return nil, nil, err
}
req, err := http.NewRequest("POST", url+"/git-upload-pack", reqBodyRaw)
if err != nil {
return nil, nil, err
}
req = req.WithContext(ctx)
if user != "" {
req.SetBasicAuth(user, password)
}
for k, v := range map[string]string{
"User-Agent": "gitaly-debug",
"Content-Type": "application/x-git-upload-pack-request",
"Accept": "application/x-git-upload-pack-result",
"Content-Encoding": "gzip",
} {
req.Header.Set(k, v)
}
return req, wants, nil
}
func performFetchPack(
ctx context.Context,
url, user, password string,
announcedRefs []Reference,
reportProgress func(string, ...interface{}),
) (HTTPFetchPack, error) {
var fetchPack HTTPFetchPack
req, wants, err := buildFetchPackRequest(ctx, url, user, password, announcedRefs)
if err != nil {
return HTTPFetchPack{}, err
}
fetchPack.wantedRefs = wants
fetchPack.start = time.Now()
reportProgress("---\n")
reportProgress("--- POST %v\n", req.URL)
reportProgress("---\n")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return HTTPFetchPack{}, err
}
defer resp.Body.Close()
if code := resp.StatusCode; code < 200 || code >= 400 {
return HTTPFetchPack{}, fmt.Errorf("git http post: unexpected http status: %d", code)
}
fetchPack.responseHeader = time.Since(fetchPack.start)
fetchPack.httpStatus = resp.StatusCode
reportProgress("response code: %d\n", resp.StatusCode)
reportProgress("response header: %v\n", resp.Header)
fetchPack.stats.ReportProgress = func(b []byte) { reportProgress("%s", string(b)) }
if err := fetchPack.stats.Parse(resp.Body); err != nil {
return HTTPFetchPack{}, err
}
reportProgress("\n")
return fetchPack, nil
}
|