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 }