blob: de60c5c8e605946ffa60591db97e69c58604a49d [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
18import (
19 "bytes"
20 "encoding/base64"
21 "encoding/json"
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020022 "flag"
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +020023 "fmt"
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +010024 "io"
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +020025 "io/ioutil"
26 "log"
27 "net/http"
28 "net/url"
29 "path"
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +010030 "strings"
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020031
Han-Wen Nienhuys0213eb62016-07-19 13:52:51 +020032 "github.com/google/slothfs/cookie"
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020033 "golang.org/x/net/context"
34 "golang.org/x/time/rate"
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +020035)
36
37// Service is a client for the Gitiles JSON interface.
38type Service struct {
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020039 limiter *rate.Limiter
40 addr url.URL
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +020041 client http.Client
42 agent string
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020043 jar http.CookieJar
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +010044 debug bool
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +020045}
46
47// Addr returns the address of the gitiles service.
48func (s *Service) Addr() string {
49 return s.addr.String()
50}
51
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020052// Options configures the the Gitiles service.
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020053type Options struct {
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020054 // A URL for the Gitiles service.
55 Address string
56
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020057 BurstQPS int
58 SustainedQPS float64
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +020059
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020060 // Path to a Netscape/Mozilla style cookie file.
61 CookieJar string
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +020062
63 // UserAgent defines how we present ourself to the server.
64 UserAgent string
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +010065
Billy Lynchecdd2552017-01-12 18:31:36 -050066 // HTTPClient allows callers to present their own http.Client instead of the default.
67 HTTPClient http.Client
68
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +010069 Debug bool
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020070}
71
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020072var defaultOptions Options
Han-Wen Nienhuys0213eb62016-07-19 13:52:51 +020073
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020074// DefineFlags sets up standard command line flags, and returns the
75// options struct in which the values are put.
76func DefineFlags() *Options {
Han-Wen Nienhuysfa039492016-08-10 12:42:29 +020077 flag.StringVar(&defaultOptions.Address, "gitiles_url", "https://android.googlesource.com", "Set the URL of the Gitiles service.")
78 flag.StringVar(&defaultOptions.CookieJar, "gitiles_cookies", "", "Set path to cURL-style cookie jar file.")
79 flag.StringVar(&defaultOptions.UserAgent, "gitiles_agent", "slothfs", "Set the User-Agent string to report to Gitiles.")
80 flag.Float64Var(&defaultOptions.SustainedQPS, "gitiles_qps", 4, "Set the maximum QPS to send to Gitiles.")
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +010081 flag.BoolVar(&defaultOptions.Debug, "gitiles_debug", false, "Print URLs as they are fetched.")
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020082 return &defaultOptions
Han-Wen Nienhuys0213eb62016-07-19 13:52:51 +020083}
84
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +020085// NewService returns a new Gitiles JSON client.
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +020086func NewService(opts Options) (*Service, error) {
87 var jar http.CookieJar
88 if nm := opts.CookieJar; nm != "" {
89 var err error
90 jar, err = cookie.NewJar(nm)
91 if err != nil {
92 return nil, err
93 }
94 if err := cookie.WatchJar(jar, nm); err != nil {
95 return nil, err
96 }
97 }
98
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +020099 if opts.SustainedQPS == 0.0 {
Han-Wen Nienhuys965dcb32016-08-01 14:09:41 +0200100 opts.SustainedQPS = 4
101 }
102 if opts.BurstQPS == 0 {
103 opts.BurstQPS = int(10.0 * opts.SustainedQPS)
104 } else if float64(opts.BurstQPS) < opts.SustainedQPS {
105 opts.BurstQPS = int(opts.SustainedQPS) + 1
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +0200106 }
107
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +0200108 url, err := url.Parse(opts.Address)
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200109 if err != nil {
110 return nil, err
111 }
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200112 s := &Service{
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +0200113 limiter: rate.NewLimiter(rate.Limit(opts.SustainedQPS), opts.BurstQPS),
114 addr: *url,
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200115 agent: opts.UserAgent,
Billy Lynchecdd2552017-01-12 18:31:36 -0500116 client: opts.HTTPClient,
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200117 }
118
Han-Wen Nienhuys21c97422016-07-22 15:23:18 +0200119 s.client.Jar = jar
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200120 s.client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
121 req.Header.Set("User-Agent", s.agent)
122 return nil
123 }
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +0100124 s.debug = opts.Debug
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200125 return s, nil
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200126}
127
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100128func (s *Service) stream(u *url.URL) (*http.Response, error) {
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +0200129 ctx := context.Background()
130
131 if err := s.limiter.Wait(ctx); err != nil {
132 return nil, err
133 }
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200134 req, err := http.NewRequest("GET", u.String(), nil)
135 if err != nil {
136 return nil, err
137 }
138 req.Header.Add("User-Agent", s.agent)
139 resp, err := s.client.Do(req)
Han-Wen Nienhuysabc8d152016-07-04 17:26:59 +0200140
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200141 if err != nil {
142 return nil, err
143 }
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100144
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200145 if resp.StatusCode != 200 {
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100146 resp.Body.Close()
Han-Wen Nienhuys39ace572016-06-02 18:30:51 +0200147 return nil, fmt.Errorf("%s: %s", u.String(), resp.Status)
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200148 }
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +0100149
150 if s.debug {
151 log.Printf("%s %s: %d", req.Method, req.URL, resp.StatusCode)
152 }
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200153 if got := resp.Request.URL.String(); got != u.String() {
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100154 resp.Body.Close()
Han-Wen Nienhuys3677ba92016-07-07 18:55:51 +0200155 // We accept redirects, but only for authentication.
156 // If we get a 200 from a different page than we
157 // requested, it's probably some sort of login page.
158 return nil, fmt.Errorf("got URL %s, want %s", got, u.String())
159 }
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200160
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100161 return resp, nil
162}
163
164func (s *Service) get(u *url.URL) ([]byte, error) {
165 resp, err := s.stream(u)
166 if err != nil {
167 return nil, err
168 }
169
170 defer resp.Body.Close()
171
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200172 c, err := ioutil.ReadAll(resp.Body)
173 if err != nil {
174 return nil, err
175 }
Han-Wen Nienhuysae6d11c2016-07-11 13:50:45 +0200176
Han-Wen Nienhuys163ec592016-07-11 14:34:17 +0200177 if resp.Header.Get("Content-Type") == "text/plain; charset=UTF-8" {
Han-Wen Nienhuysae6d11c2016-07-11 13:50:45 +0200178 out := make([]byte, base64.StdEncoding.DecodedLen(len(c)))
179 n, err := base64.StdEncoding.Decode(out, c)
180 return out[:n], err
181 }
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200182 return c, nil
183}
184
185var xssTag = []byte(")]}'\n")
186
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200187func (s *Service) getJSON(u *url.URL, dest interface{}) error {
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200188 c, err := s.get(u)
189 if err != nil {
190 return err
191 }
192
193 if !bytes.HasPrefix(c, xssTag) {
194 return fmt.Errorf("Gitiles JSON %s missing XSS tag: %q", u, c)
195 }
196 c = c[len(xssTag):]
197
198 err = json.Unmarshal(c, dest)
199 if err != nil {
200 err = fmt.Errorf("Unmarshal(%s): %v", u, err)
201 }
202 return err
203}
204
205// List retrieves the list of projects.
Han-Wen Nienhuys8b960812016-07-13 18:15:57 +0200206func (s *Service) List(branches []string) (map[string]*Project, error) {
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200207 listURL := s.addr
208 listURL.RawQuery = "format=JSON"
Han-Wen Nienhuys8b960812016-07-13 18:15:57 +0200209 for _, b := range branches {
210 listURL.RawQuery += "&b=" + b
211 }
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200212
213 projects := map[string]*Project{}
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200214 err := s.getJSON(&listURL, &projects)
Han-Wen Nienhuysd42875c2016-11-30 19:07:27 +0100215 for k, v := range projects {
216 if k != v.Name {
217 return nil, fmt.Errorf("gitiles: key %q had project name %q", k, v.Name)
218 }
219 }
220
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200221 return projects, err
222}
223
Han-Wen Nienhuysa501c042016-07-21 16:28:49 +0200224// NewRepoService creates a service for a specific repository on a Gitiles server.
Han-Wen Nienhuys39ace572016-06-02 18:30:51 +0200225func (s *Service) NewRepoService(name string) *RepoService {
226 return &RepoService{
227 Name: name,
228 service: s,
229 }
230}
231
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200232// RepoService is a JSON client for the functionality of a specific
233// respository.
234type RepoService struct {
235 Name string
236 service *Service
237}
238
Han-Wen Nienhuys27dde402016-06-03 17:44:33 +0200239// Get retrieves a single project.
240func (s *RepoService) Get() (*Project, error) {
241 jsonURL := s.service.addr
242 jsonURL.Path = path.Join(jsonURL.Path, s.Name)
243 jsonURL.RawQuery = "format=JSON"
244
245 var p Project
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200246 err := s.service.getJSON(&jsonURL, &p)
Han-Wen Nienhuys27dde402016-06-03 17:44:33 +0200247 return &p, err
248}
249
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200250// GetBlob fetches a blob.
251func (s *RepoService) GetBlob(branch, filename string) ([]byte, error) {
252 blobURL := s.service.addr
253
254 blobURL.Path = path.Join(blobURL.Path, s.Name, "+show", branch, filename)
255 blobURL.RawQuery = "format=TEXT"
256
257 // TODO(hanwen): invent a more structured mechanism for logging.
258 log.Println(blobURL.String())
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200259 return s.service.get(&blobURL)
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200260}
261
Han-Wen Nienhuysf4bca232017-12-04 10:11:59 +0100262// Archive formats for +archive. JGit also supports some shorthands.
263const (
264 ArchiveTbz = "tar.bz2"
265 ArchiveTgz = "tar.gz"
266 ArchiveTar = "tar"
267 ArchiveTxz = "tar.xz"
268
269 // the Gitiles source code claims .tar.xz and .tar are
270 // supported, but googlesource.com doesn't support it,
271 // apparently. In addition, JGit provides ZipFormat, but
272 // gitiles doesn't support it.
273)
274
275// GetArchive downloads an archive of the project. Format is one
276// ArchivXxx formats. dirPrefix, if given, restricts to the given
277// subpath, and strips the path prefix from the files in the resulting
278// tar archive. revision is a git revision, either a branch/tag name
279// ("master") or a hex commit SHA1.
280func (s *RepoService) GetArchive(revision, dirPrefix, format string) (io.ReadCloser, error) {
281 u := s.service.addr
282 u.Path = path.Join(u.Path, s.Name, "+archive", revision)
283 if dirPrefix != "" {
284 u.Path = path.Join(u.Path, dirPrefix)
285 }
286 u.Path += "." + format
287 resp, err := s.service.stream(&u)
288 if err != nil {
289 return nil, err
290 }
291 return resp.Body, err
292}
293
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200294// GetTree fetches a tree. The dir argument may not point to a
295// blob. If recursive is given, the server recursively expands the
296// tree.
297func (s *RepoService) GetTree(branch, dir string, recursive bool) (*Tree, error) {
298 jsonURL := s.service.addr
299 jsonURL.Path = path.Join(jsonURL.Path, s.Name, "+", branch, dir)
Han-Wen Nienhuys1e2b8af2016-11-23 13:58:25 +0100300 if !strings.HasSuffix(jsonURL.Path, "/") {
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200301 jsonURL.Path += "/"
302 }
303 jsonURL.RawQuery = "format=JSON&long=1"
304
305 if recursive {
306 jsonURL.RawQuery += "&recursive=1"
307 }
308
309 var tree Tree
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200310 err := s.service.getJSON(&jsonURL, &tree)
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200311 return &tree, err
312}
313
314// GetCommit gets the data of a commit in a branch.
315func (s *RepoService) GetCommit(branch string) (*Commit, error) {
316 jsonURL := s.service.addr
317 jsonURL.Path = path.Join(jsonURL.Path, s.Name, "+", branch)
318 jsonURL.RawQuery = "format=JSON"
319
320 var c Commit
Han-Wen Nienhuysf065f142016-07-11 14:03:57 +0200321 err := s.service.getJSON(&jsonURL, &c)
Han-Wen Nienhuys4bf7fcc2016-06-02 15:03:59 +0200322 return &c, err
323}