blob: beac2fac6966625ffa24080affffede7d70777d9 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package googlesource contains library functions for interacting with
// googlesource repository host.
package googlesource
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"fuchsia.googlesource.com/jiri"
)
// RepoStatus represents the status of a remote repository on googlesource.
type RepoStatus struct {
Name string `json:"name"`
CloneUrl string `json:"clone_url"`
Description string `json:"description"`
Branches map[string]string `json:"branches"`
}
// RepoStatuses is a map of repository name to RepoStatus.
type RepoStatuses map[string]RepoStatus
// parseCookie takes a single line from a cookie jar and parses it, returning
// an *http.Cookie.
func parseCookie(s string) (*http.Cookie, error) {
// Cookiejar files have 7 tab-delimited fields.
// See http://curl.haxx.se/mail/archive-2005-03/0099.html
// 0: domain
// 1: tailmatch
// 2: path
// 3: secure
// 4: expires
// 5: name
// 6: value
fields := strings.Fields(s)
if len(fields) != 7 {
return nil, fmt.Errorf("expected 7 fields but got %d: %q", len(fields), s)
}
expires, err := strconv.Atoi(fields[4])
if err != nil {
return nil, fmt.Errorf("invalid expiration: %q", fields[4])
}
cookie := &http.Cookie{
Domain: fields[0],
Path: fields[2],
Secure: fields[3] == "TRUE",
Expires: time.Unix(int64(expires), 0),
Name: fields[5],
Value: fields[6],
}
return cookie, nil
}
// gitCookies attempts to read and parse cookies from the .gitcookies file in
// the users home directory.
func gitCookies(jirix *jiri.X) []*http.Cookie {
cookies := []*http.Cookie{}
homeDir := os.Getenv("HOME")
if homeDir == "" {
return cookies
}
cookieFile := filepath.Join(homeDir, ".gitcookies")
bytes, err := jirix.NewSeq().ReadFile(cookieFile)
if err != nil {
return cookies
}
return parseCookieFile(jirix, bytes)
}
func parseCookieFile(jirix *jiri.X, bytes []byte) (cookies []*http.Cookie) {
lines := strings.Split(string(bytes), "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "" || line[0] == '#' {
continue
}
cookie, err := parseCookie(line)
if err != nil {
fmt.Fprintf(jirix.Stderr(), "error parsing cookie in .gitcookies: %v\n", err)
} else {
cookies = append(cookies, cookie)
}
}
return
}
// GetRepoStatuses returns the RepoStatus of all public projects hosted on the
// remote host. Host must be a googlesource host.
//
// NOTE(nlacasse): Googlesource uses gitiles as its git repo browser. gitiles
// has a completely undocumented feature that allows one to query the state of
// all repositories in a single request. See "doGetJson" method in
// https://gerrit.googlesource.com/gitiles/+/master/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java
//
// It's possible that gitiles will stop responding to this request at some
// future version, or that googlesource will move away from gitiles entirely.
// If that happens we can still get all the repo information in one request by
// using the /projects/ endpoint on Gerrit. See
// https://review.typo3.org/Documentation/rest-api-projects.html#list-projects
func GetRepoStatuses(jirix *jiri.X, host string, branches []string) (RepoStatuses, error) {
u, err := url.Parse(host)
if err != nil {
return nil, err
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, fmt.Errorf("remote host scheme is not http(s): %s", host)
}
u.Path = "/"
q := u.Query()
q.Set("format", "json")
for _, b := range branches {
q.Add("show-branch", b)
}
u.RawQuery = q.Encode()
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, fmt.Errorf("NewRequest(%q, %q, %v) failed: %v", "GET", u.String(), nil, err)
}
for _, c := range gitCookies(jirix) {
req.AddCookie(c)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("Do(%v) failed: %v", req, err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("got status code %v fetching %s: %s", resp.StatusCode, host, string(body))
}
// body has leading ")]}'" to prevent js hijacking. We must trim it.
trimmedBody := strings.TrimPrefix(string(body), ")]}'")
repoStatuses := make(RepoStatuses)
if err := json.Unmarshal([]byte(trimmedBody), &repoStatuses); err != nil {
return nil, fmt.Errorf("Unmarshal(%v) failed: %v", trimmedBody, err)
}
return repoStatuses, nil
}
var googleSourceRemoteRegExp = regexp.MustCompile(`(?i)https?://.*\.googlesource.com.*`)
// IsGoogleSourceRemote returns true if the host url is a googlesource remote.
func IsGoogleSourceRemote(host string) bool {
return googleSourceRemoteRegExp.MatchString(host)
}