A console script that allows you to easily update multiple git repositories at once
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

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