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 }