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