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  }