v.io/jiri@v0.0.0-20160715023856-abfb8b131290/googlesource/googlesource.go (about) 1 // Copyright 2015 The Vanadium 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 googlesource contains library functions for interacting with 6 // googlesource repository host. 7 8 package googlesource 9 10 import ( 11 "encoding/json" 12 "fmt" 13 "io/ioutil" 14 "net/http" 15 "net/url" 16 "os" 17 "path/filepath" 18 "regexp" 19 "strconv" 20 "strings" 21 "time" 22 23 "v.io/jiri" 24 ) 25 26 // RepoStatus represents the status of a remote repository on googlesource. 27 type RepoStatus struct { 28 Name string `json:"name"` 29 CloneUrl string `json:"clone_url"` 30 Description string `json:"description"` 31 Branches map[string]string `json:"branches"` 32 } 33 34 // RepoStatuses is a map of repository name to RepoStatus. 35 type RepoStatuses map[string]RepoStatus 36 37 // parseCookie takes a single line from a cookie jar and parses it, returning 38 // an *http.Cookie. 39 func parseCookie(s string) (*http.Cookie, error) { 40 // Cookiejar files have 7 tab-delimited fields. 41 // See http://curl.haxx.se/mail/archive-2005-03/0099.html 42 // 0: domain 43 // 1: tailmatch 44 // 2: path 45 // 3: secure 46 // 4: expires 47 // 5: name 48 // 6: value 49 50 fields := strings.Fields(s) 51 if len(fields) != 7 { 52 return nil, fmt.Errorf("expected 7 fields but got %d: %q", len(fields), s) 53 } 54 expires, err := strconv.Atoi(fields[4]) 55 if err != nil { 56 return nil, fmt.Errorf("invalid expiration: %q", fields[4]) 57 } 58 59 cookie := &http.Cookie{ 60 Domain: fields[0], 61 Path: fields[2], 62 Secure: fields[3] == "TRUE", 63 Expires: time.Unix(int64(expires), 0), 64 Name: fields[5], 65 Value: fields[6], 66 } 67 return cookie, nil 68 } 69 70 // gitCookies attempts to read and parse cookies from the .gitcookies file in 71 // the users home directory. 72 func gitCookies(jirix *jiri.X) []*http.Cookie { 73 cookies := []*http.Cookie{} 74 75 homeDir := os.Getenv("HOME") 76 if homeDir == "" { 77 return cookies 78 } 79 80 cookieFile := filepath.Join(homeDir, ".gitcookies") 81 bytes, err := jirix.NewSeq().ReadFile(cookieFile) 82 if err != nil { 83 return cookies 84 } 85 return parseCookieFile(jirix, bytes) 86 } 87 88 func parseCookieFile(jirix *jiri.X, bytes []byte) (cookies []*http.Cookie) { 89 lines := strings.Split(string(bytes), "\n") 90 91 for _, line := range lines { 92 if strings.TrimSpace(line) == "" || line[0] == '#' { 93 continue 94 } 95 cookie, err := parseCookie(line) 96 if err != nil { 97 fmt.Fprintf(jirix.Stderr(), "error parsing cookie in .gitcookies: %v\n", err) 98 } else { 99 cookies = append(cookies, cookie) 100 } 101 } 102 return 103 } 104 105 // GetRepoStatuses returns the RepoStatus of all public projects hosted on the 106 // remote host. Host must be a googlesource host. 107 // 108 // NOTE(nlacasse): Googlesource uses gitiles as its git repo browser. gitiles 109 // has a completely undocumented feature that allows one to query the state of 110 // all repositories in a single request. See "doGetJson" method in 111 // https://gerrit.googlesource.com/gitiles/+/master/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java 112 // 113 // It's possible that gitiles will stop responding to this request at some 114 // future version, or that googlesource will move away from gitiles entirely. 115 // If that happens we can still get all the repo information in one request by 116 // using the /projects/ endpoint on Gerrit. See 117 // https://review.typo3.org/Documentation/rest-api-projects.html#list-projects 118 func GetRepoStatuses(jirix *jiri.X, host string, branches []string) (RepoStatuses, error) { 119 u, err := url.Parse(host) 120 if err != nil { 121 return nil, err 122 } 123 if u.Scheme != "http" && u.Scheme != "https" { 124 return nil, fmt.Errorf("remote host scheme is not http(s): %s", host) 125 } 126 127 u.Path = "/" 128 q := u.Query() 129 q.Set("format", "json") 130 for _, b := range branches { 131 q.Add("show-branch", b) 132 } 133 u.RawQuery = q.Encode() 134 135 req, err := http.NewRequest("GET", u.String(), nil) 136 if err != nil { 137 return nil, fmt.Errorf("NewRequest(%q, %q, %v) failed: %v", "GET", u.String(), nil, err) 138 } 139 for _, c := range gitCookies(jirix) { 140 req.AddCookie(c) 141 } 142 resp, err := http.DefaultClient.Do(req) 143 if err != nil { 144 return nil, fmt.Errorf("Do(%v) failed: %v", req, err) 145 } 146 defer resp.Body.Close() 147 body, err := ioutil.ReadAll(resp.Body) 148 if resp.StatusCode != http.StatusOK { 149 return nil, fmt.Errorf("got status code %v fetching %s: %s", resp.StatusCode, host, string(body)) 150 } 151 152 // body has leading ")]}'" to prevent js hijacking. We must trim it. 153 trimmedBody := strings.TrimPrefix(string(body), ")]}'") 154 155 repoStatuses := make(RepoStatuses) 156 if err := json.Unmarshal([]byte(trimmedBody), &repoStatuses); err != nil { 157 return nil, fmt.Errorf("Unmarshal(%v) failed: %v", trimmedBody, err) 158 } 159 return repoStatuses, nil 160 } 161 162 var googleSourceRemoteRegExp = regexp.MustCompile(`(?i)https?://.*\.googlesource.com.*`) 163 164 // IsGoogleSourceRemote returns true if the host url is a googlesource remote. 165 func IsGoogleSourceRemote(host string) bool { 166 return googleSourceRemoteRegExp.MatchString(host) 167 }