blob: 95d37d4dc50311a2c38688424c6d550cf9432ab7 [file] [log] [blame]
// Copyright 2017 The Fuchsia 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 project
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"fuchsia.googlesource.com/jiri"
"fuchsia.googlesource.com/jiri/gerrit"
"fuchsia.googlesource.com/jiri/gitutil"
)
const (
SourceManifestVersion = int32(0)
)
// This was created using proto file: https://github.com/luci/recipes-py/blob/master/recipe_engine/source_manifest.proto.
type SourceManifest_GitCheckout struct {
// The canonicalized URL of the original repo that is considered the “source
// of truth” for the source code. Ex.
// https://chromium.googlesource.com/chromium/tools/build.git
// https://github.com/luci/recipes-py
RepoUrl string `json:"repo_url,omitempty"`
// If different from repo_url, this can be the URL of the repo that the source
// was actually fetched from (i.e. a mirror). Ex.
// https://chromium.googlesource.com/external/github.com/luci/recipes-py
//
// If this is empty, it's presumed to be equal to repo_url.
FetchUrl string `json:"fetch_url,omitempty"`
// The fully resolved revision (commit hash) of the source. Ex.
// 3617b0eea7ec74b8e731a23fed2f4070cbc284c4
//
// Note that this is the raw revision bytes, not their hex-encoded form.
Revision string `json:"revision,omitempty"`
// The ref that the task used to resolve/fetch the revision of the source
// (if any). Ex.
// refs/heads/master
// refs/changes/04/511804/4
//
// This should always be a ref on the hosted repo (not any local alias
// like 'refs/remotes/...').
//
// This should always be an absolute ref (i.e. starts with 'refs/'). An
// example of a non-absolute ref would be 'master'.
FetchRef string `json:"fetch_ref,omitempty"`
}
type SourceManifest_Directory struct {
GitCheckout *SourceManifest_GitCheckout `json:"git_checkout,omitempty"`
}
type SourceManifest struct {
// Version will increment on backwards-incompatible changes only. Backwards
// compatible changes will not alter this version number.
//
// Currently, the only valid version number is 0.
Version int32 `json:"version"`
// Map of local file system directory path (with forward slashes) to
// a Directory message containing one or more deployments.
//
// The local path is relative to some job-specific root. This should be used
// for informational/display/organization purposes, and should not be used as
// a global primary key. i.e. if you depend on chromium/src.git being in
// a folder called “src”, I will find you and make really angry faces at you
// until you change it...(╬ಠ益ಠ). Instead, implementations should consider
// indexing by e.g. git repository URL or cipd package name as more better
// primary keys.
Directories map[string]*SourceManifest_Directory `json:"directories"`
}
func getCLRefByCommit(jirix *jiri.X, gerritHost, revision string) (string, error) {
hostUrl, err := url.Parse(gerritHost)
if err != nil {
return "", fmt.Errorf("invalid gerrit host %q: %s", gerritHost, err)
}
g := gerrit.New(jirix, hostUrl)
cls, err := g.ListChangesByCommit(revision)
if err != nil {
return "", fmt.Errorf("not able to get CL for revision %s: %s", revision, err)
}
for _, c := range cls {
if v, ok := c.Revisions[revision]; ok {
return v.Fetch.Ref, nil
}
}
return "", nil
}
func NewSourceManifest(jirix *jiri.X, projects Projects) (*SourceManifest, MultiError) {
jirix.TimerPush("create source manifest")
defer jirix.TimerPop()
workQueue := make(chan Project, len(projects))
for _, proj := range projects {
if err := proj.relativizePaths(jirix.Root); err != nil {
return nil, MultiError{err}
}
workQueue <- proj
}
close(workQueue)
errs := make(chan error, len(projects))
sm := &SourceManifest{
Version: SourceManifestVersion,
Directories: make(map[string]*SourceManifest_Directory),
}
var mux sync.Mutex
processProject := func(proj Project) error {
gc := &SourceManifest_GitCheckout{
RepoUrl: rewriteRemote(jirix, proj.Remote),
}
scm := gitutil.New(jirix, gitutil.RootDirOpt(filepath.Join(jirix.Root, proj.Path)))
if rev, err := scm.CurrentRevision(); err != nil {
return err
} else {
gc.Revision = rev
}
if proj.RemoteBranch == "" {
proj.RemoteBranch = "master"
}
branchMap, err := scm.ListRemoteBranchesContainingRef(gc.Revision)
if err != nil {
return err
}
if branchMap["origin/"+proj.RemoteBranch] {
gc.FetchRef = "refs/heads/" + proj.RemoteBranch
} else {
for b, _ := range branchMap {
if strings.HasPrefix(b, "origin/HEAD ") {
continue
}
if strings.HasPrefix(b, "origin") {
gc.FetchRef = "refs/heads/" + strings.TrimLeft(b, "origin/")
break
}
}
// Try getting from gerrit
if gc.FetchRef == "" && proj.GerritHost != "" {
if ref, err := getCLRefByCommit(jirix, proj.GerritHost, gc.Revision); err != nil {
// Don't fail
jirix.Logger.Debugf("Error while fetching from gerrit for project %q: %s", proj.Name, err)
} else if ref == "" {
jirix.Logger.Debugf("Cannot get ref for project: %q, revision: %q", proj.Name, gc.Revision)
} else {
gc.FetchRef = ref
}
}
}
mux.Lock()
sm.Directories[proj.Path] = &SourceManifest_Directory{GitCheckout: gc}
mux.Unlock()
return nil
}
var wg sync.WaitGroup
for i := uint(0); i < jirix.Jobs; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for p := range workQueue {
if err := processProject(p); err != nil {
errs <- err
}
}
}()
}
wg.Wait()
close(errs)
var multiErr MultiError
for err := range errs {
multiErr = append(multiErr, err)
}
return sm, multiErr
}
func (sm *SourceManifest) ToFile(jirix *jiri.X, filename string) error {
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
return fmtError(err)
}
out, err := json.MarshalIndent(sm, "", " ")
if err != nil {
return fmt.Errorf("failed to serialize JSON output: %s\n", err)
}
err = ioutil.WriteFile(filename, out, 0600)
if err != nil {
return fmt.Errorf("failed write JSON output to %s: %s\n", filename, err)
}
return nil
}