git.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. package vcs
  2. import (
  3. "bytes"
  4. "encoding/xml"
  5. "io/ioutil"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "runtime"
  10. "strings"
  11. "time"
  12. )
  13. // NewGitRepo creates a new instance of GitRepo. The remote and local directories
  14. // need to be passed in.
  15. func NewGitRepo(remote, local string) (*GitRepo, error) {
  16. ins := depInstalled("git")
  17. if !ins {
  18. return nil, NewLocalError("git is not installed", nil, "")
  19. }
  20. ltype, err := DetectVcsFromFS(local)
  21. // Found a VCS other than Git. Need to report an error.
  22. if err == nil && ltype != Git {
  23. return nil, ErrWrongVCS
  24. }
  25. r := &GitRepo{}
  26. r.setRemote(remote)
  27. r.setLocalPath(local)
  28. r.RemoteLocation = "origin"
  29. r.Logger = Logger
  30. // Make sure the local Git repo is configured the same as the remote when
  31. // A remote value was passed in.
  32. if err == nil && r.CheckLocal() {
  33. c := exec.Command("git", "config", "--get", "remote.origin.url")
  34. c.Dir = local
  35. c.Env = envForDir(c.Dir)
  36. out, err := c.CombinedOutput()
  37. if err != nil {
  38. return nil, NewLocalError("Unable to retrieve local repo information", err, string(out))
  39. }
  40. localRemote := strings.TrimSpace(string(out))
  41. if remote != "" && localRemote != remote {
  42. return nil, ErrWrongRemote
  43. }
  44. // If no remote was passed in but one is configured for the locally
  45. // checked out Git repo use that one.
  46. if remote == "" && localRemote != "" {
  47. r.setRemote(localRemote)
  48. }
  49. }
  50. return r, nil
  51. }
  52. // GitRepo implements the Repo interface for the Git source control.
  53. type GitRepo struct {
  54. base
  55. RemoteLocation string
  56. }
  57. // Vcs retrieves the underlying VCS being implemented.
  58. func (s GitRepo) Vcs() Type {
  59. return Git
  60. }
  61. // Get is used to perform an initial clone of a repository.
  62. func (s *GitRepo) Get() error {
  63. out, err := s.run("git", "clone", "--recursive", "--", s.Remote(), s.LocalPath())
  64. // There are some windows cases where Git cannot create the parent directory,
  65. // if it does not already exist, to the location it's trying to create the
  66. // repo. Catch that error and try to handle it.
  67. if err != nil && s.isUnableToCreateDir(err) {
  68. basePath := filepath.Dir(filepath.FromSlash(s.LocalPath()))
  69. if _, err := os.Stat(basePath); os.IsNotExist(err) {
  70. err = os.MkdirAll(basePath, 0755)
  71. if err != nil {
  72. return NewLocalError("Unable to create directory", err, "")
  73. }
  74. out, err = s.run("git", "clone", "--recursive", "--", s.Remote(), s.LocalPath())
  75. if err != nil {
  76. return NewRemoteError("Unable to get repository", err, string(out))
  77. }
  78. return err
  79. }
  80. } else if err != nil {
  81. return NewRemoteError("Unable to get repository", err, string(out))
  82. }
  83. return nil
  84. }
  85. // Init initializes a git repository at local location.
  86. func (s *GitRepo) Init() error {
  87. out, err := s.run("git", "init", "--", s.LocalPath())
  88. // There are some windows cases where Git cannot create the parent directory,
  89. // if it does not already exist, to the location it's trying to create the
  90. // repo. Catch that error and try to handle it.
  91. if err != nil && s.isUnableToCreateDir(err) {
  92. basePath := filepath.Dir(filepath.FromSlash(s.LocalPath()))
  93. if _, err := os.Stat(basePath); os.IsNotExist(err) {
  94. err = os.MkdirAll(basePath, 0755)
  95. if err != nil {
  96. return NewLocalError("Unable to initialize repository", err, "")
  97. }
  98. out, err = s.run("git", "init", "--", s.LocalPath())
  99. if err != nil {
  100. return NewLocalError("Unable to initialize repository", err, string(out))
  101. }
  102. return nil
  103. }
  104. } else if err != nil {
  105. return NewLocalError("Unable to initialize repository", err, string(out))
  106. }
  107. return nil
  108. }
  109. // Update performs an Git fetch and pull to an existing checkout.
  110. func (s *GitRepo) Update() error {
  111. // Perform a fetch to make sure everything is up to date.
  112. out, err := s.RunFromDir("git", "fetch", "--tags", "--", s.RemoteLocation)
  113. if err != nil {
  114. return NewRemoteError("Unable to update repository", err, string(out))
  115. }
  116. // When in a detached head state, such as when an individual commit is checked
  117. // out do not attempt a pull. It will cause an error.
  118. detached, err := isDetachedHead(s.LocalPath())
  119. if err != nil {
  120. return NewLocalError("Unable to update repository", err, "")
  121. }
  122. if detached {
  123. return nil
  124. }
  125. out, err = s.RunFromDir("git", "pull")
  126. if err != nil {
  127. return NewRemoteError("Unable to update repository", err, string(out))
  128. }
  129. return s.defendAgainstSubmodules()
  130. }
  131. // UpdateVersion sets the version of a package currently checked out via Git.
  132. func (s *GitRepo) UpdateVersion(version string) error {
  133. out, err := s.RunFromDir("git", "checkout", version)
  134. if err != nil {
  135. return NewLocalError("Unable to update checked out version", err, string(out))
  136. }
  137. return s.defendAgainstSubmodules()
  138. }
  139. // defendAgainstSubmodules tries to keep repo state sane in the event of
  140. // submodules. Or nested submodules. What a great idea, submodules.
  141. func (s *GitRepo) defendAgainstSubmodules() error {
  142. // First, update them to whatever they should be, if there should happen to be any.
  143. out, err := s.RunFromDir("git", "submodule", "update", "--init", "--recursive")
  144. if err != nil {
  145. return NewLocalError("Unexpected error while defensively updating submodules", err, string(out))
  146. }
  147. // Now, do a special extra-aggressive clean in case changing versions caused
  148. // one or more submodules to go away.
  149. out, err = s.RunFromDir("git", "clean", "-x", "-d", "-f", "-f")
  150. if err != nil {
  151. return NewLocalError("Unexpected error while defensively cleaning up after possible derelict submodule directories", err, string(out))
  152. }
  153. // Then, repeat just in case there are any nested submodules that went away.
  154. out, err = s.RunFromDir("git", "submodule", "foreach", "--recursive", "git clean -x -d -f -f")
  155. if err != nil {
  156. return NewLocalError("Unexpected error while defensively cleaning up after possible derelict nested submodule directories", err, string(out))
  157. }
  158. return nil
  159. }
  160. // Version retrieves the current version.
  161. func (s *GitRepo) Version() (string, error) {
  162. out, err := s.RunFromDir("git", "rev-parse", "HEAD")
  163. if err != nil {
  164. return "", NewLocalError("Unable to retrieve checked out version", err, string(out))
  165. }
  166. return strings.TrimSpace(string(out)), nil
  167. }
  168. // Current returns the current version-ish. This means:
  169. // * Branch name if on the tip of the branch
  170. // * Tag if on a tag
  171. // * Otherwise a revision id
  172. func (s *GitRepo) Current() (string, error) {
  173. out, err := s.RunFromDir("git", "symbolic-ref", "HEAD")
  174. if err == nil {
  175. o := bytes.TrimSpace(bytes.TrimPrefix(out, []byte("refs/heads/")))
  176. return string(o), nil
  177. }
  178. v, err := s.Version()
  179. if err != nil {
  180. return "", err
  181. }
  182. ts, err := s.TagsFromCommit(v)
  183. if err != nil {
  184. return "", err
  185. }
  186. if len(ts) > 0 {
  187. return ts[0], nil
  188. }
  189. return v, nil
  190. }
  191. // Date retrieves the date on the latest commit.
  192. func (s *GitRepo) Date() (time.Time, error) {
  193. out, err := s.RunFromDir("git", "log", "-1", "--date=iso", "--pretty=format:%cd")
  194. if err != nil {
  195. return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
  196. }
  197. t, err := time.Parse(longForm, string(out))
  198. if err != nil {
  199. return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
  200. }
  201. return t, nil
  202. }
  203. // Branches returns a list of available branches on the RemoteLocation
  204. func (s *GitRepo) Branches() ([]string, error) {
  205. out, err := s.RunFromDir("git", "show-ref")
  206. if err != nil {
  207. return []string{}, NewLocalError("Unable to retrieve branches", err, string(out))
  208. }
  209. branches := s.referenceList(string(out), `(?m-s)(?:`+s.RemoteLocation+`)/(\S+)$`)
  210. return branches, nil
  211. }
  212. // Tags returns a list of available tags on the RemoteLocation
  213. func (s *GitRepo) Tags() ([]string, error) {
  214. out, err := s.RunFromDir("git", "show-ref")
  215. if err != nil {
  216. return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
  217. }
  218. tags := s.referenceList(string(out), `(?m-s)(?:tags)/(\S+)$`)
  219. return tags, nil
  220. }
  221. // CheckLocal verifies the local location is a Git repo.
  222. func (s *GitRepo) CheckLocal() bool {
  223. if _, err := os.Stat(s.LocalPath() + "/.git"); err == nil {
  224. return true
  225. }
  226. return false
  227. }
  228. // IsReference returns if a string is a reference. A reference can be a
  229. // commit id, branch, or tag.
  230. func (s *GitRepo) IsReference(r string) bool {
  231. _, err := s.RunFromDir("git", "rev-parse", "--verify", r)
  232. if err == nil {
  233. return true
  234. }
  235. // Some refs will fail rev-parse. For example, a remote branch that has
  236. // not been checked out yet. This next step should pickup the other
  237. // possible references.
  238. _, err = s.RunFromDir("git", "show-ref", r)
  239. return err == nil
  240. }
  241. // IsDirty returns if the checkout has been modified from the checked
  242. // out reference.
  243. func (s *GitRepo) IsDirty() bool {
  244. out, err := s.RunFromDir("git", "diff")
  245. return err != nil || len(out) != 0
  246. }
  247. // CommitInfo retrieves metadata about a commit.
  248. func (s *GitRepo) CommitInfo(id string) (*CommitInfo, error) {
  249. fm := `--pretty=format:"<logentry><commit>%H</commit><author>%an &lt;%ae&gt;</author><date>%aD</date><message>%s</message></logentry>"`
  250. out, err := s.RunFromDir("git", "log", id, fm, "-1")
  251. if err != nil {
  252. return nil, ErrRevisionUnavailable
  253. }
  254. cis := struct {
  255. Commit string `xml:"commit"`
  256. Author string `xml:"author"`
  257. Date string `xml:"date"`
  258. Message string `xml:"message"`
  259. }{}
  260. err = xml.Unmarshal(out, &cis)
  261. if err != nil {
  262. return nil, NewLocalError("Unable to retrieve commit information", err, string(out))
  263. }
  264. t, err := time.Parse("Mon, _2 Jan 2006 15:04:05 -0700", cis.Date)
  265. if err != nil {
  266. return nil, NewLocalError("Unable to retrieve commit information", err, string(out))
  267. }
  268. ci := &CommitInfo{
  269. Commit: cis.Commit,
  270. Author: cis.Author,
  271. Date: t,
  272. Message: cis.Message,
  273. }
  274. return ci, nil
  275. }
  276. // TagsFromCommit retrieves tags from a commit id.
  277. func (s *GitRepo) TagsFromCommit(id string) ([]string, error) {
  278. // This is imperfect and a better method would be great.
  279. var re []string
  280. out, err := s.RunFromDir("git", "show-ref", "-d")
  281. if err != nil {
  282. return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
  283. }
  284. lines := strings.Split(string(out), "\n")
  285. var list []string
  286. for _, i := range lines {
  287. if strings.HasPrefix(strings.TrimSpace(i), id) {
  288. list = append(list, i)
  289. }
  290. }
  291. tags := s.referenceList(strings.Join(list, "\n"), `(?m-s)(?:tags)/(\S+)$`)
  292. for _, t := range tags {
  293. // Dereferenced tags have ^{} appended to them.
  294. re = append(re, strings.TrimSuffix(t, "^{}"))
  295. }
  296. return re, nil
  297. }
  298. // Ping returns if remote location is accessible.
  299. func (s *GitRepo) Ping() bool {
  300. c := exec.Command("git", "ls-remote", s.Remote())
  301. // If prompted for a username and password, which GitHub does for all things
  302. // not public, it's considered not available. To make it available the
  303. // remote needs to be different.
  304. c.Env = mergeEnvLists([]string{"GIT_TERMINAL_PROMPT=0"}, os.Environ())
  305. _, err := c.CombinedOutput()
  306. return err == nil
  307. }
  308. // EscapePathSeparator escapes the path separator by replacing it with several.
  309. // Note: this is harmless on Unix, and needed on Windows.
  310. func EscapePathSeparator(path string) string {
  311. switch runtime.GOOS {
  312. case `windows`:
  313. // On Windows, triple all path separators.
  314. // Needed to escape backslash(s) preceding doublequotes,
  315. // because of how Windows strings treats backslash+doublequote combo,
  316. // and Go seems to be implicitly passing around a doublequoted string on Windows,
  317. // so we cannot use default string instead.
  318. // See: https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
  319. // e.g., C:\foo\bar\ -> C:\\\foo\\\bar\\\
  320. // used with --prefix, like this: --prefix=C:\foo\bar\ -> --prefix=C:\\\foo\\\bar\\\
  321. return strings.Replace(path,
  322. string(os.PathSeparator),
  323. string(os.PathSeparator)+string(os.PathSeparator)+string(os.PathSeparator),
  324. -1)
  325. default:
  326. return path
  327. }
  328. }
  329. // ExportDir exports the current revision to the passed in directory.
  330. func (s *GitRepo) ExportDir(dir string) error {
  331. var path string
  332. // Without the trailing / there can be problems.
  333. if !strings.HasSuffix(dir, string(os.PathSeparator)) {
  334. dir = dir + string(os.PathSeparator)
  335. }
  336. // checkout-index on some systems, such as some Windows cases, does not
  337. // create the parent directory to export into if it does not exist. Explicitly
  338. // creating it.
  339. err := os.MkdirAll(dir, 0755)
  340. if err != nil {
  341. return NewLocalError("Unable to create directory", err, "")
  342. }
  343. path = EscapePathSeparator(dir)
  344. out, err := s.RunFromDir("git", "checkout-index", "-f", "-a", "--prefix="+path)
  345. s.log(out)
  346. if err != nil {
  347. return NewLocalError("Unable to export source", err, string(out))
  348. }
  349. // and now, the horror of submodules
  350. handleSubmodules(s, dir)
  351. s.log(out)
  352. if err != nil {
  353. return NewLocalError("Error while exporting submodule sources", err, string(out))
  354. }
  355. return nil
  356. }
  357. // isDetachedHead will detect if git repo is in "detached head" state.
  358. func isDetachedHead(dir string) (bool, error) {
  359. p := filepath.Join(dir, ".git", "HEAD")
  360. contents, err := ioutil.ReadFile(p)
  361. if err != nil {
  362. return false, err
  363. }
  364. contents = bytes.TrimSpace(contents)
  365. if bytes.HasPrefix(contents, []byte("ref: ")) {
  366. return false, nil
  367. }
  368. return true, nil
  369. }
  370. // isUnableToCreateDir checks for an error in Init() to see if an error
  371. // where the parent directory of the VCS local path doesn't exist. This is
  372. // done in a multi-lingual manner.
  373. func (s *GitRepo) isUnableToCreateDir(err error) bool {
  374. msg := err.Error()
  375. if strings.HasPrefix(msg, "could not create work tree dir") ||
  376. strings.HasPrefix(msg, "不能创建工作区目录") ||
  377. strings.HasPrefix(msg, "no s'ha pogut crear el directori d'arbre de treball") ||
  378. strings.HasPrefix(msg, "impossible de créer le répertoire de la copie de travail") ||
  379. strings.HasPrefix(msg, "kunde inte skapa arbetskatalogen") ||
  380. (strings.HasPrefix(msg, "Konnte Arbeitsverzeichnis") && strings.Contains(msg, "nicht erstellen")) ||
  381. (strings.HasPrefix(msg, "작업 디렉터리를") && strings.Contains(msg, "만들 수 없습니다")) {
  382. return true
  383. }
  384. return false
  385. }