A console script that allows you to easily update multiple git repositories at once
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

10 年前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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 RemoteReference as RemoteRef, Repo, exc
  9. from git.util import RemoteProgress
  10. __all__ = ["update_bookmarks", "update_directories"]
  11. BOLD = Style.BRIGHT
  12. RED = Fore.RED + BOLD
  13. GREEN = Fore.GREEN + BOLD
  14. BLUE = Fore.BLUE + BOLD
  15. RESET = Style.RESET_ALL
  16. INDENT1 = " " * 3
  17. INDENT2 = " " * 7
  18. ERROR = RED + "Error:" + RESET
  19. class _ProgressMonitor(RemoteProgress):
  20. """Displays relevant output during the fetching process."""
  21. def __init__(self):
  22. super(_ProgressMonitor, self).__init__()
  23. def update(self, op_code, cur_count, max_count=None, message=''):
  24. """Called whenever progress changes. Overrides default behavior."""
  25. if op_code & self.COUNTING:
  26. print(" ({0})".format(cur_count), end="")
  27. elif op_code & (self.COMPRESSING | self.RECEIVING):
  28. if op_code & self.BEGIN:
  29. print("\b, ", end="")
  30. if op_code & self.END:
  31. end = ")"
  32. else:
  33. end = "\b" * (1 + len(cur_count) + len(max_count))
  34. print("{0}/{1}".format(cur_count, max_count), end=end)
  35. class _Stasher(object):
  36. """Manages the stash state of a given repository."""
  37. def __init__(self, repo):
  38. self._repo = repo
  39. self._clean = self._stashed = False
  40. def clean(self):
  41. """Ensure the working directory is clean, so we can do checkouts."""
  42. if not self._clean:
  43. res = self._repo.git.stash("--all")
  44. self._clean = True
  45. if res != "No local changes to save":
  46. self._stashed = True
  47. def restore(self):
  48. """Restore the pre-stash state."""
  49. if self._stashed:
  50. self._repo.git.stash("pop", "--index")
  51. def _read_config(repo, attr):
  52. """Read an attribute from git config."""
  53. try:
  54. return repo.git.config("--get", attr)
  55. except exc.GitCommandError:
  56. return None
  57. def _fetch_remotes(remotes):
  58. """Fetch a list of remotes, displaying progress info along the way."""
  59. def _get_name(ref):
  60. """Return the local name of a remote or tag reference."""
  61. return ref.remote_head if isinstance(ref, RemoteRef) else ref.name
  62. info = [("NEW_HEAD", "new branch", "new branches"),
  63. ("NEW_TAG", "new tag", "new tags"),
  64. ("FAST_FORWARD", "branch update", "branch updates"),
  65. ("ERROR", "error", "errors")]
  66. up_to_date = BLUE + "up to date" + RESET
  67. for remote in remotes:
  68. print(INDENT2, "Fetching", BOLD + remote.name, end="")
  69. results = remote.fetch(progress=_ProgressMonitor())
  70. rlist = []
  71. for attr, singular, plural in info:
  72. names = [_get_name(res.ref)
  73. for res in results if res.flags & getattr(res, attr)]
  74. if names:
  75. desc = singular if len(names) == 1 else plural
  76. colored = GREEN + desc + RESET
  77. rlist.append("{0} ({1})".format(colored, ", ".join(names)))
  78. print(":", (", ".join(rlist) if rlist else up_to_date) + ".")
  79. def _is_up_to_date(repo, branch, upstream):
  80. """Return whether *branch* is up-to-date with its *upstream*."""
  81. base = repo.git.merge_base(branch.commit, upstream.commit)
  82. return repo.commit(base) == upstream.commit
  83. def _rebase(repo, name):
  84. """Rebase the current HEAD of *repo* onto the branch *name*."""
  85. print(" rebasing...", end="")
  86. try:
  87. res = repo.git.rebase(name)
  88. except exc.GitCommandError as err:
  89. msg = err.stderr.replace("\n", " ").strip()
  90. if "unstaged changes" in msg:
  91. print(" error:", "unstaged changes.")
  92. elif "uncommitted changes" in msg:
  93. print(" error:", "uncommitted changes.")
  94. else:
  95. try:
  96. repo.git.rebase("--abort")
  97. except exc.GitCommandError:
  98. pass
  99. print(" error:", msg if msg else "rebase conflict")
  100. else:
  101. print(" done.")
  102. def _merge(repo, name):
  103. """Merge the branch *name* into the current HEAD of *repo*."""
  104. print(" merging...", end="")
  105. try:
  106. repo.git.merge(name)
  107. except exc.GitCommandError as err:
  108. msg = err.stderr.replace("\n", " ").strip()
  109. if "local changes" in msg and "would be overwritten" in msg:
  110. print(" error:", "uncommitted changes.")
  111. else:
  112. try:
  113. repo.git.merge("--abort")
  114. except exc.GitCommandError:
  115. pass
  116. print(" error:", msg if msg else "merge conflict")
  117. else:
  118. print(" done.")
  119. def _update_branch(repo, branch, merge, rebase, stasher=None):
  120. """Update a single branch."""
  121. print(INDENT2, "Updating", branch, end="...")
  122. upstream = branch.tracking_branch()
  123. if not upstream:
  124. print(" skipped: no upstream is tracked.")
  125. return
  126. try:
  127. branch.commit, upstream.commit
  128. except ValueError:
  129. print(" skipped: branch contains no revisions.")
  130. return
  131. if _is_up_to_date(repo, branch, upstream):
  132. print(" up to date.")
  133. return
  134. if stasher:
  135. stasher.clean()
  136. branch.checkout()
  137. config_attr = "branch.{0}.rebase".format(branch.name)
  138. if not merge and (rebase or _read_config(repo, config_attr)):
  139. _rebase(repo, upstream.name)
  140. else:
  141. _merge(repo, upstream.name)
  142. def _update_repository(repo, current_only=False, rebase=False, merge=False):
  143. """Update a single git repository by fetching remotes and rebasing/merging.
  144. The specific actions depend on the arguments given. We will fetch all
  145. remotes if *current_only* is ``False``, or only the remote tracked by the
  146. current branch if ``True``. By default, we will merge unless
  147. ``pull.rebase`` or ``branch.<name>.rebase`` is set in config; *rebase* will
  148. cause us to always rebase with ``--preserve-merges``, and *merge* will
  149. cause us to always merge.
  150. """
  151. print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":")
  152. active = repo.active_branch
  153. if current_only:
  154. ref = active.tracking_branch()
  155. if not ref:
  156. print(INDENT2, ERROR, "no remote tracked by current branch.")
  157. return
  158. remotes = [repo.remotes[ref.remote_name]]
  159. else:
  160. remotes = repo.remotes
  161. if not remotes:
  162. print(INDENT2, ERROR, "no remotes configured to pull from.")
  163. return
  164. _fetch_remotes(remotes)
  165. rebase = rebase or _read_config(repo, "pull.rebase")
  166. _update_branch(repo, active, merge, rebase)
  167. branches = set(repo.heads) - {active}
  168. if branches:
  169. stasher = _Stasher(repo)
  170. try:
  171. for branch in sorted(branches, key=lambda b: b.name):
  172. _update_branch(repo, branch, merge, rebase, stasher)
  173. finally:
  174. active.checkout()
  175. stasher.restore()
  176. def _update_subdirectories(path, long_name, update_args):
  177. """Update all subdirectories that are git repos in a given directory."""
  178. repos = []
  179. for item in os.listdir(path):
  180. try:
  181. repo = Repo(os.path.join(path, item))
  182. except (exc.InvalidGitRepositoryError, exc.NoSuchPathError):
  183. continue
  184. repos.append(repo)
  185. suffix = "ies" if len(repos) != 1 else "y"
  186. print(long_name[0].upper() + long_name[1:],
  187. "contains {0} git repositor{1}:".format(len(repos), suffix))
  188. for repo in sorted(repos, key=lambda r: os.path.split(r.working_dir)[1]):
  189. _update_repository(repo, *update_args)
  190. def _update_directory(path, update_args, is_bookmark=False):
  191. """Update a particular directory.
  192. Determine whether the directory is a git repo on its own, a directory of
  193. git repositories, or something invalid. If the first, update the single
  194. repository; if the second, update all repositories contained within; if the
  195. third, print an error.
  196. """
  197. dir_type = "bookmark" if is_bookmark else "directory"
  198. long_name = dir_type + ' "' + BOLD + path + RESET + '"'
  199. try:
  200. repo = Repo(path)
  201. except exc.NoSuchPathError:
  202. print(ERROR, long_name, "doesn't exist!")
  203. except exc.InvalidGitRepositoryError:
  204. if os.path.isdir(path):
  205. _update_subdirectories(path, long_name, update_args)
  206. else:
  207. print(ERROR, long_name, "isn't a repository!")
  208. else:
  209. long_name = (dir_type.capitalize() + ' "' + BOLD + repo.working_dir +
  210. RESET + '"')
  211. print(long_name, "is a git repository:")
  212. _update_repository(repo, *update_args)
  213. def update_bookmarks(bookmarks, update_args):
  214. """Loop through and update all bookmarks."""
  215. if bookmarks:
  216. for path, name in bookmarks:
  217. _update_directory(path, update_args, is_bookmark=True)
  218. else:
  219. print("You don't have any bookmarks configured! Get help with 'gitup -h'.")
  220. def update_directories(paths, update_args):
  221. """Update a list of directories supplied by command arguments."""
  222. for path in paths:
  223. full_path = os.path.abspath(path)
  224. _update_directory(full_path, update_args, is_bookmark=False)