blob: 69284b8cc72e74aac925396a8a2c6a3c451ac86f [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 (
"fmt"
"io"
"os"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"fuchsia.googlesource.com/jiri"
"fuchsia.googlesource.com/jiri/gitutil"
"fuchsia.googlesource.com/jiri/log"
"fuchsia.googlesource.com/jiri/osutil"
)
// fsUpdates is used to track filesystem updates made by operations.
// TODO(nlacasse): Currently we only use fsUpdates to track deletions so that
// jiri can delete and create a project in the same directory in one update.
// There are lots of other cases that should be covered though, like detecting
// when two projects would be created in the same directory.
type fsUpdates struct {
deletedDirs map[string]bool
}
func newFsUpdates() *fsUpdates {
return &fsUpdates{
deletedDirs: map[string]bool{},
}
}
func (u *fsUpdates) deleteDir(dir string) {
dir = filepath.Clean(dir)
u.deletedDirs[dir] = true
}
func (u *fsUpdates) isDeleted(dir string) bool {
_, ok := u.deletedDirs[filepath.Clean(dir)]
return ok
}
type operation interface {
// Project identifies the project this operation pertains to.
Project() Project
// Kind returns the kind of operation.
Kind() string
// Run executes the operation.
Run(jirix *jiri.X) error
// String returns a string representation of the operation.
String() string
// Test checks whether the operation would fail.
Test(jirix *jiri.X, updates *fsUpdates) 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
// state is the state of the local project
state ProjectState
}
func (op commonOperation) Project() Project {
return op.project
}
// createOperation represents the creation of a project.
type createOperation struct {
commonOperation
}
func (op createOperation) Kind() string {
return "create"
}
func (op createOperation) checkoutProject(jirix *jiri.X, cache string) error {
var err error
remote := rewriteRemote(jirix, op.project.Remote)
// Hack to make fuchsia.git happen
if op.destination == jirix.Root {
scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path))
if err = scm.Init(op.destination); err != nil {
return err
}
if err = scm.AddOrReplaceRemote("origin", remote); err != nil {
return err
}
// We must specify a refspec here in order for patch to be able to set
// upstream to 'origin/master'.
if op.project.HistoryDepth > 0 && cache != "" {
if err = scm.FetchRefspec(cache, "+refs/heads/*:refs/remotes/origin/*", gitutil.DepthOpt(op.project.HistoryDepth)); err != nil {
return err
}
} else if cache != "" {
if err = scm.FetchRefspec(cache, "+refs/heads/*:refs/remotes/origin/*"); err != nil {
return err
}
} else {
if err = scm.FetchRefspec(remote, "+refs/heads/*:refs/remotes/origin/*"); err != nil {
return err
}
}
} else {
opts := []gitutil.CloneOpt{gitutil.NoCheckoutOpt(true)}
if op.project.HistoryDepth > 0 {
opts = append(opts, gitutil.DepthOpt(op.project.HistoryDepth))
} else {
// Shallow clones can not be used as as local git reference
opts = append(opts, gitutil.ReferenceOpt(cache))
}
if jirix.Partial {
opts = append(opts, gitutil.OmitBlobsOpt(true))
}
if cache != "" {
if err = clone(jirix, cache, op.destination, opts...); err != nil {
return err
}
scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path))
if err = scm.AddOrReplaceRemote("origin", remote); err != nil {
return err
}
} else {
if err = clone(jirix, remote, op.destination, opts...); err != nil {
return err
}
}
}
if err := os.Chmod(op.destination, os.FileMode(0755)); err != nil {
return fmtError(err)
}
if err := checkoutHeadRevision(jirix, op.project, false); err != nil {
return err
}
if err := writeMetadata(jirix, op.project, op.project.Path); err != nil {
return err
}
scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path))
// Reset remote to point to correct location so that shared cache does not cause problem.
if err := scm.SetRemoteUrl("origin", remote); err != nil {
return err
}
// Delete inital branch(es)
if branches, _, err := scm.GetBranches(); err != nil {
jirix.Logger.Warningf("not able to get branches for newly created project %s(%s)\n\n", op.project.Name, op.project.Path)
} else {
scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path))
for _, b := range branches {
if err := scm.DeleteBranch(b); err != nil {
jirix.Logger.Warningf("not able to delete branch %s for project %s(%s)\n\n", b, op.project.Name, op.project.Path)
}
}
}
return nil
}
func (op createOperation) Run(jirix *jiri.X) (e error) {
path, perm := filepath.Dir(op.destination), os.FileMode(0755)
// Check the local file system.
if op.destination != jirix.Root {
if _, err := os.Stat(op.destination); err != nil {
if !os.IsNotExist(err) {
return fmtError(err)
}
} else {
if isEmpty, err := isEmpty(op.destination); err != nil {
return err
} else if !isEmpty {
return fmt.Errorf("cannot create %q as it already exists and is not empty", op.destination)
} else {
if err := os.RemoveAll(op.destination); err != nil {
return fmt.Errorf("Not able to delete %q", op.destination)
}
}
}
if err := os.MkdirAll(path, perm); err != nil {
return fmtError(err)
}
}
cache, err := op.project.CacheDirPath(jirix)
if err != nil {
return err
}
if !isPathDir(cache) {
cache = ""
}
if err := op.checkoutProject(jirix, cache); err != nil {
if op.destination != jirix.Root {
if err := os.RemoveAll(op.destination); err != nil {
jirix.Logger.Warningf("Not able to remove %q after create failed: %s", op.destination, err)
}
}
return err
}
return nil
}
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, updates *fsUpdates) error {
return nil
}
// deleteOperation represents the deletion of a project.
type deleteOperation struct {
commonOperation
}
func (op deleteOperation) Kind() string {
return "delete"
}
func (op deleteOperation) Run(jirix *jiri.X) error {
if op.project.LocalConfig.Ignore {
jirix.Logger.Warningf("Project %s(%s) won't be deleted due to it's local-config\n\n", op.project.Name, op.source)
return nil
}
// Never delete projects with non-master branches, uncommitted
// work, or untracked content.
scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path))
branches, _, err := scm.GetBranches()
if err != nil {
return fmt.Errorf("Cannot get branches for project %q: %s", op.Project().Name, err)
}
uncommitted, err := scm.HasUncommittedChanges()
if err != nil {
return fmt.Errorf("Cannot get uncommited changes for project %q: %s", op.Project().Name, err)
}
untracked, err := scm.HasUntrackedFiles()
if err != nil {
return fmt.Errorf("Cannot get untracked changes for project %q: %s", op.Project().Name, err)
}
extraBranches := false
for _, branch := range branches {
if !strings.Contains(branch, "HEAD detached") {
extraBranches = true
break
}
}
if extraBranches || uncommitted || untracked {
rmCommand := jirix.Color.Yellow("rm -rf %q", op.source)
unManageCommand := jirix.Color.Yellow("rm -rf %q", filepath.Join(op.source, jiri.ProjectMetaDir))
msg := ""
if extraBranches {
msg = fmt.Sprintf("Project %q won't be deleted as it contains branches", op.project.Name)
} else {
msg = fmt.Sprintf("Project %q won't be deleted as it might contain changes", op.project.Name)
}
msg += fmt.Sprintf("\nIf you no longer need it, invoke '%s'", rmCommand)
msg += fmt.Sprintf("\nIf you no longer want jiri to manage it, invoke '%s'\n\n", unManageCommand)
jirix.Logger.Warningf(msg)
return nil
}
if err := os.RemoveAll(op.source); err != nil {
return fmtError(err)
}
return removeEmptyParents(jirix, path.Dir(op.source))
}
func removeEmptyParents(jirix *jiri.X, dir string) error {
isEmpty := func(name string) (bool, error) {
f, err := os.Open(name)
if err != nil {
return false, err
}
defer f.Close()
_, err = f.Readdirnames(1)
if err == io.EOF {
// empty dir
return true, nil
}
if err != nil {
return false, err
}
return false, nil
}
if jirix.Root == dir || dir == "" || dir == "." {
return nil
}
empty, err := isEmpty(dir)
if err != nil {
return err
}
if empty {
if err := os.Remove(dir); err != nil {
return err
}
jirix.Logger.Debugf("gc deleted empty parent directory: %v", dir)
return removeEmptyParents(jirix, path.Dir(dir))
}
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, updates *fsUpdates) error {
if _, err := os.Stat(op.source); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("cannot delete %q as it does not exist", op.source)
}
return fmtError(err)
}
updates.deleteDir(op.source)
return nil
}
// moveOperation represents the relocation of a project.
type moveOperation struct {
commonOperation
rebaseTracked bool
rebaseUntracked bool
rebaseAll bool
snapshot bool
}
func (op moveOperation) Kind() string {
return "move"
}
func (op moveOperation) Run(jirix *jiri.X) error {
if op.project.LocalConfig.Ignore {
jirix.Logger.Warningf("Project %s(%s) won't be moved or updated due to it's local-config\n\n", op.project.Name, op.source)
return nil
}
// If it was nested project it might have been moved with its parent project
if op.source != op.destination {
path, perm := filepath.Dir(op.destination), os.FileMode(0755)
if err := os.MkdirAll(path, perm); err != nil {
return fmtError(err)
}
if err := osutil.Rename(op.source, op.destination); err != nil {
return fmtError(err)
}
}
if err := syncProjectMaster(jirix, op.project, op.state, op.rebaseTracked, op.rebaseUntracked, op.rebaseAll, op.snapshot); err != nil {
return err
}
return writeMetadata(jirix, op.project, op.project.Path)
}
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, updates *fsUpdates) error {
if _, err := os.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 fmtError(err)
}
if _, err := os.Stat(op.destination); err != nil {
if !os.IsNotExist(err) {
return fmtError(err)
}
} else {
return fmt.Errorf("cannot move %q to %q as the destination already exists", op.source, op.destination)
}
updates.deleteDir(op.source)
return nil
}
// changeRemoteOperation represents the chnage of remote URL
type changeRemoteOperation struct {
commonOperation
rebaseTracked bool
rebaseUntracked bool
rebaseAll bool
snapshot bool
}
func (op changeRemoteOperation) Kind() string {
return "change-remote"
}
func (op changeRemoteOperation) Run(jirix *jiri.X) error {
if op.project.LocalConfig.Ignore || op.project.LocalConfig.NoUpdate {
jirix.Logger.Warningf("Project %s(%s) won't be updated due to it's local-config. It has a changed remote\n\n", op.project.Name, op.project.Path)
return nil
}
git := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path))
tempRemote := "new-remote-origin"
if err := git.AddRemote(tempRemote, op.project.Remote); err != nil {
return err
}
defer git.DeleteRemote(tempRemote)
if err := fetch(jirix, op.project.Path, tempRemote); err != nil {
return err
}
// Check for all leaf commits in new remote
for _, branch := range op.state.Branches {
if containingBranches, err := git.GetRemoteBranchesContaining(branch.Revision); err != nil {
return err
} else {
foundBranch := false
for _, remoteBranchName := range containingBranches {
if strings.HasPrefix(remoteBranchName, tempRemote) {
foundBranch = true
break
}
}
if !foundBranch {
jirix.Logger.Errorf("Note: For project %q(%v), remote url has changed. Its branch %q is on a commit", op.project.Name, op.project.Path, branch.Name)
jirix.Logger.Errorf("which is not in new remote(%v). Please manually reset your branches or move", op.project.Remote)
jirix.Logger.Errorf("your project folder out of the root and try again")
return nil
}
}
}
// Everything ok, change the remote url
if err := git.SetRemoteUrl("origin", op.project.Remote); err != nil {
return err
}
if err := fetch(jirix, op.project.Path, "", gitutil.AllOpt(true), gitutil.PruneOpt(true)); err != nil {
return err
}
if err := syncProjectMaster(jirix, op.project, op.state, op.rebaseTracked, op.rebaseUntracked, op.rebaseAll, op.snapshot); err != nil {
return err
}
return writeMetadata(jirix, op.project, op.project.Path)
}
func (op changeRemoteOperation) String() string {
return fmt.Sprintf("Change remote of project %q to %q and update it to %q", op.project.Name, op.project.Remote, fmtRevision(op.project.Revision))
}
func (op changeRemoteOperation) Test(jirix *jiri.X, _ *fsUpdates) error {
return nil
}
// updateOperation represents the update of a project.
type updateOperation struct {
commonOperation
rebaseTracked bool
rebaseUntracked bool
rebaseAll bool
snapshot bool
}
func (op updateOperation) Kind() string {
return "update"
}
func (op updateOperation) Run(jirix *jiri.X) error {
if err := syncProjectMaster(jirix, op.project, op.state, op.rebaseTracked, op.rebaseUntracked, op.rebaseAll, op.snapshot); err != nil {
return err
}
return writeMetadata(jirix, op.project, op.project.Path)
}
func (op updateOperation) String() string {
return fmt.Sprintf("advance/rebase project %q located in %q to %q", op.project.Name, op.source, fmtRevision(op.project.Revision))
}
func (op updateOperation) Test(jirix *jiri.X, _ *fsUpdates) 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) Kind() string {
return "null"
}
func (op nullOperation) Run(jirix *jiri.X) error {
return writeMetadata(jirix, op.project, op.project.Path)
}
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, _ *fsUpdates) 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 path.
//
// 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 change-remote operations, which should happen before move
// 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.Kind() {
case "delete":
vals[idx] = 0
case "change-remote":
vals[idx] = 1
case "move":
vals[idx] = 2
case "create":
vals[idx] = 3
case "update":
vals[idx] = 4
case "null":
vals[idx] = 5
}
}
if vals[0] != vals[1] {
return vals[0] < vals[1]
}
if vals[0] == 0 {
// delete sub folder first
return ops[i].Project().Path+string(filepath.Separator) > ops[j].Project().Path+string(filepath.Separator)
} else {
return ops[i].Project().Path+string(filepath.Separator) < ops[j].Project().Path+string(filepath.Separator)
}
}
// 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, states map[ProjectKey]*ProjectState, gc, rebaseTracked, rebaseUntracked, rebaseAll, snapshot bool) operations {
result := operations{}
allProjects := map[ProjectKey]bool{}
for _, p := range localProjects {
allProjects[p.Key()] = true
}
for _, p := range remoteProjects {
allProjects[p.Key()] = true
}
for key, _ := range allProjects {
var local, remote *Project
var state *ProjectState
if project, ok := localProjects[key]; ok {
local = &project
}
if project, ok := remoteProjects[key]; ok {
// update remote local config
if local != nil {
project.LocalConfig = local.LocalConfig
remoteProjects[key] = project
}
remote = &project
}
if s, ok := states[key]; ok {
state = s
}
result = append(result, computeOp(local, remote, state, gc, rebaseTracked, rebaseUntracked, rebaseAll, snapshot))
}
sort.Sort(result)
return result
}
func computeOp(local, remote *Project, state *ProjectState, gc, rebaseTracked, rebaseUntracked, rebaseAll, snapshot bool) operation {
switch {
case local == nil && remote != nil:
return createOperation{commonOperation{
destination: remote.Path,
project: *remote,
source: "",
}}
case local != nil && remote == nil:
return deleteOperation{commonOperation{
destination: "",
project: *local,
source: local.Path,
}}
case local != nil && remote != nil:
localBranchesNeedUpdating := false
if !snapshot {
cb := state.CurrentBranch
if rebaseAll {
for _, branch := range state.Branches {
if branch.Tracking != nil {
if branch.Revision != branch.Tracking.Revision {
localBranchesNeedUpdating = true
break
}
} else if rebaseUntracked && rebaseAll {
// We put checks for untracked-branch updation in syncProjectMaster funtion
localBranchesNeedUpdating = true
break
}
}
} else if cb.Name != "" && cb.Tracking != nil && cb.Revision != cb.Tracking.Revision {
localBranchesNeedUpdating = true
}
}
switch {
case local.Remote != remote.Remote:
return changeRemoteOperation{commonOperation{
destination: remote.Path,
project: *remote,
source: local.Path,
state: *state,
}, rebaseTracked, rebaseUntracked, rebaseAll, snapshot}
case local.Path != remote.Path:
// moveOperation also does an update, so we don't need to check the
// revision here.
return moveOperation{commonOperation{
destination: remote.Path,
project: *remote,
source: local.Path,
state: *state,
}, rebaseTracked, rebaseUntracked, rebaseAll, snapshot}
case snapshot && local.Revision != remote.Revision:
return updateOperation{commonOperation{
destination: remote.Path,
project: *remote,
source: local.Path,
state: *state,
}, rebaseTracked, rebaseUntracked, rebaseAll, snapshot}
case localBranchesNeedUpdating || (state.CurrentBranch.Name == "" && local.Revision != remote.Revision):
return updateOperation{commonOperation{
destination: remote.Path,
project: *remote,
source: local.Path,
state: *state,
}, rebaseTracked, rebaseUntracked, rebaseAll, snapshot}
case state.CurrentBranch.Tracking == nil && local.Revision != remote.Revision:
return updateOperation{commonOperation{
destination: remote.Path,
project: *remote,
source: local.Path,
state: *state,
}, rebaseTracked, rebaseUntracked, rebaseAll, snapshot}
default:
return nullOperation{commonOperation{
destination: remote.Path,
project: *remote,
source: local.Path,
state: *state,
}}
}
default:
panic("jiri: computeOp called with nil local and remote")
}
}
// This function creates worktree and runs create operation in parallel
func runCreateOperations(jirix *jiri.X, ops []createOperation) MultiError {
count := len(ops)
if count == 0 {
return nil
}
type workTree struct {
// dir is the top level directory in which operations will be performed
dir string
// op is an ordered list of operations that must be performed serially,
// affecting dir
ops []operation
// after contains a tree of work that must be performed after ops
after map[string]*workTree
}
head := &workTree{
dir: "",
ops: []operation{},
after: make(map[string]*workTree),
}
for _, op := range ops {
node := head
parts := strings.Split(op.Project().Path, string(filepath.Separator))
// walk down the file path tree, creating any work tree nodes as required
for _, part := range parts {
if part == "" {
continue
}
next, ok := node.after[part]
if !ok {
next = &workTree{
dir: part,
ops: []operation{},
after: make(map[string]*workTree),
}
node.after[part] = next
}
node = next
}
node.ops = append(node.ops, op)
}
workQueue := make(chan *workTree, count)
errs := make(chan error, count)
var wg sync.WaitGroup
processTree := func(tree *workTree) {
defer wg.Done()
for _, op := range tree.ops {
logMsg := fmt.Sprintf("Creating project %q", op.Project().Name)
task := jirix.Logger.AddTaskMsg(logMsg)
jirix.Logger.Debugf("%v", op)
if err := op.Run(jirix); err != nil {
task.Done()
errs <- fmt.Errorf("%s: %s", logMsg, err)
return
}
task.Done()
}
for _, v := range tree.after {
wg.Add(1)
workQueue <- v
}
}
wg.Add(1)
workQueue <- head
for i := uint(0); i < jirix.Jobs; i++ {
go func() {
for tree := range workQueue {
processTree(tree)
}
}()
}
wg.Wait()
close(workQueue)
close(errs)
var multiErr MultiError
for err := range errs {
multiErr = append(multiErr, err)
}
return multiErr
}
type PathTrie struct {
current string
children map[string]*PathTrie
}
func NewPathTrie() *PathTrie {
return &PathTrie{
current: "",
children: make(map[string]*PathTrie),
}
}
func (p *PathTrie) Contains(path string) bool {
parts := strings.Split(path, string(filepath.Separator))
node := p
for _, part := range parts {
if part == "" {
continue
}
child, ok := node.children[part]
if !ok {
return false
}
node = child
}
return true
}
func (p *PathTrie) Insert(path string) {
parts := strings.Split(path, string(filepath.Separator))
node := p
for _, part := range parts {
if part == "" {
continue
}
child, ok := node.children[part]
if !ok {
child = &PathTrie{
current: part,
children: make(map[string]*PathTrie),
}
node.children[part] = child
}
node = child
}
}
func runDeleteOperations(jirix *jiri.X, ops []deleteOperation, gc bool) error {
if len(ops) == 0 {
return nil
}
notDeleted := NewPathTrie()
if !gc {
msg := fmt.Sprintf("%d project(s) is/are marked to be deleted. Run '%s' to delete them.", len(ops), jirix.Color.Yellow("jiri update -gc"))
if jirix.Logger.LoggerLevel < log.DebugLevel {
msg = fmt.Sprintf("%s\nOr run '%s' or '%s' to see the list of projects.", msg, jirix.Color.Yellow("jiri update -v"), jirix.Color.Yellow("jiri status -d"))
}
jirix.Logger.Warningf("%s\n\n", msg)
if jirix.Logger.LoggerLevel >= log.DebugLevel {
msg = "List of project(s) marked to be deleted:"
for _, op := range ops {
msg = fmt.Sprintf("%s\nName: %s, Path: '%s'", msg, jirix.Color.Yellow(op.project.Name), jirix.Color.Yellow(op.source))
}
jirix.Logger.Debugf("%s\n\n", msg)
}
return nil
}
for _, op := range ops {
if notDeleted.Contains(op.Project().Path) {
// not deleting project, add it to trie
notDeleted.Insert(op.source)
rmCommand := jirix.Color.Yellow("rm -rf %q", op.source)
msg := fmt.Sprintf("Project %q won't be deleted because of its sub project(s)", op.project.Name)
msg += fmt.Sprintf("\nIf you no longer need it, invoke '%s'\n\n", rmCommand)
jirix.Logger.Warningf(msg)
continue
}
logMsg := fmt.Sprintf("Deleting project %q", op.Project().Name)
task := jirix.Logger.AddTaskMsg(logMsg)
jirix.Logger.Debugf("%s", op)
if err := op.Run(jirix); err != nil {
task.Done()
return fmt.Errorf("%s: %s", logMsg, err)
}
task.Done()
if _, err := os.Stat(op.source); err == nil {
// project not deleted, add it to trie
notDeleted.Insert(op.source)
} else if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("Checking if %q exists", op.source)
}
}
return nil
}
func runMoveOperations(jirix *jiri.X, ops []moveOperation) error {
parentSrcPath := ""
parentDestPath := ""
for _, op := range ops {
if parentSrcPath != "" && strings.HasPrefix(op.source, parentSrcPath) {
op.source = filepath.Join(parentDestPath, strings.Replace(op.source, parentSrcPath, "", 1))
} else {
parentSrcPath = op.source
parentDestPath = op.destination
}
logMsg := fmt.Sprintf("Moving and updating project %q", op.Project().Name)
task := jirix.Logger.AddTaskMsg(logMsg)
jirix.Logger.Debugf("%s", op)
if err := op.Run(jirix); err != nil {
task.Done()
return fmt.Errorf("%s: %s", logMsg, err)
}
task.Done()
}
return nil
}
func runCommonOperations(jirix *jiri.X, ops operations, loglevel log.LogLevel) error {
for _, op := range ops {
logMsg := fmt.Sprintf("Updating project %q", op.Project().Name)
task := jirix.Logger.AddTaskMsg(logMsg)
jirix.Logger.Logf(loglevel, "%s", op)
if err := op.Run(jirix); err != nil {
task.Done()
return fmt.Errorf("%s: %s", logMsg, err)
}
task.Done()
}
return nil
}