Welcome to mirror list, hosted at ThFree Co, Russian Federation.

http_fetch_pack.go « stats « git « internal - gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 07089d5b18ae691a7d8e9f2c4f681dcd138db23f (plain)
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
}