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 }