github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/review/git-codereview/api.go (about) 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 5 package main 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "net/http" 14 "os" 15 "sort" 16 "strings" 17 ) 18 19 // auth holds cached data about authentication to Gerrit. 20 var auth struct { 21 host string // "go.googlesource.com" 22 url string // "https://go-review.googlesource.com" 23 project string // "go", "tools", "crypto", etc 24 25 // Authentication information. 26 // Either cookie name + value from git cookie file 27 // or username and password from .netrc. 28 cookieName string 29 cookieValue string 30 user string 31 password string 32 } 33 34 // loadGerritOrigin loads the Gerrit host name from the origin remote. 35 // If the origin remote does not appear to be a Gerrit server 36 // (is missing, is GitHub, is not https, has too many path elements), 37 // loadGerritOrigin dies. 38 func loadGerritOrigin() { 39 if auth.host != "" { 40 return 41 } 42 43 // Gerrit must be set, either explicitly via the code review config or 44 // implicitly as Git's origin remote. 45 origin := config()["gerrit"] 46 if origin == "" { 47 origin = trim(cmdOutput("git", "config", "remote.origin.url")) 48 } 49 50 if strings.Contains(origin, "github.com") { 51 dief("git origin must be a Gerrit host, not GitHub: %s", origin) 52 } 53 54 if !strings.HasPrefix(origin, "https://") { 55 dief("git origin must be an https:// URL: %s", origin) 56 } 57 // https:// prefix and then one slash between host and top-level name 58 if strings.Count(origin, "/") != 3 { 59 dief("git origin is malformed: %s", origin) 60 } 61 host := origin[len("https://"):strings.LastIndex(origin, "/")] 62 63 // In the case of Google's Gerrit, host is go.googlesource.com 64 // and apiURL uses go-review.googlesource.com, but the Gerrit 65 // setup instructions do not write down a cookie explicitly for 66 // go-review.googlesource.com, so we look for the non-review 67 // host name instead. 68 url := origin 69 if i := strings.Index(url, ".googlesource.com"); i >= 0 { 70 url = url[:i] + "-review" + url[i:] 71 } 72 i := strings.LastIndex(url, "/") 73 url, project := url[:i], url[i+1:] 74 75 auth.host = host 76 auth.url = url 77 auth.project = project 78 } 79 80 // loadAuth loads the authentication tokens for making API calls to 81 // the Gerrit origin host. 82 func loadAuth() { 83 if auth.user != "" || auth.cookieName != "" { 84 return 85 } 86 87 loadGerritOrigin() 88 89 // First look in Git's http.cookiefile, which is where Gerrit 90 // now tells users to store this information. 91 if cookieFile, _ := trimErr(cmdOutputErr("git", "config", "http.cookiefile")); cookieFile != "" { 92 data, _ := ioutil.ReadFile(cookieFile) 93 maxMatch := -1 94 for _, line := range lines(string(data)) { 95 f := strings.Split(line, "\t") 96 if len(f) >= 7 && (f[0] == auth.host || strings.HasPrefix(f[0], ".") && strings.HasSuffix(auth.host, f[0])) { 97 if len(f[0]) > maxMatch { 98 auth.cookieName = f[5] 99 auth.cookieValue = f[6] 100 maxMatch = len(f[0]) 101 } 102 } 103 } 104 if maxMatch > 0 { 105 return 106 } 107 } 108 109 // If not there, then look in $HOME/.netrc, which is where Gerrit 110 // used to tell users to store the information, until the passwords 111 // got so long that old versions of curl couldn't handle them. 112 data, _ := ioutil.ReadFile(os.Getenv("HOME") + "/.netrc") 113 for _, line := range lines(string(data)) { 114 if i := strings.Index(line, "#"); i >= 0 { 115 line = line[:i] 116 } 117 f := strings.Fields(line) 118 if len(f) >= 6 && f[0] == "machine" && f[1] == auth.host && f[2] == "login" && f[4] == "password" { 119 auth.user = f[3] 120 auth.password = f[5] 121 return 122 } 123 } 124 125 dief("cannot find authentication info for %s", auth.host) 126 } 127 128 // gerritError is an HTTP error response served by Gerrit. 129 type gerritError struct { 130 url string 131 statusCode int 132 status string 133 body string 134 } 135 136 func (e *gerritError) Error() string { 137 if e.statusCode == http.StatusNotFound { 138 return "change not found on Gerrit server" 139 } 140 141 extra := strings.TrimSpace(e.body) 142 if extra != "" { 143 extra = ": " + extra 144 } 145 return fmt.Sprintf("%s%s", e.status, extra) 146 } 147 148 // gerritAPI executes a GET or POST request to a Gerrit API endpoint. 149 // It uses GET when requestBody is nil, otherwise POST. If target != nil, 150 // gerritAPI expects to get a 200 response with a body consisting of an 151 // anti-xss line (]})' or some such) followed by JSON. 152 // If requestBody != nil, gerritAPI sets the Content-Type to application/json. 153 func gerritAPI(path string, requestBody []byte, target interface{}) error { 154 // Strictly speaking, we might be able to use unauthenticated 155 // access, by removing the /a/ from the URL, but that assumes 156 // that all the information we care about is publicly visible. 157 // Using authentication makes it possible for this to work with 158 // non-public CLs or Gerrit hosts too. 159 loadAuth() 160 161 if !strings.HasPrefix(path, "/") { 162 dief("internal error: gerritAPI called with malformed path") 163 } 164 165 url := auth.url + path 166 method := "GET" 167 var reader io.Reader 168 if requestBody != nil { 169 method = "POST" 170 reader = bytes.NewReader(requestBody) 171 } 172 req, err := http.NewRequest(method, url, reader) 173 if err != nil { 174 return err 175 } 176 if requestBody != nil { 177 req.Header.Set("Content-Type", "application/json") 178 } 179 if auth.cookieName != "" { 180 req.AddCookie(&http.Cookie{ 181 Name: auth.cookieName, 182 Value: auth.cookieValue, 183 }) 184 } else { 185 req.SetBasicAuth(auth.user, auth.password) 186 } 187 188 resp, err := http.DefaultClient.Do(req) 189 if err != nil { 190 return err 191 } 192 body, err := ioutil.ReadAll(resp.Body) 193 resp.Body.Close() 194 195 if err != nil { 196 return fmt.Errorf("reading response body: %v", err) 197 } 198 if resp.StatusCode != http.StatusOK { 199 return &gerritError{url, resp.StatusCode, resp.Status, string(body)} 200 } 201 202 if target != nil { 203 i := bytes.IndexByte(body, '\n') 204 if i < 0 { 205 return fmt.Errorf("%s: malformed json response", url) 206 } 207 body = body[i:] 208 if err := json.Unmarshal(body, target); err != nil { 209 return fmt.Errorf("%s: malformed json response", url) 210 } 211 } 212 return nil 213 } 214 215 // fullChangeID returns the unambigous Gerrit change ID for the commit c on branch b. 216 // The retruned ID has the form project~originbranch~Ihexhexhexhexhex. 217 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id for details. 218 func fullChangeID(b *Branch, c *Commit) string { 219 loadGerritOrigin() 220 return auth.project + "~" + strings.TrimPrefix(b.OriginBranch(), "origin/") + "~" + c.ChangeID 221 } 222 223 // readGerritChange reads the metadata about a change from the Gerrit server. 224 // The changeID should use the syntax project~originbranch~Ihexhexhexhexhex returned 225 // by fullChangeID. Using only Ihexhexhexhexhex will work provided it uniquely identifies 226 // a single change on the server. 227 // The changeID can have additional query parameters appended to it, as in "normalid?o=LABELS". 228 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id for details. 229 func readGerritChange(changeID string) (*GerritChange, error) { 230 var c GerritChange 231 err := gerritAPI("/a/changes/"+changeID, nil, &c) 232 if err != nil { 233 return nil, err 234 } 235 return &c, nil 236 } 237 238 // GerritChange is the JSON struct returned by a Gerrit CL query. 239 type GerritChange struct { 240 ID string 241 Project string 242 Branch string 243 ChangeId string `json:"change_id"` 244 Subject string 245 Status string 246 Created string 247 Updated string 248 Mergeable bool 249 Insertions int 250 Deletions int 251 Number int `json:"_number"` 252 Owner *GerritAccount 253 Labels map[string]*GerritLabel 254 CurrentRevision string `json:"current_revision"` 255 Revisions map[string]*GerritRevision 256 Messages []*GerritMessage 257 } 258 259 // LabelNames returns the label names for the change, in lexicographic order. 260 func (g *GerritChange) LabelNames() []string { 261 var names []string 262 for name := range g.Labels { 263 names = append(names, name) 264 } 265 sort.Strings(names) 266 return names 267 } 268 269 // GerritMessage is the JSON struct for a Gerrit MessageInfo. 270 type GerritMessage struct { 271 Author struct { 272 Name string 273 } 274 Message string 275 } 276 277 // GerritLabel is the JSON struct for a Gerrit LabelInfo. 278 type GerritLabel struct { 279 Optional bool 280 Blocking bool 281 Approved *GerritAccount 282 Rejected *GerritAccount 283 All []*GerritApproval 284 } 285 286 // GerritAccount is the JSON struct for a Gerrit AccountInfo. 287 type GerritAccount struct { 288 ID int `json:"_account_id"` 289 Name string 290 Email string 291 Username string 292 } 293 294 // GerritApproval is the JSON struct for a Gerrit ApprovalInfo. 295 type GerritApproval struct { 296 GerritAccount 297 Value int 298 Date string 299 } 300 301 // GerritRevision is the JSON struct for a Gerrit RevisionInfo. 302 type GerritRevision struct { 303 Number int `json:"_number"` 304 Ref string 305 Fetch map[string]*GerritFetch 306 } 307 308 // GerritFetch is the JSON struct for a Gerrit FetchInfo 309 type GerritFetch struct { 310 URL string 311 Ref string 312 }