blob: fc40d27e686e510943b9c8307d245ac19196a2ed [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 project
import (
"bytes"
"encoding/xml"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"v.io/jiri/collect"
"v.io/jiri/gitutil"
"v.io/jiri/googlesource"
"v.io/jiri/jiri"
"v.io/jiri/runutil"
"v.io/jiri/tool"
"v.io/x/lib/cmdline"
"v.io/x/lib/set"
)
var JiriProject = "release.go.jiri"
var JiriName = "jiri"
var JiriPackage = "v.io/jiri"
// CL represents a changelist.
type CL struct {
// Author identifies the author of the changelist.
Author string
// Email identifies the author's email.
Email string
// Description holds the description of the changelist.
Description string
}
// Manifest represents a setting used for updating the universe.
type Manifest struct {
Hooks []Hook `xml:"hooks>hook"`
Hosts []Host `xml:"hosts>host"`
Imports []Import `xml:"imports>import"`
Label string `xml:"label,attr"`
Projects []Project `xml:"projects>project"`
Tools []Tool `xml:"tools>tool"`
XMLName struct{} `xml:"manifest"`
}
// Hooks maps hook names to their detailed description.
type Hooks map[string]Hook
// Hook represents a post-update project hook.
type Hook struct {
// Exclude is flag used to exclude previously included hooks.
Exclude bool `xml:"exclude,attr"`
// Name is the hook name.
Name string `xml:"name,attr"`
// Project is the name of the project the hook is associated with.
Project string `xml:"project,attr"`
// Path is the path of the hook relative to its project's root.
Path string `xml:"path,attr"`
// Interpreter is an optional program used to interpret the hook (i.e. python). Unlike Path,
// Interpreter is relative to the environment's PATH and not the project's root.
Interpreter string `xml:"interpreter,attr"`
// Arguments for the hook.
Args []HookArg `xml:"arg"`
}
type HookArg struct {
Arg string `xml:",chardata"`
}
// Hosts map host name to their detailed description.
type Hosts map[string]Host
// Host represents the locations of git and gerrit repository hosts.
type Host struct {
// Name is the host name.
Name string `xml:"name,attr"`
// Location is the url of the host.
Location string `xml:"location,attr"`
// Git hooks to apply to repos from this host.
GitHooks []GitHook `xml:"githooks>githook"`
}
// GitHook represents the name and source of git hooks.
type GitHook struct {
// The hook name, as required by git (e.g. commit-msg, pre-rebase, etc.)
Name string `xml:"name,attr"`
// The filename of the hook implementation. When editing the manifest,
// specify this path as relative to the manifest dir. In loadManifest,
// this gets resolved to the absolute path.
Path string `xml:"path,attr"`
}
// Imports maps manifest import names to their detailed description.
type Imports map[string]Import
// Import represnts a manifest import.
type Import struct {
// Name is the name under which the manifest can be found the
// manifest repository.
Name string `xml:"name,attr"`
}
// Projects maps project names to their detailed description.
type Projects map[string]Project
// Project represents a jiri project.
type Project struct {
// Exclude is flag used to exclude previously included projects.
Exclude bool `xml:"exclude,attr"`
// Name is the project name.
Name string `xml:"name,attr"`
// Path is the path used to store the project locally. Project
// manifest uses paths that are relative to the $JIRI_ROOT
// environment variable. When a manifest is parsed (e.g. in
// RemoteProjects), the program logic converts the relative
// paths to an absolute paths, using the current value of the
// $JIRI_ROOT environment variable as a prefix.
Path string `xml:"path,attr"`
// Protocol is the version control protocol used by the
// project. If not set, "git" is used as the default.
Protocol string `xml:"protocol,attr"`
// Remote is the project remote.
Remote string `xml:"remote,attr"`
// RemoteBranch is the name of the remote branch to track. It doesn't affect
// the name of the local branch that jiri maintains, which is always "master".
RemoteBranch string `xml:"remotebranch,attr"`
// Revision is the revision the project should be advanced to
// during "jiri update". If not set, "HEAD" is used as the
// default.
Revision string `xml:"revision,attr"`
}
// Tools maps jiri tool names, to their detailed description.
type Tools map[string]Tool
// Tool represents a jiri tool.
type Tool struct {
// Exclude is flag used to exclude previously included projects.
Exclude bool `xml:"exclude,attr"`
// Data is a relative path to a directory for storing tool data
// (e.g. tool configuration files). The purpose of this field is to
// decouple the configuration of the data directory from the tool
// itself so that the location of the data directory can change
// without the need to change the tool.
Data string `xml:"data,attr"`
// Name is the name of the tool binary.
Name string `xml:"name,attr"`
// Package is the package path of the tool.
Package string `xml:"package,attr"`
// Project identifies the project that contains the tool. If not
// set, "https://vanadium.googlesource.com/<JiriProject>" is
// used as the default.
Project string `xml:"project,attr"`
}
// ScanMode determines whether LocalProjects should scan the local filesystem
// for projects (FullScan), or optimistically assume that the local projects
// will match those in the manifest (FastScan).
type ScanMode bool
const (
FastScan = ScanMode(false)
FullScan = ScanMode(true)
)
type UnsupportedProtocolErr string
func (e UnsupportedProtocolErr) Error() string {
return "unsupported protocol: " + string(e)
}
// Update represents an update of projects as a map from
// project names to a collections of commits.
type Update map[string][]CL
var devtoolsBinDir = filepath.Join("devtools", "bin")
// CreateSnapshot creates a manifest that encodes the current state of
// master branches of all projects and writes this snapshot out to the
// given file.
func CreateSnapshot(jirix *jiri.X, path string) error {
jirix.TimerPush("create snapshot")
defer jirix.TimerPop()
manifest := Manifest{}
// Add all local projects to manifest.
localProjects, err := LocalProjects(jirix, FullScan)
if err != nil {
return err
}
for _, project := range localProjects {
relPath, err := toRel(jirix, project.Path)
if err != nil {
return err
}
project.Path = relPath
manifest.Projects = append(manifest.Projects, project)
}
// Add all hosts, tools, and hooks from the current manifest to the
// snapshot manifest.
hosts, _, tools, hooks, err := readManifest(jirix, true)
if err != nil {
return err
}
for _, tool := range tools {
manifest.Tools = append(manifest.Tools, tool)
}
for _, host := range hosts {
manifest.Hosts = append(manifest.Hosts, host)
}
for _, hook := range hooks {
manifest.Hooks = append(manifest.Hooks, hook)
}
perm := os.FileMode(0755)
if err := jirix.Run().MkdirAll(filepath.Dir(path), perm); err != nil {
return err
}
data, err := xml.MarshalIndent(manifest, "", " ")
if err != nil {
return fmt.Errorf("MarshalIndent(%v) failed: %v", manifest, err)
}
perm = os.FileMode(0644)
if err := jirix.Run().WriteFile(path, data, perm); err != nil {
return err
}
return nil
}
const currentManifestFileName = ".current_manifest"
// CurrentManifest returns a manifest that identifies the result of
// the most recent "jiri update" invocation.
func CurrentManifest(jirix *jiri.X) (*Manifest, error) {
currentManifestPath := toAbs(jirix, currentManifestFileName)
bytes, err := jirix.Run().ReadFile(currentManifestPath)
if err != nil {
if os.IsNotExist(err) {
fmt.Fprintf(jirix.Stderr(), `WARNING: Could not find %s.
The contents of this file are stored as metadata in binaries the jiri
tool builds. To fix this problem, please run "jiri update".
`, currentManifestPath)
return &Manifest{}, nil
}
return nil, err
}
var m Manifest
if err := xml.Unmarshal(bytes, &m); err != nil {
return nil, fmt.Errorf("Unmarshal(%v) failed: %v", string(bytes), err)
}
return &m, nil
}
// writeCurrentManifest writes the given manifest to a file that
// stores the result of the most recent "jiri update" invocation.
func writeCurrentManifest(jirix *jiri.X, manifest *Manifest) error {
currentManifestPath := toAbs(jirix, currentManifestFileName)
bytes, err := xml.MarshalIndent(manifest, "", " ")
if err != nil {
return fmt.Errorf("MarshalIndent(%v) failed: %v", manifest, err)
}
if err := jirix.Run().WriteFile(currentManifestPath, bytes, os.FileMode(0644)); err != nil {
return err
}
return nil
}
// CurrentProjectName gets the name of the current project from the
// current directory by reading the jiri project metadata located in a
// directory at the root of the current repository.
func CurrentProjectName(jirix *jiri.X) (string, error) {
topLevel, err := jirix.Git().TopLevel()
if err != nil {
return "", nil
}
metadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir)
if _, err := jirix.Run().Stat(metadataDir); err == nil {
metadataFile := filepath.Join(metadataDir, jiri.ProjectMetaFile)
bytes, err := jirix.Run().ReadFile(metadataFile)
if err != nil {
return "", err
}
var project Project
if err := xml.Unmarshal(bytes, &project); err != nil {
return "", fmt.Errorf("Unmarshal() failed: %v", err)
}
return project.Name, nil
}
return "", nil
}
// setProjectRevisions sets the current project revision from the master for
// each project as found on the filesystem
func setProjectRevisions(jirix *jiri.X, projects Projects) (_ Projects, e error) {
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
for name, project := range projects {
switch project.Protocol {
case "git":
if err := jirix.Run().Chdir(project.Path); err != nil {
return nil, err
}
revision, err := jirix.Git().CurrentRevisionOfBranch("master")
if err != nil {
return nil, err
}
project.Revision = revision
default:
return nil, UnsupportedProtocolErr(project.Protocol)
}
projects[name] = project
}
return projects, nil
}
// LocalProjects returns projects on the local filesystem. If all projects in
// the manifest exist locally and scanMode is set to FastScan, then only the
// projects in the manifest that exist locally will be returned. Otherwise, a
// full scan of the filesystem will take place, and all found projects will be
// returned.
func LocalProjects(jirix *jiri.X, scanMode ScanMode) (Projects, error) {
jirix.TimerPush("local projects")
defer jirix.TimerPop()
if scanMode == FastScan {
// Fast path: Full scan was not requested, and all projects in
// manifest exist on local filesystem. We just use the projects
// directly from the manifest.
manifestProjects, _, err := ReadManifest(jirix)
if err != nil {
return nil, err
}
projectsExist, err := projectsExistLocally(jirix, manifestProjects)
if err != nil {
return nil, err
}
if projectsExist {
return setProjectRevisions(jirix, manifestProjects)
}
}
// Slow path: Either full scan was not requested, or projects exist in
// manifest that were not found locally. Do a recursive scan of all projects
// under JIRI_ROOT.
projects := Projects{}
jirix.TimerPush("scan fs")
err := findLocalProjects(jirix, jirix.Root, projects)
jirix.TimerPop()
if err != nil {
return nil, err
}
return setProjectRevisions(jirix, projects)
}
// projectsExistLocally returns true iff all the given projects exist on the
// local filesystem.
// Note that this may return true even if there are projects on the local
// filesystem not included in the provided projects argument.
func projectsExistLocally(jirix *jiri.X, projects Projects) (bool, error) {
jirix.TimerPush("match manifest")
defer jirix.TimerPop()
for _, p := range projects {
isLocal, err := isLocalProject(jirix, p.Path)
if err != nil {
return false, err
}
if !isLocal {
return false, nil
}
}
return true, nil
}
// PollProjects returns the set of changelists that exist remotely but not
// locally. Changes are grouped by projects and contain author identification
// and a description of their content.
func PollProjects(jirix *jiri.X, projectSet map[string]struct{}) (_ Update, e error) {
jirix.TimerPush("poll projects")
defer jirix.TimerPop()
// Switch back to current working directory when we're done.
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
// Gather local & remote project data.
localProjects, err := LocalProjects(jirix, FastScan)
if err != nil {
return nil, err
}
_, remoteProjects, _, _, err := readManifest(jirix, false)
if err != nil {
return nil, err
}
// Compute difference between local and remote.
update := Update{}
ops, err := computeOperations(localProjects, remoteProjects, false)
if err != nil {
return nil, err
}
for _, op := range ops {
name := op.Project().Name
// If given a project set, limit our results to those projects in the set.
if len(projectSet) > 0 {
if _, ok := projectSet[name]; !ok {
continue
}
}
// We only inspect this project if an update operation is required.
cls := []CL{}
if updateOp, ok := op.(updateOperation); ok {
switch updateOp.project.Protocol {
case "git":
// Enter project directory - this assumes absolute paths.
if err := jirix.Run().Chdir(updateOp.destination); err != nil {
return nil, err
}
// Fetch the latest from origin.
if err := jirix.Git().FetchRefspec("origin", updateOp.project.RemoteBranch); err != nil {
return nil, err
}
// Collect commits visible from FETCH_HEAD that aren't visible from master.
commitsText, err := jirix.Git().Log("FETCH_HEAD", "master", "%an%n%ae%n%B")
if err != nil {
return nil, err
}
// Format those commits and add them to the results.
for _, commitText := range commitsText {
if got, want := len(commitText), 3; got < want {
return nil, fmt.Errorf("Unexpected length of %v: got %v, want at least %v", commitText, got, want)
}
cls = append(cls, CL{
Author: commitText[0],
Email: commitText[1],
Description: strings.Join(commitText[2:], "\n"),
})
}
default:
return nil, UnsupportedProtocolErr(updateOp.project.Protocol)
}
}
update[name] = cls
}
return update, nil
}
// ReadManifest retrieves and parses the manifest that determines what
// projects and tools are part of the jiri universe.
func ReadManifest(jirix *jiri.X) (Projects, Tools, error) {
_, p, t, _, e := readManifest(jirix, false)
return p, t, e
}
// getManifestRemote returns the remote url of the origin from the manifest
// repo.
// TODO(nlacasse,toddw): Once the manifest project is specified in the
// manifest, we should get the remote directly from the manifest, and not from
// the filesystem.
func getManifestRemote(jirix *jiri.X, manifestPath string) (string, error) {
var remote string
return remote, jirix.NewSeq().Pushd(manifestPath).Call(
func() (e error) {
remote, e = jirix.Git().RemoteUrl("origin")
return
}, "get manifest origin").Done()
}
// readManifest implements the ReadManifest logic and provides an
// optional flag that can be used to fetch the latest manifest updates
// from the manifest repository.
func readManifest(jirix *jiri.X, update bool) (Hosts, Projects, Tools, Hooks, error) {
jirix.TimerPush("read manifest")
defer jirix.TimerPop()
if update {
manifestPath := toAbs(jirix, ".manifest")
manifestRemote, err := getManifestRemote(jirix, manifestPath)
if err != nil {
return nil, nil, nil, nil, err
}
project := Project{
Path: manifestPath,
Protocol: "git",
Remote: manifestRemote,
Revision: "HEAD",
RemoteBranch: "master",
}
if err := resetProject(jirix, project); err != nil {
return nil, nil, nil, nil, err
}
}
path, err := jirix.ResolveManifestPath(jirix.Manifest())
if err != nil {
return nil, nil, nil, nil, err
}
hosts, projects, tools, hooks, stack := Hosts{}, Projects{}, Tools{}, Hooks{}, map[string]struct{}{}
if err := loadManifest(jirix, path, hosts, projects, tools, hooks, stack); err != nil {
return nil, nil, nil, nil, err
}
return hosts, projects, tools, hooks, nil
}
// UpdateUniverse updates all local projects and tools to match the
// remote counterparts identified by the given manifest. Optionally,
// the 'gc' flag can be used to indicate that local projects that no
// longer exist remotely should be removed.
func UpdateUniverse(jirix *jiri.X, gc bool) (e error) {
jirix.TimerPush("update universe")
defer jirix.TimerPop()
_, remoteProjects, remoteTools, remoteHooks, err := readManifest(jirix, true)
if err != nil {
return err
}
// 1. Update all local projects to match their remote counterparts.
if err := updateProjects(jirix, remoteProjects, gc); err != nil {
return err
}
// 2. Build all tools in a temporary directory.
tmpDir, err := jirix.Run().TempDir("", "tmp-jiri-tools-build")
if err != nil {
return fmt.Errorf("TempDir() failed: %v", err)
}
defer collect.Error(func() error { return jirix.Run().RemoveAll(tmpDir) }, &e)
if err := buildToolsFromMaster(jirix, remoteTools, tmpDir); err != nil {
return err
}
// 3. Install the tools into $JIRI_ROOT/devtools/bin.
if err := InstallTools(jirix, tmpDir); err != nil {
return err
}
// 4. Run all specified hooks
return runHooks(jirix, remoteHooks)
}
// ApplyToLocalMaster applies an operation expressed as the given function to
// the local master branch of the given projects.
func ApplyToLocalMaster(jirix *jiri.X, projects Projects, fn func() error) (e error) {
cwd, err := os.Getwd()
if err != nil {
return err
}
defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
// Loop through all projects, checking out master and stashing any unstaged
// changes.
for _, project := range projects {
p := project
if err := jirix.Run().Chdir(p.Path); err != nil {
return err
}
switch p.Protocol {
case "git":
branch, err := jirix.Git().CurrentBranchName()
if err != nil {
return err
}
stashed, err := jirix.Git().Stash()
if err != nil {
return err
}
if err := jirix.Git().CheckoutBranch("master"); err != nil {
return err
}
// After running the function, return to this project's directory,
// checkout the original branch, and stash pop if necessary.
defer collect.Error(func() error {
if err := jirix.Run().Chdir(p.Path); err != nil {
return err
}
if err := jirix.Git().CheckoutBranch(branch); err != nil {
return err
}
if stashed {
return jirix.Git().StashPop()
}
return nil
}, &e)
default:
return UnsupportedProtocolErr(p.Protocol)
}
}
return fn()
}
// BuildTools builds the given tools and places the resulting binaries into the
// given directory.
func BuildTools(jirix *jiri.X, tools Tools, outputDir string) error {
jirix.TimerPush("build tools")
defer jirix.TimerPop()
if len(tools) == 0 {
// Nothing to do here...
return nil
}
projects, err := LocalProjects(jirix, FastScan)
if err != nil {
return err
}
toolPkgs := []string{}
workspaceSet := map[string]bool{}
for _, tool := range tools {
toolPkgs = append(toolPkgs, tool.Package)
toolProject, ok := projects[tool.Project]
if !ok {
return fmt.Errorf("project not found for tool %v", tool.Name)
}
// Identify the Go workspace the tool is in. To this end we use a
// heuristic that identifies the maximal suffix of the project path
// that corresponds to a prefix of the package name.
workspace := ""
for i := 0; i < len(toolProject.Path); i++ {
if toolProject.Path[i] == filepath.Separator {
if strings.HasPrefix("src/"+tool.Package, filepath.ToSlash(toolProject.Path[i+1:])) {
workspace = toolProject.Path[:i]
break
}
}
}
if workspace == "" {
return fmt.Errorf("could not identify go workspace for tool %v", tool.Name)
}
workspaceSet[workspace] = true
}
workspaces := []string{}
for workspace := range workspaceSet {
workspaces = append(workspaces, workspace)
}
if envGoPath := os.Getenv("GOPATH"); envGoPath != "" {
workspaces = append(workspaces, strings.Split(envGoPath, string(filepath.ListSeparator))...)
}
var stderr bytes.Buffer
opts := jirix.Run().Opts()
// We unset GOARCH and GOOS because jiri update should always build for the
// native architecture and OS. Also, as of go1.5, setting GOBIN is not
// compatible with GOARCH or GOOS.
opts.Env = map[string]string{
"GOARCH": "",
"GOOS": "",
"GOBIN": outputDir,
"GOPATH": strings.Join(workspaces, string(filepath.ListSeparator)),
}
opts.Stdout = ioutil.Discard
opts.Stderr = &stderr
args := append([]string{"install"}, toolPkgs...)
if err := jirix.Run().CommandWithOpts(opts, "go", args...); err != nil {
return fmt.Errorf("tool build failed\n%v", stderr.String())
}
return nil
}
// buildToolsFromMaster builds and installs all jiri tools using the version
// available in the local master branch of the tools repository. Notably, this
// function does not perform any version control operation on the master
// branch.
func buildToolsFromMaster(jirix *jiri.X, tools Tools, outputDir string) error {
localProjects, err := LocalProjects(jirix, FastScan)
if err != nil {
return err
}
failed := false
toolsToBuild, toolProjects := Tools{}, Projects{}
toolNames := []string{} // Used for logging purposes.
for _, tool := range tools {
// Skip tools with no package specified. Besides increasing
// robustness, this step also allows us to create jiri root
// fakes without having to provide an implementation for the "jiri"
// tool, which every manifest needs to specify.
if tool.Package == "" {
continue
}
project, ok := localProjects[tool.Project]
if !ok {
fmt.Errorf("unknown project %v for tool %v", tool.Project, tool.Name)
}
toolProjects[tool.Project] = project
toolsToBuild[tool.Name] = tool
toolNames = append(toolNames, tool.Name)
}
updateFn := func() error {
return ApplyToLocalMaster(jirix, toolProjects, func() error {
return BuildTools(jirix, toolsToBuild, outputDir)
})
}
// Always log the output of updateFn, irrespective of
// the value of the verbose flag.
opts := runutil.Opts{Verbose: true}
if err := jirix.Run().FunctionWithOpts(opts, updateFn, "build tools: %v", strings.Join(toolNames, " ")); err != nil {
fmt.Fprintf(jirix.Stderr(), "%v\n", err)
failed = true
}
if failed {
return cmdline.ErrExitCode(2)
}
return nil
}
// CleanupProjects restores the given jiri projects back to their master
// branches and gets rid of all the local changes. If "cleanupBranches" is
// true, it will also delete all the non-master branches.
func CleanupProjects(jirix *jiri.X, projects Projects, cleanupBranches bool) (e error) {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("Getwd() failed: %v", err)
}
defer collect.Error(func() error { return jirix.Run().Chdir(wd) }, &e)
for _, project := range projects {
localProjectDir := project.Path
if err := jirix.Run().Chdir(localProjectDir); err != nil {
return err
}
if err := resetLocalProject(jirix, cleanupBranches, project.RemoteBranch); err != nil {
return err
}
}
return nil
}
// resetLocalProject checks out the master branch, cleans up untracked files
// and uncommitted changes, and optionally deletes all the other branches.
func resetLocalProject(jirix *jiri.X, cleanupBranches bool, remoteBranch string) error {
// Check out master and clean up changes.
curBranchName, err := jirix.Git().CurrentBranchName()
if err != nil {
return err
}
if curBranchName != "master" {
if err := jirix.Git().CheckoutBranch("master", gitutil.ForceOpt(true)); err != nil {
return err
}
}
if err := jirix.Git().RemoveUntrackedFiles(); err != nil {
return err
}
// Discard any uncommitted changes.
if remoteBranch == "" {
remoteBranch = "master"
}
if err := jirix.Git().Reset("origin/" + remoteBranch); err != nil {
return err
}
// Delete all the other branches.
// At this point we should be at the master branch.
branches, _, err := jirix.Git().GetBranches()
if err != nil {
return err
}
for _, branch := range branches {
if branch == "master" {
continue
}
if cleanupBranches {
if err := jirix.Git().DeleteBranch(branch, gitutil.ForceOpt(true)); err != nil {
return nil
}
}
}
return nil
}
// isLocalProject returns true if there is a project at the given path.
func isLocalProject(jirix *jiri.X, path string) (bool, error) {
absPath := toAbs(jirix, path)
// Existence of a metadata directory is how we know we've found a
// Jiri-maintained project.
metadataDir := filepath.Join(absPath, jiri.ProjectMetaDir)
if _, err := jirix.Run().Stat(metadataDir); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
// projectAtPath returns a Project struct corresponding to the project at the
// path in the filesystem.
func projectAtPath(jirix *jiri.X, path string) (Project, error) {
var project Project
absPath := toAbs(jirix, path)
metadataFile := filepath.Join(absPath, jiri.ProjectMetaDir, jiri.ProjectMetaFile)
bytes, err := jirix.Run().ReadFile(metadataFile)
if err != nil {
return project, err
}
if err := xml.Unmarshal(bytes, &project); err != nil {
return project, fmt.Errorf("Unmarshal() failed: %v\n%s", err, string(bytes))
}
project.Path = toAbs(jirix, project.Path)
return project, nil
}
// findLocalProjects scans the filesystem for all projects. Note that project
// directories can be nested recursively.
func findLocalProjects(jirix *jiri.X, path string, projects Projects) error {
absPath := toAbs(jirix, path)
isLocal, err := isLocalProject(jirix, absPath)
if err != nil {
return err
}
if isLocal {
project, err := projectAtPath(jirix, absPath)
if err != nil {
return err
}
if absPath != project.Path {
return fmt.Errorf("project %v has path %v but was found in %v", project.Name, project.Path, absPath)
}
if p, ok := projects[project.Name]; ok {
return fmt.Errorf("name conflict: both %v and %v contain the project %v", p.Path, project.Path, project.Name)
}
projects[project.Name] = project
}
// Recurse into all the sub directories.
fileInfos, err := jirix.Run().ReadDir(path)
if err != nil {
return err
}
for _, fileInfo := range fileInfos {
if fileInfo.IsDir() && !strings.HasPrefix(fileInfo.Name(), ".") {
if err := findLocalProjects(jirix, filepath.Join(path, fileInfo.Name()), projects); err != nil {
return err
}
}
}
return nil
}
// InstallTools installs the tools from the given directory into
// $JIRI_ROOT/devtools/bin.
func InstallTools(jirix *jiri.X, dir string) error {
jirix.TimerPush("install tools")
defer jirix.TimerPop()
if jirix.DryRun() {
// In "dry run" mode, no binaries are built.
return nil
}
binDir := toAbs(jirix, devtoolsBinDir)
fis, err := ioutil.ReadDir(dir)
if err != nil {
return fmt.Errorf("ReadDir(%v) failed: %v", dir, err)
}
failed := false
for _, fi := range fis {
installFn := func() error {
src := filepath.Join(dir, fi.Name())
dst := filepath.Join(binDir, fi.Name())
if err := jirix.Run().Rename(src, dst); err != nil {
return err
}
return nil
}
opts := runutil.Opts{Verbose: true}
if err := jirix.Run().FunctionWithOpts(opts, installFn, "install tool %q", fi.Name()); err != nil {
fmt.Fprintf(jirix.Stderr(), "%v\n", err)
failed = true
}
}
if failed {
return cmdline.ErrExitCode(2)
}
// Delete any old subcommands.
v23SubCmds := []string{
"jiri-profile",
"jiri-env",
}
for _, subCmd := range v23SubCmds {
subCmdPath := filepath.Join(binDir, subCmd)
if err := jirix.Run().RemoveAll(subCmdPath); err != nil {
return err
}
}
return nil
}
// runHooks runs the specified hooks
func runHooks(jirix *jiri.X, hooks Hooks) error {
jirix.TimerPush("run hooks")
defer jirix.TimerPop()
for _, hook := range hooks {
command := hook.Path
args := []string{}
if hook.Interpreter != "" {
command = hook.Interpreter
args = append(args, hook.Path)
}
for _, arg := range hook.Args {
args = append(args, arg.Arg)
}
if err := jirix.Run().Command(command, args...); err != nil {
return fmt.Errorf("Hook %v failed: %v command: %v args: %v", hook.Name, err, command, args)
}
}
return nil
}
// resetProject advances the local master branch of the given
// project, which is expected to exist locally at project.Path.
func resetProject(jirix *jiri.X, project Project) error {
fn := func() error {
switch project.Protocol {
case "git":
if project.Remote == "" {
return fmt.Errorf("project %v does not have a remote", project.Name)
}
if err := jirix.Git().SetRemoteUrl("origin", project.Remote); err != nil {
return err
}
if err := jirix.Git().Fetch("origin"); err != nil {
return err
}
// Having a specific revision trumps everything else - once fetched,
// always reset to that revision.
if project.Revision != "" && project.Revision != "HEAD" {
return jirix.Git().Reset(project.Revision)
}
// If no revision, reset to the configured remote branch, or master
// if no remote branch.
remoteBranch := project.RemoteBranch
if remoteBranch == "" {
remoteBranch = "master"
}
return jirix.Git().Reset("origin/" + remoteBranch)
default:
return UnsupportedProtocolErr(project.Protocol)
}
}
return ApplyToLocalMaster(jirix, Projects{project.Name: project}, fn)
}
// loadManifest loads the given manifest, processing all of its
// imports, projects and tools settings.
func loadManifest(jirix *jiri.X, path string, hosts Hosts, projects Projects, tools Tools, hooks Hooks, stack map[string]struct{}) error {
data, err := jirix.Run().ReadFile(path)
if err != nil {
return err
}
m := &Manifest{}
if err := xml.Unmarshal(data, m); err != nil {
return fmt.Errorf("Unmarshal(%v) failed: %v", string(data), err)
}
// Process all imports.
for _, manifest := range m.Imports {
if _, ok := stack[manifest.Name]; ok {
return fmt.Errorf("import cycle encountered")
}
path, err := jirix.ResolveManifestPath(manifest.Name)
if err != nil {
return err
}
stack[manifest.Name] = struct{}{}
if err := loadManifest(jirix, path, hosts, projects, tools, hooks, stack); err != nil {
return err
}
delete(stack, manifest.Name)
}
// Process all projects.
for _, project := range m.Projects {
if project.Exclude {
// Exclude the project in case it was
// previously included.
delete(projects, project.Name)
continue
}
// Replace the relative path with an absolute one.
project.Path = toAbs(jirix, project.Path)
// Use git as the default protocol.
if project.Protocol == "" {
project.Protocol = "git"
}
// Use HEAD and tip as the default revision for git
// and mercurial respectively.
if project.Revision == "" {
switch project.Protocol {
case "git":
project.Revision = "HEAD"
default:
return UnsupportedProtocolErr(project.Protocol)
}
}
// Default to "master" branch if none is provided.
if project.RemoteBranch == "" {
project.RemoteBranch = "master"
}
projects[project.Name] = project
}
// Process all tools.
for _, tool := range m.Tools {
if tool.Exclude {
// Exclude the tool in case it was previously
// included.
delete(tools, tool.Name)
continue
}
// Use <JiriProject> as the default project.
if tool.Project == "" {
tool.Project = "https://vanadium.googlesource.com/" + JiriProject
}
// Use "data" as the default data.
if tool.Data == "" {
tool.Data = "data"
}
tools[tool.Name] = tool
}
// Process all hooks.
for _, hook := range m.Hooks {
if hook.Exclude {
// Exclude the hook in case it was previously
// included.
delete(hooks, hook.Name)
continue
}
project, found := projects[hook.Project]
if !found {
return fmt.Errorf("hook %v specified project %v which was not found",
hook.Name, hook.Project)
}
// Replace project-relative path with absolute path.
hook.Path = filepath.Join(project.Path, hook.Path)
hooks[hook.Name] = hook
}
// Process all hosts.
for _, host := range m.Hosts {
hosts[host.Name] = host
// Sanity check that we only have githooks for git hosts.
if host.Name != "git" {
if len(host.GitHooks) > 0 {
return fmt.Errorf("githook provided for a non-Git host: %s", host.Location)
}
}
}
return nil
}
// reportNonMaster checks if the given project is on master branch and
// if not, reports this fact along with information on how to update it.
func reportNonMaster(jirix *jiri.X, project Project) (e error) {
cwd, err := os.Getwd()
if err != nil {
return err
}
defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
if err := jirix.Run().Chdir(project.Path); err != nil {
return err
}
switch project.Protocol {
case "git":
current, err := jirix.Git().CurrentBranchName()
if err != nil {
return err
}
if current != "master" {
line1 := fmt.Sprintf(`NOTE: "jiri update" only updates the "master" branch and the current branch is %q`, current)
line2 := fmt.Sprintf(`to update the %q branch once the master branch is updated, run "git merge master"`, current)
opts := runutil.Opts{Verbose: true}
jirix.Run().OutputWithOpts(opts, []string{line1, line2})
}
return nil
default:
return UnsupportedProtocolErr(project.Protocol)
}
}
// getRemoteHeadRevisions attempts to get the repo statuses from remote for HEAD
// projects so we can detect when a local project is already up-to-date.
func getRemoteHeadRevisions(jirix *jiri.X, remoteProjects Projects) {
someAtHead := false
for _, rp := range remoteProjects {
if rp.Revision == "HEAD" {
someAtHead = true
break
}
}
if !someAtHead {
return
}
gitHost, gitHostErr := GitHost(jirix)
if gitHostErr != nil || !googlesource.IsGoogleSourceHost(gitHost) {
return
}
repoStatuses, err := googlesource.GetRepoStatuses(jirix.Context, gitHost)
if err != nil {
// Log the error but don't fail.
fmt.Fprintf(jirix.Stderr(), "Error fetching repo statuses from remote: %v\n", err)
return
}
for name, rp := range remoteProjects {
if rp.Revision != "HEAD" {
continue
}
status, ok := repoStatuses[rp.Name]
if !ok {
continue
}
masterRev, ok := status.Branches["master"]
if !ok || masterRev == "" {
continue
}
rp.Revision = masterRev
remoteProjects[name] = rp
}
}
func updateProjects(jirix *jiri.X, remoteProjects Projects, gc bool) error {
jirix.TimerPush("update projects")
defer jirix.TimerPop()
scanMode := FastScan
if gc {
scanMode = FullScan
}
localProjects, err := LocalProjects(jirix, scanMode)
if err != nil {
return err
}
getRemoteHeadRevisions(jirix, remoteProjects)
ops, err := computeOperations(localProjects, remoteProjects, gc)
if err != nil {
return err
}
for _, op := range ops {
if err := op.Test(jirix); err != nil {
return err
}
}
failed := false
manifest := &Manifest{Label: jirix.Manifest()}
for _, op := range ops {
updateFn := func() error { return op.Run(jirix, manifest) }
// Always log the output of updateFn, irrespective of
// the value of the verbose flag.
opts := runutil.Opts{Verbose: true}
if err := jirix.Run().FunctionWithOpts(opts, updateFn, "%v", op); err != nil {
fmt.Fprintf(jirix.Stderr(), "%v\n", err)
failed = true
}
}
if failed {
return cmdline.ErrExitCode(2)
}
if err := writeCurrentManifest(jirix, manifest); err != nil {
return err
}
return nil
}
// writeMetadata stores the given project metadata in the directory
// identified by the given path.
func writeMetadata(jirix *jiri.X, project Project, dir string) (e error) {
metadataDir := filepath.Join(dir, jiri.ProjectMetaDir)
cwd, err := os.Getwd()
if err != nil {
return err
}
defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
if err := jirix.Run().MkdirAll(metadataDir, os.FileMode(0755)); err != nil {
return err
}
if err := jirix.Run().Chdir(metadataDir); err != nil {
return err
}
// Replace absolute project paths with relative paths to make it
// possible to move the $JIRI_ROOT directory locally.
relPath, err := toRel(jirix, project.Path)
if err != nil {
return err
}
project.Path = relPath
bytes, err := xml.Marshal(project)
if err != nil {
return fmt.Errorf("Marhsal() failed: %v", err)
}
metadataFile := filepath.Join(metadataDir, jiri.ProjectMetaFile)
tmpMetadataFile := metadataFile + ".tmp"
if err := jirix.Run().WriteFile(tmpMetadataFile, bytes, os.FileMode(0644)); err != nil {
return err
}
if err := jirix.Run().Rename(tmpMetadataFile, metadataFile); err != nil {
return err
}
return nil
}
// addProjectToManifest records the information about the given
// project in the given manifest. The function is used to create a
// manifest that records the current state of jiri projects, which
// can be used to restore this state at some later point.
//
// NOTE: The function assumes that the the given project is on a
// master branch.
func addProjectToManifest(jirix *jiri.X, manifest *Manifest, project Project) error {
// If the project uses relative revision, replace it with an absolute one.
switch project.Protocol {
case "git":
if project.Revision == "HEAD" {
revision, err := jirix.Git(tool.RootDirOpt(project.Path)).CurrentRevision()
if err != nil {
return err
}
project.Revision = revision
}
default:
return UnsupportedProtocolErr(project.Protocol)
}
relPath, err := toRel(jirix, project.Path)
if err != nil {
return err
}
project.Path = relPath
manifest.Projects = append(manifest.Projects, project)
return nil
}
type operation interface {
// Project identifies the project this operation pertains to.
Project() Project
// Run executes the operation.
Run(jirix *jiri.X, manifest *Manifest) error
// String returns a string representation of the operation.
String() string
// Test checks whether the operation would fail.
Test(jirix *jiri.X) error
}
// commonOperation represents a project operation.
type commonOperation struct {
// project holds information about the project such as its
// name, local path, and the protocol it uses for version
// control.
project Project
// destination is the new project path.
destination string
// source is the current project path.
source string
}
func (op commonOperation) Project() Project {
return op.project
}
// createOperation represents the creation of a project.
type createOperation struct {
commonOperation
}
func (op createOperation) Run(jirix *jiri.X, manifest *Manifest) (e error) {
hosts, _, _, _, err := readManifest(jirix, false)
if err != nil {
return err
}
path, perm := filepath.Dir(op.destination), os.FileMode(0755)
if err := jirix.Run().MkdirAll(path, perm); err != nil {
return err
}
// Create a temporary directory for the initial setup of the
// project to prevent an untimely termination from leaving the
// $JIRI_ROOT directory in an inconsistent state.
tmpDirPrefix := strings.Replace(op.Project().Name, "/", ".", -1) + "-"
tmpDir, err := jirix.Run().TempDir(path, tmpDirPrefix)
if err != nil {
return err
}
defer collect.Error(func() error { return jirix.Run().RemoveAll(tmpDir) }, &e)
switch op.project.Protocol {
case "git":
if err := jirix.Git().Clone(op.project.Remote, tmpDir); err != nil {
return err
}
// Apply git hooks. We're creating this repo, so there's no danger of
// overriding existing hooks. Customizing your git hooks with jiri is a bad
// idea anyway, since jiri won't know to not delete the project when you
// switch between manifests or do a cleanup.
host, found := hosts["git"]
if found && strings.HasPrefix(op.project.Remote, host.Location) {
gitHookDir := filepath.Join(tmpDir, ".git", "hooks")
for _, githook := range host.GitHooks {
mdir := jirix.ManifestDir()
src, err := jirix.Run().ReadFile(filepath.Join(mdir, githook.Path))
if err != nil {
return err
}
dst := filepath.Join(gitHookDir, githook.Name)
if err := jirix.Run().WriteFile(dst, src, perm); err != nil {
return err
}
}
}
// Apply exclusion for /.jiri/. We're creating the repo so we can safely
// write to .git/info/exclude
excludeString := "/.jiri/\n"
excludeDir := filepath.Join(tmpDir, ".git", "info")
if err := jirix.Run().MkdirAll(excludeDir, os.FileMode(0750)); err != nil {
return err
}
excludeFile := filepath.Join(excludeDir, "exclude")
if err := jirix.Run().WriteFile(excludeFile, []byte(excludeString), perm); err != nil {
return err
}
cwd, err := os.Getwd()
if err != nil {
return err
}
defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
if err := jirix.Run().Chdir(tmpDir); err != nil {
return err
}
if err := jirix.Git().Reset(op.project.Revision); err != nil {
return err
}
default:
return UnsupportedProtocolErr(op.project.Protocol)
}
if err := writeMetadata(jirix, op.project, tmpDir); err != nil {
return err
}
if err := jirix.Run().Chmod(tmpDir, os.FileMode(0755)); err != nil {
return err
}
if err := jirix.Run().Rename(tmpDir, op.destination); err != nil {
return err
}
if err := resetProject(jirix, op.project); err != nil {
return err
}
return addProjectToManifest(jirix, manifest, op.project)
}
func (op createOperation) String() string {
return fmt.Sprintf("create project %q in %q and advance it to %q", op.project.Name, op.destination, fmtRevision(op.project.Revision))
}
func (op createOperation) Test(jirix *jiri.X) error {
// Check the local file system.
if _, err := jirix.Run().Stat(op.destination); err != nil {
if !os.IsNotExist(err) {
return err
}
} else {
return fmt.Errorf("cannot create %q as it already exists", op.destination)
}
return nil
}
// deleteOperation represents the deletion of a project.
type deleteOperation struct {
commonOperation
// gc determines whether the operation should be executed or
// whether it should only print a notification.
gc bool
}
func (op deleteOperation) Run(jirix *jiri.X, _ *Manifest) error {
if op.gc {
// Never delete the <JiriProject>.
if op.project.Name == JiriProject {
lines := []string{
fmt.Sprintf("NOTE: project %v was not found in the project manifest", op.project.Name),
"however this project is required for correct operation of the jiri",
"development tools and will thus not be deleted",
}
opts := runutil.Opts{Verbose: true}
jirix.Run().OutputWithOpts(opts, lines)
return nil
}
// Never delete projects with non-master branches, uncommitted
// work, or untracked content.
git := jirix.Git(tool.RootDirOpt(op.project.Path))
branches, _, err := git.GetBranches()
if err != nil {
return err
}
uncommitted, err := git.HasUncommittedChanges()
if err != nil {
return err
}
untracked, err := git.HasUntrackedFiles()
if err != nil {
return err
}
if len(branches) != 1 || uncommitted || untracked {
lines := []string{
fmt.Sprintf("NOTE: project %v was not found in the project manifest", op.project.Name),
"however this project either contains non-master branches, uncommitted",
"work, or untracked files and will thus not be deleted",
}
opts := runutil.Opts{Verbose: true}
jirix.Run().OutputWithOpts(opts, lines)
return nil
}
return jirix.Run().RemoveAll(op.source)
}
lines := []string{
fmt.Sprintf("NOTE: project %v was not found in the project manifest", op.project.Name),
"it was not automatically removed to avoid deleting uncommitted work",
fmt.Sprintf(`if you no longer need it, invoke "rm -rf %v"`, op.source),
`or invoke "jiri update -gc" to remove all such local projects`,
}
opts := runutil.Opts{Verbose: true}
jirix.Run().OutputWithOpts(opts, lines)
return nil
}
func (op deleteOperation) String() string {
return fmt.Sprintf("delete project %q from %q", op.project.Name, op.source)
}
func (op deleteOperation) Test(jirix *jiri.X) error {
if _, err := jirix.Run().Stat(op.source); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("cannot delete %q as it does not exist", op.source)
}
return err
}
return nil
}
// moveOperation represents the relocation of a project.
type moveOperation struct {
commonOperation
}
func (op moveOperation) Run(jirix *jiri.X, manifest *Manifest) error {
path, perm := filepath.Dir(op.destination), os.FileMode(0755)
if err := jirix.Run().MkdirAll(path, perm); err != nil {
return err
}
if err := jirix.Run().Rename(op.source, op.destination); err != nil {
return err
}
if err := reportNonMaster(jirix, op.project); err != nil {
return err
}
if err := resetProject(jirix, op.project); err != nil {
return err
}
if err := writeMetadata(jirix, op.project, op.project.Path); err != nil {
return err
}
return addProjectToManifest(jirix, manifest, op.project)
}
func (op moveOperation) String() string {
return fmt.Sprintf("move project %q located in %q to %q and advance it to %q", op.project.Name, op.source, op.destination, fmtRevision(op.project.Revision))
}
func (op moveOperation) Test(jirix *jiri.X) error {
if _, err := jirix.Run().Stat(op.source); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("cannot move %q to %q as the source does not exist", op.source, op.destination)
}
return err
}
if _, err := jirix.Run().Stat(op.destination); err != nil {
if !os.IsNotExist(err) {
return err
}
} else {
return fmt.Errorf("cannot move %q to %q as the destination already exists", op.source, op.destination)
}
return nil
}
// updateOperation represents the update of a project.
type updateOperation struct {
commonOperation
}
func (op updateOperation) Run(jirix *jiri.X, manifest *Manifest) error {
if err := reportNonMaster(jirix, op.project); err != nil {
return err
}
if err := resetProject(jirix, op.project); err != nil {
return err
}
if err := writeMetadata(jirix, op.project, op.project.Path); err != nil {
return err
}
return addProjectToManifest(jirix, manifest, op.project)
}
func (op updateOperation) String() string {
return fmt.Sprintf("advance project %q located in %q to %q", op.project.Name, op.source, fmtRevision(op.project.Revision))
}
func (op updateOperation) Test(jirix *jiri.X) error {
return nil
}
// nullOperation represents a noop. It is used for logging and adding project
// information to the current manifest.
type nullOperation struct {
commonOperation
}
func (op nullOperation) Run(jirix *jiri.X, manifest *Manifest) error {
return addProjectToManifest(jirix, manifest, op.project)
}
func (op nullOperation) String() string {
return fmt.Sprintf("project %q located in %q at revision %q is up-to-date", op.project.Name, op.source, fmtRevision(op.project.Revision))
}
func (op nullOperation) Test(jirix *jiri.X) error {
return nil
}
// operations is a sortable collection of operations
type operations []operation
// Len returns the length of the collection.
func (ops operations) Len() int {
return len(ops)
}
// Less defines the order of operations. Operations are ordered first
// by their type and then by their project name.
//
// The order in which operation types are defined determines the order
// in which operations are performed. For correctness and also to
// minimize the chance of a conflict, the delete operations should
// happen before move operations, which should happen before create
// operations. If two create operations make nested directories, the
// outermost should be created first.
func (ops operations) Less(i, j int) bool {
vals := make([]int, 2)
for idx, op := range []operation{ops[i], ops[j]} {
switch op.(type) {
case deleteOperation:
vals[idx] = 0
case moveOperation:
vals[idx] = 1
case createOperation:
vals[idx] = 2
case updateOperation:
vals[idx] = 3
case nullOperation:
vals[idx] = 4
}
}
if vals[0] != vals[1] {
return vals[0] < vals[1]
}
if vals[0] == 2 && vals[1] == 2 {
pathI := ops[i].Project().Path
pathJ := ops[j].Project().Path
if strings.HasPrefix(pathI, pathJ) {
return false
}
if strings.HasPrefix(pathJ, pathI) {
return true
}
}
return ops[i].Project().Name < ops[j].Project().Name
}
// Swap swaps two elements of the collection.
func (ops operations) Swap(i, j int) {
ops[i], ops[j] = ops[j], ops[i]
}
// computeOperations inputs a set of projects to update and the set of
// current and new projects (as defined by contents of the local file
// system and manifest file respectively) and outputs a collection of
// operations that describe the actions needed to update the target
// projects.
func computeOperations(localProjects, remoteProjects Projects, gc bool) (operations, error) {
result := operations{}
allProjects := map[string]struct{}{}
for name, _ := range localProjects {
allProjects[name] = struct{}{}
}
for name, _ := range remoteProjects {
allProjects[name] = struct{}{}
}
for name, _ := range allProjects {
if localProject, ok := localProjects[name]; ok {
if remoteProject, ok := remoteProjects[name]; ok {
if localProject.Path != remoteProject.Path {
// moveOperation also does an update, so we don't need to
// check the revision here.
result = append(result, moveOperation{commonOperation{
destination: remoteProject.Path,
project: remoteProject,
source: localProject.Path,
}})
} else {
if localProject.Revision != remoteProject.Revision {
result = append(result, updateOperation{commonOperation{
destination: remoteProject.Path,
project: remoteProject,
source: localProject.Path,
}})
} else {
result = append(result, nullOperation{commonOperation{
destination: remoteProject.Path,
project: remoteProject,
source: localProject.Path,
}})
}
}
} else {
result = append(result, deleteOperation{commonOperation{
destination: "",
project: localProject,
source: localProject.Path,
}, gc})
}
} else if remoteProject, ok := remoteProjects[name]; ok {
result = append(result, createOperation{commonOperation{
destination: remoteProject.Path,
project: remoteProject,
source: "",
}})
} else {
return nil, fmt.Errorf("project %v does not exist", name)
}
}
sort.Sort(result)
return result, nil
}
// ParseNames identifies the set of projects that a jiri command should
// be applied to.
func ParseNames(jirix *jiri.X, args []string, defaultProjects map[string]struct{}) (map[string]Project, error) {
projects, _, err := ReadManifest(jirix)
if err != nil {
return nil, err
}
result := map[string]Project{}
if len(args) == 0 {
// Use the default set of projects.
args = set.String.ToSlice(defaultProjects)
}
for _, name := range args {
if project, ok := projects[name]; ok {
result[name] = project
} else {
// Issue a warning if the target project does not exist in the
// project manifest.
fmt.Fprintf(jirix.Stderr(), "WARNING: project %q does not exist in the project manifest and will be skipped\n", name)
}
}
return result, nil
}
// fmtRevision returns the first 8 chars of a revision hash.
func fmtRevision(r string) string {
l := 8
if len(r) < l {
return r
}
return r[:l]
}