1// Copyright 2014 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package main 6 7import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "net/http" 14 "net/url" 15 "os/user" 16 "path/filepath" 17 "runtime" 18 "sort" 19 "strings" 20 "sync" 21) 22 23// auth holds cached data about authentication to Gerrit. 24var auth struct { 25 initialized bool 26 27 host string // "go.googlesource.com" 28 url string // "https://go-review.googlesource.com" 29 project string // "go", "tools", "crypto", etc 30 31 // Authentication information. 32 // Either cookie name + value from git cookie file 33 // or username and password from .netrc. 34 cookieName string 35 cookieValue string 36 user string 37 password string 38} 39 40// loadGerritOriginMutex is used to control access when initializing auth 41// in loadGerritOrigin, which can be called in parallel by "pending". 42// We use a mutex rather than a sync.Once because the tests clear auth. 43var loadGerritOriginMutex sync.Mutex 44 45// loadGerritOrigin loads the Gerrit host name from the origin remote. 46// This sets auth.{initialized,host,url,project}. 47// If the origin remote does not appear to be a Gerrit server 48// (is missing, is GitHub, is not https, has too many path elements), 49// loadGerritOrigin dies. 50func loadGerritOrigin() { 51 loadGerritOriginMutex.Lock() 52 defer loadGerritOriginMutex.Unlock() 53 54 if auth.initialized { 55 return 56 } 57 58 // Gerrit must be set, either explicitly via the code review config or 59 // implicitly as Git's origin remote. 60 origin := config()["gerrit"] 61 originUrl := trim(cmdOutput("git", "config", "remote.origin.url")) 62 63 err := loadGerritOriginInternal(origin, originUrl) 64 if err != nil { 65 dief("failed to load Gerrit origin: %v", err) 66 } 67 68 auth.initialized = true 69} 70 71// loadGerritOriginInternal does the work of loadGerritOrigin, just extracted out 72// for easier testing. 73func loadGerritOriginInternal(origin, remoteOrigin string) error { 74 originUrl, err := url.Parse(remoteOrigin) 75 if err != nil { 76 return fmt.Errorf("failed to parse git's remote.origin.url %q as a URL: %v", remoteOrigin, err) 77 } else { 78 originUrl.User = nil 79 remoteOrigin = originUrl.String() 80 } 81 hasGerritConfig := true 82 if origin == "" { 83 hasGerritConfig = false 84 origin = remoteOrigin 85 } 86 if strings.Contains(origin, "github.com") { 87 return fmt.Errorf("git origin must be a Gerrit host, not GitHub: %s", origin) 88 } 89 90 if googlesourceIndex := strings.Index(origin, ".googlesource.com"); googlesourceIndex >= 0 { 91 if !strings.HasPrefix(origin, "https://") { 92 return fmt.Errorf("git origin must be an https:// URL: %s", origin) 93 } 94 // https:// prefix and then one slash between host and top-level name 95 if strings.Count(origin, "/") != 3 { 96 return fmt.Errorf("git origin is malformed: %s", origin) 97 } 98 host := origin[len("https://"):strings.LastIndex(origin, "/")] 99 100 // In the case of Google's Gerrit, host is go.googlesource.com 101 // and apiURL uses go-review.googlesource.com, but the Gerrit 102 // setup instructions do not write down a cookie explicitly for 103 // go-review.googlesource.com, so we look for the non-review 104 // host name instead. 105 url := origin 106 i := googlesourceIndex 107 url = url[:i] + "-review" + url[i:] 108 109 i = strings.LastIndex(url, "/") 110 url, project := url[:i], url[i+1:] 111 112 auth.host = host 113 auth.url = url 114 auth.project = project 115 return nil 116 } 117 118 // Origin is not *.googlesource.com. 119 // 120 // If the Gerrit origin is set from the codereview.cfg file than we handle it 121 // differently to allow for sub-path hosted Gerrit. 122 auth.host = originUrl.Host 123 if hasGerritConfig { 124 if !strings.HasPrefix(remoteOrigin, origin) { 125 return fmt.Errorf("Gerrit origin %q from %q different than git origin url %q", origin, configPath, originUrl) 126 } 127 128 auth.project = strings.Trim(strings.TrimPrefix(remoteOrigin, origin), "/") 129 auth.url = origin 130 } else { 131 auth.project = strings.Trim(originUrl.Path, "/") 132 auth.url = strings.TrimSuffix(remoteOrigin, originUrl.Path) 133 } 134 135 return nil 136} 137 138// testHomeDir is empty for normal use. During tests it may be set and used 139// in place of the actual home directory. Tests may still need to 140// set the HOME var for sub-processes such as git. 141var testHomeDir = "" 142 143func netrcName() string { 144 // Git on Windows will look in $HOME\_netrc. 145 if runtime.GOOS == "windows" { 146 return "_netrc" 147 } 148 return ".netrc" 149} 150 151// loadAuth loads the authentication tokens for making API calls to 152// the Gerrit origin host. 153func loadAuth() { 154 if auth.user != "" || auth.cookieName != "" { 155 return 156 } 157 158 loadGerritOrigin() 159 160 // First look in Git's http.cookiefile, which is where Gerrit 161 // now tells users to store this information. 162 if cookieFile, _ := trimErr(cmdOutputErr("git", "config", "--path", "--get-urlmatch", "http.cookiefile", auth.url)); cookieFile != "" { 163 data, _ := ioutil.ReadFile(cookieFile) 164 maxMatch := -1 165 for _, line := range lines(string(data)) { 166 f := strings.Split(line, "\t") 167 if len(f) >= 7 && (f[0] == auth.host || strings.HasPrefix(f[0], ".") && strings.HasSuffix(auth.host, f[0])) { 168 if len(f[0]) > maxMatch { 169 auth.cookieName = f[5] 170 auth.cookieValue = f[6] 171 maxMatch = len(f[0]) 172 } 173 } 174 } 175 if maxMatch > 0 { 176 return 177 } 178 } 179 180 // If not there, then look in $HOME/.netrc, which is where Gerrit 181 // used to tell users to store the information, until the passwords 182 // got so long that old versions of curl couldn't handle them. 183 netrc := netrcName() 184 homeDir := testHomeDir 185 if homeDir == "" { 186 usr, err := user.Current() 187 if err != nil { 188 dief("failed to get current user home directory to look for %q: %v", netrc, err) 189 } 190 homeDir = usr.HomeDir 191 } 192 data, _ := ioutil.ReadFile(filepath.Join(homeDir, netrc)) 193 for _, line := range lines(string(data)) { 194 if i := strings.Index(line, "#"); i >= 0 { 195 line = line[:i] 196 } 197 f := strings.Fields(line) 198 if len(f) >= 6 && f[0] == "machine" && f[1] == auth.host && f[2] == "login" && f[4] == "password" { 199 auth.user = f[3] 200 auth.password = f[5] 201 return 202 } 203 } 204 205 dief("cannot find authentication info for %s", auth.host) 206} 207 208// gerritError is an HTTP error response served by Gerrit. 209type gerritError struct { 210 url string 211 statusCode int 212 status string 213 body string 214} 215 216func (e *gerritError) Error() string { 217 if e.statusCode == http.StatusNotFound { 218 return "change not found on Gerrit server" 219 } 220 221 extra := strings.TrimSpace(e.body) 222 if extra != "" { 223 extra = ": " + extra 224 } 225 return fmt.Sprintf("%s%s", e.status, extra) 226} 227 228// gerritAPI executes a GET or POST request to a Gerrit API endpoint. 229// It uses GET when requestBody is nil, otherwise POST. If target != nil, 230// gerritAPI expects to get a 200 response with a body consisting of an 231// anti-xss line (]})' or some such) followed by JSON. 232// If requestBody != nil, gerritAPI sets the Content-Type to application/json. 233func gerritAPI(path string, requestBody []byte, target interface{}) error { 234 // Strictly speaking, we might be able to use unauthenticated 235 // access, by removing the /a/ from the URL, but that assumes 236 // that all the information we care about is publicly visible. 237 // Using authentication makes it possible for this to work with 238 // non-public CLs or Gerrit hosts too. 239 loadAuth() 240 241 if !strings.HasPrefix(path, "/") { 242 dief("internal error: gerritAPI called with malformed path") 243 } 244 245 url := auth.url + path 246 method := "GET" 247 var reader io.Reader 248 if requestBody != nil { 249 method = "POST" 250 reader = bytes.NewReader(requestBody) 251 } 252 req, err := http.NewRequest(method, url, reader) 253 if err != nil { 254 return err 255 } 256 if requestBody != nil { 257 req.Header.Set("Content-Type", "application/json") 258 } 259 if auth.cookieName != "" { 260 req.AddCookie(&http.Cookie{ 261 Name: auth.cookieName, 262 Value: auth.cookieValue, 263 }) 264 } else { 265 req.SetBasicAuth(auth.user, auth.password) 266 } 267 268 resp, err := http.DefaultClient.Do(req) 269 if err != nil { 270 return err 271 } 272 body, err := ioutil.ReadAll(resp.Body) 273 resp.Body.Close() 274 275 if err != nil { 276 return fmt.Errorf("reading response body: %v", err) 277 } 278 if resp.StatusCode != http.StatusOK { 279 return &gerritError{url, resp.StatusCode, resp.Status, string(body)} 280 } 281 282 if target != nil { 283 i := bytes.IndexByte(body, '\n') 284 if i < 0 { 285 return fmt.Errorf("%s: malformed json response - bad header", url) 286 } 287 body = body[i:] 288 if err := json.Unmarshal(body, target); err != nil { 289 return fmt.Errorf("%s: malformed json response", url) 290 } 291 } 292 return nil 293} 294 295// fullChangeID returns the unambigous Gerrit change ID for the commit c on branch b. 296// The returned ID has the form project~originbranch~Ihexhexhexhexhex. 297// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id for details. 298func fullChangeID(b *Branch, c *Commit) string { 299 loadGerritOrigin() 300 return auth.project + "~" + strings.TrimPrefix(b.OriginBranch(), "origin/") + "~" + c.ChangeID 301} 302 303// readGerritChange reads the metadata about a change from the Gerrit server. 304// The changeID should use the syntax project~originbranch~Ihexhexhexhexhex returned 305// by fullChangeID. Using only Ihexhexhexhexhex will work provided it uniquely identifies 306// a single change on the server. 307// The changeID can have additional query parameters appended to it, as in "normalid?o=LABELS". 308// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id for details. 309func readGerritChange(changeID string) (*GerritChange, error) { 310 var c GerritChange 311 err := gerritAPI("/a/changes/"+changeID, nil, &c) 312 if err != nil { 313 return nil, err 314 } 315 return &c, nil 316} 317 318// readGerritChanges is like readGerritChange but expects changeID 319// to be a query parameter list like q=change:XXX&q=change:YYY&o=OPTIONS, 320// and it expects to receive a JSON array of GerritChanges, not just one. 321func readGerritChanges(query string) ([][]*GerritChange, error) { 322 // The Gerrit server imposes a limit of at most 10 q= parameters. 323 v, err := url.ParseQuery(query) 324 if err != nil { 325 return nil, err 326 } 327 var results []chan gerritChangeResult 328 for len(v["q"]) > 0 { 329 n := len(v["q"]) 330 if n > 10 { 331 n = 10 332 } 333 all := v["q"] 334 v["q"] = all[:n] 335 query := v.Encode() 336 v["q"] = all[n:] 337 ch := make(chan gerritChangeResult, 1) 338 go readGerritChangesBatch(query, n, ch) 339 results = append(results, ch) 340 } 341 342 var c [][]*GerritChange 343 for _, ch := range results { 344 res := <-ch 345 if res.err != nil { 346 return nil, res.err 347 } 348 c = append(c, res.c...) 349 } 350 return c, nil 351} 352 353type gerritChangeResult struct { 354 c [][]*GerritChange 355 err error 356} 357 358func readGerritChangesBatch(query string, n int, ch chan gerritChangeResult) { 359 var c [][]*GerritChange 360 // If there are multiple q=, the server sends back an array of arrays of results. 361 // If there is a single q=, it only sends back an array of results; in that case 362 // we need to do the wrapping ourselves. 363 var arg interface{} = &c 364 if n == 1 { 365 c = append(c, nil) 366 arg = &c[0] 367 } 368 err := gerritAPI("/a/changes/?"+query, nil, arg) 369 if len(c) != n && err == nil { 370 err = fmt.Errorf("gerrit result count mismatch") 371 } 372 ch <- gerritChangeResult{c, err} 373} 374 375// GerritChange is the JSON struct returned by a Gerrit CL query. 376type GerritChange struct { 377 ID string 378 Project string 379 Branch string 380 ChangeId string `json:"change_id"` 381 Subject string 382 Status string 383 Created string 384 Updated string 385 Insertions int 386 Deletions int 387 Number int `json:"_number"` 388 Owner *GerritAccount 389 Labels map[string]*GerritLabel 390 CurrentRevision string `json:"current_revision"` 391 Revisions map[string]*GerritRevision 392 Messages []*GerritMessage 393} 394 395// LabelNames returns the label names for the change, in lexicographic order. 396func (g *GerritChange) LabelNames() []string { 397 var names []string 398 for name := range g.Labels { 399 names = append(names, name) 400 } 401 sort.Strings(names) 402 return names 403} 404 405// GerritMessage is the JSON struct for a Gerrit MessageInfo. 406type GerritMessage struct { 407 Author struct { 408 Name string 409 } 410 Message string 411} 412 413// GerritLabel is the JSON struct for a Gerrit LabelInfo. 414type GerritLabel struct { 415 Optional bool 416 Blocking bool 417 Approved *GerritAccount 418 Rejected *GerritAccount 419 All []*GerritApproval 420} 421 422// GerritAccount is the JSON struct for a Gerrit AccountInfo. 423type GerritAccount struct { 424 ID int `json:"_account_id"` 425 Name string 426 Email string 427 Username string 428} 429 430// GerritApproval is the JSON struct for a Gerrit ApprovalInfo. 431type GerritApproval struct { 432 GerritAccount 433 Value int 434 Date string 435} 436 437// GerritRevision is the JSON struct for a Gerrit RevisionInfo. 438type GerritRevision struct { 439 Number int `json:"_number"` 440 Ref string 441 Fetch map[string]*GerritFetch 442} 443 444// GerritFetch is the JSON struct for a Gerrit FetchInfo 445type GerritFetch struct { 446 URL string 447 Ref string 448} 449