A console script that allows you to easily update multiple git repositories at once
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

223 rader
7.5 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
  4. # See the LICENSE file for details.
  5. from __future__ import print_function
  6. import os
  7. from colorama import Fore, Style
  8. from git import Repo, exc
  9. __all__ = ["update_bookmarks", "update_directories"]
  10. BOLD = Style.BRIGHT
  11. RED = Fore.RED + BOLD
  12. GREEN = Fore.GREEN + BOLD
  13. BLUE = Fore.BLUE + BOLD
  14. RESET = Style.RESET_ALL
  15. INDENT1 = " " * 3
  16. INDENT2 = " " * 7
  17. ERROR = RED + "Error:" + RESET
  18. class _Stasher(object):
  19. """Manages the stash state of a given repository."""
  20. def __init__(self, repo):
  21. self._repo = repo
  22. self._clean = self._stashed = False
  23. def clean(self):
  24. """Ensure the working directory is clean, so we can do checkouts."""
  25. if not self._clean:
  26. res = self._repo.git.stash("--all")
  27. self._clean = True
  28. if res != "No local changes to save":
  29. self._stashed = True
  30. def restore(self):
  31. """Restore the pre-stash state."""
  32. if self._stashed:
  33. self._repo.git.stash("pop", "--index")
  34. def _read_config(repo, attr):
  35. """Read an attribute from git config."""
  36. try:
  37. return repo.git.config("--get", attr)
  38. except exc.GitCommandError:
  39. return None
  40. def _fetch_remote(remote):
  41. """Fetch a given remote, and display progress info along the way."""
  42. print(INDENT2, "Fetching", remote.name, end="...")
  43. remote.fetch() ### TODO: show progress
  44. print(" done.")
  45. def _is_up_to_date(repo, branch, upstream):
  46. """Return whether *branch* is up-to-date with its *upstream*."""
  47. base = repo.git.merge_base(branch.commit, upstream.commit)
  48. return repo.commit(base) == upstream.commit
  49. def _rebase(repo, name):
  50. """Rebase the current HEAD of *repo* onto the branch *name*."""
  51. print(" rebasing...", end="")
  52. try:
  53. res = repo.git.rebase(name)
  54. except exc.GitCommandError as err:
  55. msg = err.stderr.replace("\n", " ").strip()
  56. if "unstaged changes" in msg:
  57. print(" error:", "unstaged changes.")
  58. elif "uncommitted changes" in msg:
  59. print(" error:", "uncommitted changes.")
  60. else:
  61. try:
  62. repo.git.rebase("--abort")
  63. except exc.GitCommandError:
  64. pass
  65. print(" error:", msg if msg else "rebase conflict")
  66. else:
  67. print(" done.")
  68. def _merge(repo, name):
  69. """Merge the branch *name* into the current HEAD of *repo*."""
  70. print(" merging...", end="")
  71. try:
  72. repo.git.merge(name)
  73. except exc.GitCommandError as err:
  74. msg = err.stderr.replace("\n", " ").strip()
  75. if "local changes" in msg and "would be overwritten" in msg:
  76. print(" error:", "uncommitted changes.")
  77. else:
  78. try:
  79. repo.git.merge("--abort")
  80. except exc.GitCommandError:
  81. pass
  82. print(" error:", msg if msg else "merge conflict")
  83. else:
  84. print(" done.")
  85. def _update_branch(repo, branch, merge, rebase, stasher=None):
  86. """Update a single branch."""
  87. print(INDENT2, "Updating", branch, end="...")
  88. upstream = branch.tracking_branch()
  89. if not upstream:
  90. print(" skipped: no upstream is tracked.")
  91. return
  92. try:
  93. branch.commit, upstream.commit
  94. except ValueError:
  95. print(" skipped: branch contains no revisions.")
  96. return
  97. if _is_up_to_date(repo, branch, upstream):
  98. print(" up to date.")
  99. return
  100. if stasher:
  101. stasher.clean()
  102. branch.checkout()
  103. config_attr = "branch.{0}.rebase".format(branch.name)
  104. if not merge and (rebase or _read_config(repo, config_attr)):
  105. _rebase(repo, upstream.name)
  106. else:
  107. _merge(repo, upstream.name)
  108. def _update_repository(repo, current_only=False, rebase=False, merge=False,
  109. verbose=False):
  110. """Update a single git repository by fetching remotes and rebasing/merging.
  111. The specific actions depend on the arguments given. We will fetch all
  112. remotes if *current_only* is ``False``, or only the remote tracked by the
  113. current branch if ``True``. By default, we will merge unless
  114. ``pull.rebase`` or ``branch.<name>.rebase`` is set in config; *rebase* will
  115. cause us to always rebase with ``--preserve-merges``, and *merge* will
  116. cause us to always merge. If *verbose* is set, additional information is
  117. printed out for the user.
  118. """
  119. print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":")
  120. active = repo.active_branch
  121. if current_only:
  122. ref = active.tracking_branch()
  123. if not ref:
  124. print(INDENT2, ERROR, "no remote tracked by current branch.")
  125. return
  126. remotes = [repo.remotes[ref.remote_name]]
  127. else:
  128. remotes = repo.remotes
  129. if not remotes:
  130. print(INDENT2, ERROR, "no remotes configured to pull from.")
  131. return
  132. for remote in remotes:
  133. _fetch_remote(remote)
  134. rebase = rebase or _read_config(repo, "pull.rebase")
  135. _update_branch(repo, active, merge, rebase)
  136. branches = set(repo.heads) - {active}
  137. if branches:
  138. stasher = _Stasher(repo)
  139. try:
  140. for branch in sorted(branches, key=lambda b: b.name):
  141. _update_branch(repo, branch, merge, rebase, stasher)
  142. finally:
  143. active.checkout()
  144. stasher.restore()
  145. def _update_subdirectories(path, long_name, update_args):
  146. """Update all subdirectories that are git repos in a given directory."""
  147. repos = []
  148. for item in os.listdir(path):
  149. try:
  150. repo = Repo(os.path.join(path, item))
  151. except (exc.InvalidGitRepositoryError, exc.NoSuchPathError):
  152. continue
  153. repos.append(repo)
  154. suffix = "ies" if len(repos) != 1 else "y"
  155. print(long_name[0].upper() + long_name[1:],
  156. "contains {0} git repositor{1}:".format(len(repos), suffix))
  157. for repo in sorted(repos, key=lambda r: os.path.split(r.working_dir)[1]):
  158. _update_repository(repo, *update_args)
  159. def _update_directory(path, update_args, is_bookmark=False):
  160. """Update a particular directory.
  161. Determine whether the directory is a git repo on its own, a directory of
  162. git repositories, or something invalid. If the first, update the single
  163. repository; if the second, update all repositories contained within; if the
  164. third, print an error.
  165. """
  166. dir_type = "bookmark" if is_bookmark else "directory"
  167. long_name = dir_type + ' "' + BOLD + path + RESET + '"'
  168. try:
  169. repo = Repo(path)
  170. except exc.NoSuchPathError:
  171. print(ERROR, long_name, "doesn't exist!")
  172. except exc.InvalidGitRepositoryError:
  173. if os.path.isdir(path):
  174. _update_subdirectories(path, long_name, update_args)
  175. else:
  176. print(ERROR, long_name, "isn't a repository!")
  177. else:
  178. long_name = (dir_type.capitalize() + ' "' + BOLD + repo.working_dir +
  179. RESET + '"')
  180. print(long_name, "is a git repository:")
  181. _update_repository(repo, *update_args)
  182. def update_bookmarks(bookmarks, update_args):
  183. """Loop through and update all bookmarks."""
  184. if bookmarks:
  185. for path, name in bookmarks:
  186. _update_directory(path, update_args, is_bookmark=True)
  187. else:
  188. print("You don't have any bookmarks configured! Get help with 'gitup -h'.")
  189. def update_directories(paths, update_args):
  190. """Update a list of directories supplied by command arguments."""
  191. for path in paths:
  192. full_path = os.path.abspath(path)
  193. _update_directory(full_path, update_args, is_bookmark=False)