github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/worker/uniter/charm/git.go (about)

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