blob: e5c3872a3f0922dfe70f52244670314f94b76b6a [file] [log] [blame]
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +02001// Copyright 2016 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package gitiles is a client library for the Gitiles source viewer.
16package gitiles
17
Han-Wen Nienhuysf92f01c2017-12-04 10:44:14 +010018// The gitiles command set is defined here:
19//
20// https://gerrit.googlesource.com/gitiles/+/7c07a4a68ece6009909206482e0728dbbf0be77d/java/com/google/gitiles/ViewFilter.java#47
21
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +020022import (
23 "bytes"
24 "encoding/base64"
25 "encoding/json"
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020026 "flag"
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +020027 "fmt"
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +010028 "io"
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +020029 "io/ioutil"
30 "log"
31 "net/http"
32 "net/url"
33 "path"
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +010034 "strings"
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020035
Han-Wen Nienhuys0213eb62016-07-19 13:52:51 +020036 "github.com/google/slothfs/cookie"
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020037 "golang.org/x/net/context"
38 "golang.org/x/time/rate"
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +020039)
40
41// Service is a client for the Gitiles JSON interface.
42type Service struct {
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020043 limiter *rate.Limiter
44 addr url.URL
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +020045 client http.Client
46 agent string
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +010047 debug bool
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +020048}
49
50// Addr returns the address of the gitiles service.
51func (s *Service) Addr() string {
52 return s.addr.String()
53}
54
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020055// Options configures the the Gitiles service.
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020056type Options struct {
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020057 // A URL for the Gitiles service.
58 Address string
59
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020060 BurstQPS int
61 SustainedQPS float64
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +020062
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020063 // Path to a Netscape/Mozilla style cookie file.
64 CookieJar string
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +020065
66 // UserAgent defines how we present ourself to the server.
67 UserAgent string
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +010068
Billy Lynchecdd2552017-01-12 18:31:36 -050069 // HTTPClient allows callers to present their own http.Client instead of the default.
70 HTTPClient http.Client
71
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +010072 Debug bool
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020073}
74
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020075var defaultOptions Options
Han-Wen Nienhuys0213eb62016-07-19 13:52:51 +020076
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020077// DefineFlags sets up standard command line flags, and returns the
78// options struct in which the values are put.
79func DefineFlags() *Options {
Han-Wen Nienhuysfa039492016-08-10 12:42:29 +020080 flag.StringVar(&defaultOptions.Address, "gitiles_url", "https://android.googlesource.com", "Set the URL of the Gitiles service.")
81 flag.StringVar(&defaultOptions.CookieJar, "gitiles_cookies", "", "Set path to cURL-style cookie jar file.")
82 flag.StringVar(&defaultOptions.UserAgent, "gitiles_agent", "slothfs", "Set the User-Agent string to report to Gitiles.")
83 flag.Float64Var(&defaultOptions.SustainedQPS, "gitiles_qps", 4, "Set the maximum QPS to send to Gitiles.")
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +010084 flag.BoolVar(&defaultOptions.Debug, "gitiles_debug", false, "Print URLs as they are fetched.")
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020085 return &defaultOptions
Han-Wen Nienhuys0213eb62016-07-19 13:52:51 +020086}
87
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +020088// NewService returns a new Gitiles JSON client.
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020089func NewService(opts Options) (*Service, error) {
90 var jar http.CookieJar
91 if nm := opts.CookieJar; nm != "" {
92 var err error
93 jar, err = cookie.NewJar(nm)
94 if err != nil {
95 return nil, err
96 }
97 if err := cookie.WatchJar(jar, nm); err != nil {
98 return nil, err
99 }
100 }
101
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +0200102 if opts.SustainedQPS == 0.0 {
Han-Wen Nienhuys965dcb32016-08-01 14:09:41 +0200103 opts.SustainedQPS = 4
104 }
105 if opts.BurstQPS == 0 {
106 opts.BurstQPS = int(10.0 * opts.SustainedQPS)
107 } else if float64(opts.BurstQPS) < opts.SustainedQPS {
108 opts.BurstQPS = int(opts.SustainedQPS) + 1
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +0200109 }
110
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +0200111 url, err := url.Parse(opts.Address)
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200112 if err != nil {
113 return nil, err
114 }
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200115 s := &Service{
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +0200116 limiter: rate.NewLimiter(rate.Limit(opts.SustainedQPS), opts.BurstQPS),
117 addr: *url,
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200118 agent: opts.UserAgent,
Billy Lynchecdd2552017-01-12 18:31:36 -0500119 client: opts.HTTPClient,
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200120 }
121
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +0200122 s.client.Jar = jar
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200123 s.client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
124 req.Header.Set("User-Agent", s.agent)
125 return nil
126 }
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +0100127 s.debug = opts.Debug
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200128 return s, nil
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200129}
130
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100131func (s *Service) stream(u *url.URL) (*http.Response, error) {
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +0200132 ctx := context.Background()
133
134 if err := s.limiter.Wait(ctx); err != nil {
135 return nil, err
136 }
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200137 req, err := http.NewRequest("GET", u.String(), nil)
138 if err != nil {
139 return nil, err
140 }
141 req.Header.Add("User-Agent", s.agent)
142 resp, err := s.client.Do(req)
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +0200143
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200144 if err != nil {
145 return nil, err
146 }
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100147
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200148 if resp.StatusCode != 200 {
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100149 resp.Body.Close()
Han-Wen Nienhuys39ace572016-06-02 18:30:51 +0200150 return nil, fmt.Errorf("%s: %s", u.String(), resp.Status)
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200151 }
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +0100152
153 if s.debug {
154 log.Printf("%s %s: %d", req.Method, req.URL, resp.StatusCode)
155 }
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200156 if got := resp.Request.URL.String(); got != u.String() {
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100157 resp.Body.Close()
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200158 // We accept redirects, but only for authentication.
159 // If we get a 200 from a different page than we
160 // requested, it's probably some sort of login page.
161 return nil, fmt.Errorf("got URL %s, want %s", got, u.String())
162 }
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200163
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100164 return resp, nil
165}
166
167func (s *Service) get(u *url.URL) ([]byte, error) {
168 resp, err := s.stream(u)
169 if err != nil {
170 return nil, err
171 }
172
173 defer resp.Body.Close()
174
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200175 c, err := ioutil.ReadAll(resp.Body)
176 if err != nil {
177 return nil, err
178 }
Han-Wen Nienhuysae6d11c2016-07-11 13:50:45 +0200179
Han-Wen Nienhuys163ec592016-07-11 14:34:17 +0200180 if resp.Header.Get("Content-Type") == "text/plain; charset=UTF-8" {
Han-Wen Nienhuysae6d11c2016-07-11 13:50:45 +0200181 out := make([]byte, base64.StdEncoding.DecodedLen(len(c)))
182 n, err := base64.StdEncoding.Decode(out, c)
183 return out[:n], err
184 }
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200185 return c, nil
186}
187
188var xssTag = []byte(")]}'\n")
189
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200190func (s *Service) getJSON(u *url.URL, dest interface{}) error {
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200191 c, err := s.get(u)
192 if err != nil {
193 return err
194 }
195
196 if !bytes.HasPrefix(c, xssTag) {
197 return fmt.Errorf("Gitiles JSON %s missing XSS tag: %q", u, c)
198 }
199 c = c[len(xssTag):]
200
201 err = json.Unmarshal(c, dest)
202 if err != nil {
203 err = fmt.Errorf("Unmarshal(%s): %v", u, err)
204 }
205 return err
206}
207
208// List retrieves the list of projects.
Han-Wen Nienhuys8b960812016-07-13 18:15:57 +0200209func (s *Service) List(branches []string) (map[string]*Project, error) {
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200210 listURL := s.addr
211 listURL.RawQuery = "format=JSON"
Han-Wen Nienhuys8b960812016-07-13 18:15:57 +0200212 for _, b := range branches {
213 listURL.RawQuery += "&b=" + b
214 }
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200215
216 projects := map[string]*Project{}
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200217 err := s.getJSON(&listURL, &projects)
Han-Wen Nienhuysd42875c2016-11-30 19:07:27 +0100218 for k, v := range projects {
219 if k != v.Name {
220 return nil, fmt.Errorf("gitiles: key %q had project name %q", k, v.Name)
221 }
222 }
223
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200224 return projects, err
225}
226
Han-Wen Nienhuysa501c042016-07-21 16:28:49 +0200227// NewRepoService creates a service for a specific repository on a Gitiles server.
Han-Wen Nienhuys39ace572016-06-02 18:30:51 +0200228func (s *Service) NewRepoService(name string) *RepoService {
229 return &RepoService{
230 Name: name,
231 service: s,
232 }
233}
234
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200235// RepoService is a JSON client for the functionality of a specific
236// respository.
237type RepoService struct {
238 Name string
239 service *Service
240}
241
Han-Wen Nienhuys27dde402016-06-03 17:44:33 +0200242// Get retrieves a single project.
243func (s *RepoService) Get() (*Project, error) {
244 jsonURL := s.service.addr
245 jsonURL.Path = path.Join(jsonURL.Path, s.Name)
246 jsonURL.RawQuery = "format=JSON"
247
248 var p Project
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200249 err := s.service.getJSON(&jsonURL, &p)
Han-Wen Nienhuys27dde402016-06-03 17:44:33 +0200250 return &p, err
251}
252
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200253// GetBlob fetches a blob.
254func (s *RepoService) GetBlob(branch, filename string) ([]byte, error) {
255 blobURL := s.service.addr
256
257 blobURL.Path = path.Join(blobURL.Path, s.Name, "+show", branch, filename)
258 blobURL.RawQuery = "format=TEXT"
259
260 // TODO(hanwen): invent a more structured mechanism for logging.
261 log.Println(blobURL.String())
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200262 return s.service.get(&blobURL)
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200263}
264
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100265// Archive formats for +archive. JGit also supports some shorthands.
266const (
267 ArchiveTbz = "tar.bz2"
268 ArchiveTgz = "tar.gz"
269 ArchiveTar = "tar"
270 ArchiveTxz = "tar.xz"
271
272 // the Gitiles source code claims .tar.xz and .tar are
273 // supported, but googlesource.com doesn't support it,
274 // apparently. In addition, JGit provides ZipFormat, but
275 // gitiles doesn't support it.
276)
277
278// GetArchive downloads an archive of the project. Format is one
279// ArchivXxx formats. dirPrefix, if given, restricts to the given
280// subpath, and strips the path prefix from the files in the resulting
281// tar archive. revision is a git revision, either a branch/tag name
282// ("master") or a hex commit SHA1.
283func (s *RepoService) GetArchive(revision, dirPrefix, format string) (io.ReadCloser, error) {
284 u := s.service.addr
285 u.Path = path.Join(u.Path, s.Name, "+archive", revision)
286 if dirPrefix != "" {
287 u.Path = path.Join(u.Path, dirPrefix)
288 }
289 u.Path += "." + format
290 resp, err := s.service.stream(&u)
291 if err != nil {
292 return nil, err
293 }
294 return resp.Body, err
295}
296
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200297// GetTree fetches a tree. The dir argument may not point to a
298// blob. If recursive is given, the server recursively expands the
299// tree.
300func (s *RepoService) GetTree(branch, dir string, recursive bool) (*Tree, error) {
301 jsonURL := s.service.addr
302 jsonURL.Path = path.Join(jsonURL.Path, s.Name, "+", branch, dir)
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +0100303 if !strings.HasSuffix(jsonURL.Path, "/") {
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200304 jsonURL.Path += "/"
305 }
306 jsonURL.RawQuery = "format=JSON&long=1"
307
308 if recursive {
309 jsonURL.RawQuery += "&recursive=1"
310 }
311
312 var tree Tree
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200313 err := s.service.getJSON(&jsonURL, &tree)
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200314 return &tree, err
315}
316
317// GetCommit gets the data of a commit in a branch.
318func (s *RepoService) GetCommit(branch string) (*Commit, error) {
319 jsonURL := s.service.addr
320 jsonURL.Path = path.Join(jsonURL.Path, s.Name, "+", branch)
321 jsonURL.RawQuery = "format=JSON"
322
323 var c Commit
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200324 err := s.service.getJSON(&jsonURL, &c)
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200325 return &c, err
326}
Han-Wen Nienhuysf92f01c2017-12-04 10:44:14 +0100327
328// Options for Describe.
329const (
330 // Return a ref that contains said commmit
331 DescribeContains = "contains"
332
333 // Return any type of ref
334 DescribeAll = "all"
335
336 // Only return a tag ref
337 DescribeTags = "tags"
338
339 // The default for 'contains': return annotated tags
340 DescribeAnnotatedTags = ""
341)
342
343// Describe describes a possibly shortened commit hash as a ref that
344// is visible to the caller. Currently, only the 'contains' flavor is
345// implemented, so options must always include 'contains'.
346func (s *RepoService) Describe(revision string, options ...string) (string, error) {
347 jsonURL := s.service.addr
348 jsonURL.Path = path.Join(jsonURL.Path, s.Name, "+describe", revision)
349 jsonURL.RawQuery = "format=JSON&" + strings.Join(options, "&")
350
351 result := map[string]string{}
352 err := s.service.getJSON(&jsonURL, &result)
353 if err != nil {
354 return "", err
355 }
356
357 if len(result) != 1 {
358 return "", fmt.Errorf("gitiles: got map %v, want just one entry", result)
359 }
360
361 for _, v := range result {
362 return v, nil
363 }
364
365 panic("unreachable.")
366}
Han-Wen Nienhuys85e1ea12017-12-04 10:58:26 +0100367
368// Refs returns the refs of a repository, optionally filtered by prefix.
369func (s *RepoService) Refs(prefix string) (map[string]*RefData, error) {
370
371 jsonURL := s.service.addr
372 jsonURL.Path = path.Join(jsonURL.Path, s.Name, "+refs")
373 if prefix != "" {
374 jsonURL.Path = path.Join(jsonURL.Path, prefix)
375 }
376 jsonURL.RawQuery = "format=JSON"
377
378 result := map[string]*RefData{}
379 err := s.service.getJSON(&jsonURL, &result)
380 if err != nil {
381 return nil, err
382 }
383
384 return result, err
385}