launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/worker/uniter/charm/git.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charm
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"launchpad.net/errgo/errors"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	"launchpad.net/juju-core/charm"
    17  	"launchpad.net/juju-core/log"
    18  	"launchpad.net/juju-core/utils"
    19  )
    20  
    21  var ErrConflict = errors.New("charm upgrade has conflicts")
    22  
    23  // GitDir exposes a specialized subset of git operations on a directory.
    24  type GitDir struct {
    25  	path string
    26  }
    27  
    28  // NewGitDir creates a new GitDir at path. It does not touch the filesystem.
    29  func NewGitDir(path string) *GitDir {
    30  	return &GitDir{path}
    31  }
    32  
    33  // Path returns the directory path.
    34  func (d *GitDir) Path() string {
    35  	return d.path
    36  }
    37  
    38  // Exists returns true if the directory exists.
    39  func (d *GitDir) Exists() (bool, error) {
    40  	fi, err := os.Stat(d.path)
    41  	if err != nil {
    42  		if os.IsNotExist(err) {
    43  			return false, nil
    44  		}
    45  		return false, err
    46  	}
    47  	if fi.IsDir() {
    48  		return true, nil
    49  	}
    50  	return false, errors.Newf("%q is not a directory", d.path)
    51  }
    52  
    53  // Init ensures that a git repository exists in the directory.
    54  func (d *GitDir) Init() error {
    55  	if err := os.MkdirAll(d.path, 0755); err != nil {
    56  		return mask(err)
    57  	}
    58  	commands := [][]string{
    59  		{"init"},
    60  		{"config", "user.email", "juju@localhost"},
    61  		{"config", "user.name", "juju"},
    62  	}
    63  	for _, args := range commands {
    64  		if err := d.cmd(args...); err != nil {
    65  			return mask(err)
    66  		}
    67  	}
    68  	return nil
    69  }
    70  
    71  // AddAll ensures that the next commit will reflect the current contents of
    72  // the directory. Empty directories will be preserved by inserting and tracking
    73  // empty files named .empty.
    74  func (d *GitDir) AddAll() error {
    75  	walker := func(path string, fi os.FileInfo, err error) error {
    76  		if err != nil {
    77  			return mask(err)
    78  		}
    79  		if !fi.IsDir() {
    80  			return nil
    81  		}
    82  		f, err := os.Open(path)
    83  		if err != nil {
    84  			return mask(err)
    85  		}
    86  		defer f.Close()
    87  		if _, err := f.Readdir(1); err != nil {
    88  			if errors.Cause(err) == io.EOF {
    89  				empty := filepath.Join(path, ".empty")
    90  				return ioutil.WriteFile(empty, nil, 0644)
    91  			}
    92  			return err
    93  		}
    94  		return nil
    95  	}
    96  	if err := filepath.Walk(d.path, walker); err != nil {
    97  		return mask(err)
    98  	}
    99  
   100  	// special handling for addall, since there is an error condition that
   101  	// we need to suppress
   102  	return d.addAll()
   103  }
   104  
   105  // addAll runs "git add -A ."" and swallows errors about no matching files. This
   106  // is to replicate the behavior of older versions of git that returned no error
   107  // in that situation.
   108  func (d *GitDir) addAll() error {
   109  	args := []string{"add", "-A", "."}
   110  	cmd := exec.Command("git", args...)
   111  	cmd.Dir = d.path
   112  	if out, err := cmd.CombinedOutput(); err != nil {
   113  		output := string(out)
   114  		// Swallow this specific error. It's a change in behavior from older
   115  		// versions of git, and we want AddAll to be able to be used on empty
   116  		// directories.
   117  		if !strings.Contains(output, "pathspec '.' did not match any files") {
   118  			return d.logError(err, string(out), args...)
   119  		}
   120  	}
   121  	return nil
   122  }
   123  
   124  // Commitf commits a new revision to the repository with the supplied message.
   125  func (d *GitDir) Commitf(format string, args ...interface{}) error {
   126  	return d.cmd("commit", "--allow-empty", "-m", fmt.Sprintf(format, args...))
   127  }
   128  
   129  // Snapshotf adds all changes made since the last commit, including deletions
   130  // and empty directories, and commits them using the supplied message.
   131  func (d *GitDir) Snapshotf(format string, args ...interface{}) error {
   132  	if err := d.AddAll(); err != nil {
   133  		return mask(err)
   134  	}
   135  	return d.Commitf(format, args...)
   136  }
   137  
   138  // Clone creates a new GitDir at the specified path, with history cloned
   139  // from the existing GitDir. It does not check out any files.
   140  func (d *GitDir) Clone(path string) (*GitDir, error) {
   141  	if err := d.cmd("clone", "--no-checkout", ".", path); err != nil {
   142  		return nil, mask(err)
   143  	}
   144  	return &GitDir{path}, nil
   145  }
   146  
   147  // Pull pulls from the supplied GitDir.
   148  func (d *GitDir) Pull(src *GitDir) error {
   149  	err := d.cmd("pull", src.path)
   150  	if err != nil {
   151  		if conflicted, e := d.Conflicted(); e == nil && conflicted {
   152  			return ErrConflict
   153  		}
   154  	}
   155  	return err
   156  }
   157  
   158  // Dirty returns true if the directory contains any uncommitted local changes.
   159  func (d *GitDir) Dirty() (bool, error) {
   160  	statuses, err := d.statuses()
   161  	if err != nil {
   162  		return false, mask(err)
   163  	}
   164  	return len(statuses) != 0, nil
   165  }
   166  
   167  // Conflicted returns true if the directory contains any conflicts.
   168  func (d *GitDir) Conflicted() (bool, error) {
   169  	statuses, err := d.statuses()
   170  	if err != nil {
   171  		return false, mask(err)
   172  	}
   173  	for _, st := range statuses {
   174  		switch st {
   175  		case "AA", "DD", "UU", "AU", "UA", "DU", "UD":
   176  			return true, nil
   177  		}
   178  	}
   179  	return false, nil
   180  }
   181  
   182  // Revert removes unversioned files and reverts everything else to its state
   183  // as of the most recent commit.
   184  func (d *GitDir) Revert() error {
   185  	if err := d.cmd("reset", "--hard", "ORIG_HEAD"); err != nil {
   186  		return mask(err)
   187  	}
   188  	return d.cmd("clean", "-f", "-f", "-d")
   189  }
   190  
   191  // Log returns a highly compacted history of the directory.
   192  func (d *GitDir) Log() ([]string, error) {
   193  	cmd := exec.Command("git", "--no-pager", "log", "--oneline")
   194  	cmd.Dir = d.path
   195  	out, err := cmd.Output()
   196  	if err != nil {
   197  		return nil, mask(err)
   198  	}
   199  	trim := strings.TrimRight(string(out), "\n")
   200  	return strings.Split(trim, "\n"), nil
   201  }
   202  
   203  // cmd runs the specified command inside the directory. Errors will be logged
   204  // in detail.
   205  func (d *GitDir) cmd(args ...string) error {
   206  	cmd := exec.Command("git", args...)
   207  	cmd.Dir = d.path
   208  	if out, err := cmd.CombinedOutput(); err != nil {
   209  		return d.logError(err, string(out), args...)
   210  	}
   211  	return nil
   212  }
   213  
   214  func (d *GitDir) logError(err error, output string, args ...string) error {
   215  	log.Errorf("worker/uniter/charm: git command failed: %s\npath: %s\nargs: %#v\n%s",
   216  		err, d.path, args, output)
   217  	return errors.Newf("git %s failed: %s", args[0], err)
   218  }
   219  
   220  // statuses returns a list of XY-coded git statuses for the files in the directory.
   221  func (d *GitDir) statuses() ([]string, error) {
   222  	cmd := exec.Command("git", "status", "--porcelain")
   223  	cmd.Dir = d.path
   224  	out, err := cmd.Output()
   225  	if err != nil {
   226  		return nil, errors.Notef(err, "git status failed")
   227  	}
   228  	statuses := []string{}
   229  	for _, line := range strings.Split(string(out), "\n") {
   230  		if line != "" {
   231  			statuses = append(statuses, line[:2])
   232  		}
   233  	}
   234  	return statuses, nil
   235  }
   236  
   237  // ReadCharmURL reads the charm identity file from the supplied GitDir.
   238  func ReadCharmURL(d *GitDir) (*charm.URL, error) {
   239  	path := filepath.Join(d.path, ".juju-charm")
   240  	surl := ""
   241  	if err := utils.ReadYaml(path, &surl); err != nil {
   242  		return nil, mask(err, os.IsNotExist)
   243  	}
   244  	return charm.ParseURL(surl)
   245  }
   246  
   247  // WriteCharmURL writes a charm identity file into the directory.
   248  func WriteCharmURL(d *GitDir, url *charm.URL) error {
   249  	return utils.WriteYaml(filepath.Join(d.path, ".juju-charm"), url.String())
   250  }