package util
import (
"bytes"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
)
var UpstreamSummaryPattern = regexp.MustCompile(`UPSTREAM: (revert: [a-f0-9]{7,}: )?(([\w\.-]+\/[\w-\.-]+)?: )?(\d+:|<carry>:|<drop>:)`)
// supportedHosts maps source hosts to the number of path segments that
// represent the account/repo for that host. This is necessary because we
// can't tell just by looking at an import path whether the repo is identified
// by the first 2 or 3 path segments.
//
// If dependencies are introduced from new hosts, they'll need to be added
// here.
var SupportedHosts = map[string]int{
"bitbucket.org": 3,
"cloud.google.com": 2,
"code.google.com": 3,
"github.com": 3,
"golang.org": 3,
"google.golang.org": 2,
"gopkg.in": 2,
"k8s.io": 2,
"speter.net": 2,
}
type Commit struct {
Sha string
Summary string
Files []File
}
func (c Commit) DeclaresUpstreamChange() bool {
return strings.HasPrefix(strings.ToLower(c.Summary), "upstream")
}
func (c Commit) MatchesUpstreamSummaryPattern() bool {
return UpstreamSummaryPattern.MatchString(c.Summary)
}
func (c Commit) DeclaredUpstreamRepo() (string, error) {
if !c.DeclaresUpstreamChange() {
return "", fmt.Errorf("commit declares no upstream changes")
}
if !c.MatchesUpstreamSummaryPattern() {
return "", fmt.Errorf("commit doesn't match the upstream commit summary pattern")
}
groups := UpstreamSummaryPattern.FindStringSubmatch(c.Summary)
repo := groups[3]
if len(repo) == 0 {
repo = "k8s.io/kubernetes"
}
return repo, nil
}
// HasVendoredCodeChanges verifies if the commit has any changes to Godeps/_workspace/
// or vendor/ directories.
func (c Commit) HasVendoredCodeChanges() bool {
for _, file := range c.Files {
if file.HasVendoredCodeChanges() {
return true
}
}
return false
}
// HasGodepsChanges verifies if the commit has any changes to Godeps/Godeps.json file.
func (c Commit) HasGodepsChanges() bool {
for _, file := range c.Files {
if file.HasGodepsChanges() {
return true
}
}
return false
}
// HasNonVendoredCodeChanges verifies if the commit didn't modify Godeps/_workspace/
// or vendor directories.
func (c Commit) HasNonVendoredCodeChanges() bool {
for _, file := range c.Files {
if !file.HasVendoredCodeChanges() {
return true
}
}
return false
}
func (c Commit) GodepsReposChanged() ([]string, error) {
repos := map[string]struct{}{}
for _, file := range c.Files {
if !file.HasVendoredCodeChanges() {
continue
}
repo, err := file.GodepsRepoChanged()
if err != nil {
return nil, fmt.Errorf("problem with file %q in commit %s: %s", file, c.Sha, err)
}
repos[repo] = struct{}{}
}
changed := []string{}
for repo := range repos {
changed = append(changed, repo)
}
return changed, nil
}
type File string
// HasVendoredCodeChanges verifies if the modified file is from Godeps/_workspace/
// or vendor/ directories.
func (f File) HasVendoredCodeChanges() bool {
return strings.HasPrefix(string(f), "Godeps/_workspace") || strings.HasPrefix(string(f), "vendor")
}
// HasGodepsChanges verifies if the modified file is Godeps/Godeps.json.
func (f File) HasGodepsChanges() bool {
return f == "Godeps/Godeps.json"
}
func (f File) GodepsRepoChanged() (string, error) {
if !f.HasVendoredCodeChanges() {
return "", fmt.Errorf("file doesn't appear to be a Godeps or vendor change")
}
// Find the _workspace or vendor path segment index.
workspaceIdx, vendorIdx := -1, -1
parts := strings.Split(string(f), string(os.PathSeparator))
for i, part := range parts {
if part == "_workspace" {
workspaceIdx = i
break
}
if part == "vendor" {
vendorIdx = i
break
}
}
var nextIdx int
switch {
case workspaceIdx != -1:
// Godeps path struture assumption: Godeps/_workspace/src/...
if len(parts) < (workspaceIdx + 3) {
return "", fmt.Errorf("file doesn't appear to be a Godeps workspace path")
}
nextIdx = workspaceIdx + 2
case vendorIdx != -1:
// Godeps path struture assumption: vendor/...
if len(parts) < (vendorIdx + 1) {
return "", fmt.Errorf("file doesn't appear to be a vendor path")
}
nextIdx = vendorIdx + 1
default:
return "", fmt.Errorf("file doesn't appear to be a Godeps workspace path or vendor path")
}
// Deal with repos which could be identified by either 2 or 3 path segments.
host := parts[nextIdx]
segments := -1
for supportedHost, count := range SupportedHosts {
if host == supportedHost {
segments = count
break
}
}
if segments == -1 {
return "", fmt.Errorf("file modifies an unsupported repo host %q", host)
}
switch segments {
case 2:
return fmt.Sprintf("%s/%s", host, parts[nextIdx+1]), nil
case 3:
return fmt.Sprintf("%s/%s/%s", host, parts[nextIdx+1], parts[nextIdx+2]), nil
}
return "", fmt.Errorf("file modifies an unsupported repo host %q", host)
}
func IsCommit(a string) bool {
if _, _, err := run("git", "rev-parse", a); err != nil {
return false
}
return true
}
var ErrNotCommit = fmt.Errorf("one or both of the provided commits was not a valid commit")
func CommitsBetween(a, b string) ([]Commit, error) {
commits := []Commit{}
stdout, stderr, err := run("git", "log", "--oneline", fmt.Sprintf("%s..%s", a, b))
if err != nil {
if !IsCommit(a) || !IsCommit(b) {
return nil, ErrNotCommit
}
return nil, fmt.Errorf("error executing git log: %s: %s", stderr, err)
}
for _, log := range strings.Split(stdout, "\n") {
if len(log) == 0 {
continue
}
commit, err := NewCommitFromOnelineLog(log)
if err != nil {
return nil, err
}
commits = append(commits, commit)
}
return commits, nil
}
func NewCommitFromOnelineLog(log string) (Commit, error) {
var commit Commit
parts := strings.Split(log, " ")
if len(parts) < 2 {
return commit, fmt.Errorf("invalid log entry: %s", log)
}
commit.Sha = parts[0]
commit.Summary = strings.Join(parts[1:], " ")
files, err := filesInCommit(commit.Sha)
if err != nil {
return commit, err
}
commit.Files = files
return commit, nil
}
func IsAncestor(commit1, commit2, repoDir string) (bool, error) {
cwd, err := os.Getwd()
if err != nil {
return false, err
}
defer os.Chdir(cwd)
if err := os.Chdir(repoDir); err != nil {
return false, err
}
if stdout, stderr, err := run("git", "fetch", "origin"); err != nil {
return false, fmt.Errorf("out=%s, err=%s, %s", strings.TrimSpace(stdout), strings.TrimSpace(stderr), err)
}
if stdout, stderr, err := run("git", "merge-base", "--is-ancestor", commit1, commit2); err != nil {
return false, fmt.Errorf("out=%s, err=%s, %s", strings.TrimSpace(stdout), strings.TrimSpace(stderr), err)
}
return true, nil
}
func CommitDate(commit, repoDir string) (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", err
}
defer os.Chdir(cwd)
if err := os.Chdir(repoDir); err != nil {
return "", err
}
if stdout, stderr, err := run("git", "fetch", "origin"); err != nil {
return "", fmt.Errorf("out=%s, err=%s, %s", strings.TrimSpace(stdout), strings.TrimSpace(stderr), err)
}
if stdout, stderr, err := run("git", "show", "-s", "--format=%ci", commit); err != nil {
return "", fmt.Errorf("out=%s, err=%s, %s", strings.TrimSpace(stdout), strings.TrimSpace(stderr), err)
} else {
return strings.TrimSpace(stdout), nil
}
}
func Checkout(commit, repoDir string) error {
cwd, err := os.Getwd()
if err != nil {
return err
}
defer os.Chdir(cwd)
if err := os.Chdir(repoDir); err != nil {
return err
}
if stdout, stderr, err := run("git", "checkout", commit); err != nil {
return fmt.Errorf("out=%s, err=%s, %s", strings.TrimSpace(stdout), strings.TrimSpace(stderr), err)
}
return nil
}
func CurrentRev(repoDir string) (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", err
}
defer os.Chdir(cwd)
if err := os.Chdir(repoDir); err != nil {
return "", err
}
if stdout, stderr, err := run("git", "rev-parse", "HEAD"); err != nil {
return "", fmt.Errorf("out=%s, err=%s, %s", strings.TrimSpace(stdout), strings.TrimSpace(stderr), err)
} else {
return strings.TrimSpace(stdout), nil
}
}
func filesInCommit(sha string) ([]File, error) {
files := []File{}
stdout, stderr, err := run("git", "diff-tree", "--no-commit-id", "--name-only", "-r", sha)
if err != nil {
return nil, fmt.Errorf("%s: %s", stderr, err)
}
for _, filename := range strings.Split(stdout, "\n") {
if len(filename) == 0 {
continue
}
files = append(files, File(filename))
}
return files, nil
}
func run(args ...string) (string, string, error) {
cmd := exec.Command(args[0], args[1:]...)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
return stdout.String(), stderr.String(), err
}