PageRenderTime 45ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/worker/uniter/charm/git.go

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