A console script that allows you to easily update multiple git repositories at once
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

update.py 8.3 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2011-2015 Ben Kurtovic <ben.kurtovic@gmail.com>
  4. # Released under the terms of the MIT License. See LICENSE 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. cur_count = str(int(cur_count))
  29. if max_count:
  30. max_count = str(int(max_count))
  31. if op_code & self.BEGIN:
  32. print("\b, " if self._started else " (", end="")
  33. if not self._started:
  34. self._started = True
  35. if op_code & self.END:
  36. end = ")"
  37. elif max_count:
  38. end = "\b" * (1 + len(cur_count) + len(max_count))
  39. else:
  40. end = "\b" * len(cur_count)
  41. if max_count:
  42. print("{0}/{1}".format(cur_count, max_count), end=end)
  43. else:
  44. print(str(cur_count), end=end)
  45. def _fetch_remotes(remotes, prune):
  46. """Fetch a list of remotes, displaying progress info along the way."""
  47. def _get_name(ref):
  48. """Return the local name of a remote or tag reference."""
  49. return ref.remote_head if isinstance(ref, RemoteRef) else ref.name
  50. # TODO: missing branch deleted (via --prune):
  51. info = [("NEW_HEAD", "new branch", "new branches"),
  52. ("NEW_TAG", "new tag", "new tags"),
  53. ("FAST_FORWARD", "branch update", "branch updates")]
  54. up_to_date = BLUE + "up to date" + RESET
  55. for remote in remotes:
  56. print(INDENT2, "Fetching", BOLD + remote.name, end="")
  57. if not remote.config_reader.has_option("fetch"):
  58. print(":", YELLOW + "skipped:", "no configured refspec.")
  59. continue
  60. try:
  61. results = remote.fetch(progress=_ProgressMonitor(), prune=prune)
  62. except exc.GitCommandError as err:
  63. msg = err.command[0].replace("Error when fetching: ", "")
  64. if not msg.endswith("."):
  65. msg += "."
  66. print(":", RED + "error:", msg)
  67. return
  68. except AssertionError: # Seems to be the result of a bug in GitPython
  69. # This happens when git initiates an auto-gc during fetch:
  70. print(":", RED + "error:", "something went wrong in GitPython,",
  71. "but the fetch might have been successful.")
  72. return
  73. rlist = []
  74. for attr, singular, plural in info:
  75. names = [_get_name(res.ref)
  76. for res in results if res.flags & getattr(res, attr)]
  77. if names:
  78. desc = singular if len(names) == 1 else plural
  79. colored = GREEN + desc + RESET
  80. rlist.append("{0} ({1})".format(colored, ", ".join(names)))
  81. print(":", (", ".join(rlist) if rlist else up_to_date) + ".")
  82. def _update_branch(repo, branch, is_active=False):
  83. """Update a single branch."""
  84. print(INDENT2, "Updating", BOLD + branch.name, end=": ")
  85. upstream = branch.tracking_branch()
  86. if not upstream:
  87. print(YELLOW + "skipped:", "no upstream is tracked.")
  88. return
  89. try:
  90. branch.commit
  91. except ValueError:
  92. print(YELLOW + "skipped:", "branch has no revisions.")
  93. return
  94. try:
  95. upstream.commit
  96. except ValueError:
  97. print(YELLOW + "skipped:", "upstream does not exist.")
  98. return
  99. base = repo.git.merge_base(branch.commit, upstream.commit)
  100. if repo.commit(base) == upstream.commit:
  101. print(BLUE + "up to date", end=".\n")
  102. return
  103. if is_active:
  104. try:
  105. repo.git.merge(upstream.name, ff_only=True)
  106. print(GREEN + "done", end=".\n")
  107. except exc.GitCommandError as err:
  108. msg = err.stderr
  109. if "local changes" in msg and "would be overwritten" in msg:
  110. print(YELLOW + "skipped:", "uncommitted changes.")
  111. else:
  112. print(YELLOW + "skipped:", "not possible to fast-forward.")
  113. else:
  114. status = repo.git.merge_base(
  115. branch.commit, upstream.commit, is_ancestor=True,
  116. with_extended_output=True, with_exceptions=False)[0]
  117. if status != 0:
  118. print(YELLOW + "skipped:", "not possible to fast-forward.")
  119. else:
  120. repo.git.branch(branch.name, upstream.name, force=True)
  121. print(GREEN + "done", end=".\n")
  122. def _update_repository(repo, current_only, fetch_only, prune):
  123. """Update a single git repository by fetching remotes and rebasing/merging.
  124. The specific actions depend on the arguments given. We will fetch all
  125. remotes if *current_only* is ``False``, or only the remote tracked by the
  126. current branch if ``True``. If *fetch_only* is ``False``, we will also
  127. update all fast-forwardable branches that are tracking valid upstreams.
  128. If *prune* is ``True``, remote-tracking branches that no longer exist on
  129. their remote after fetching will be deleted.
  130. """
  131. print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":")
  132. try:
  133. active = repo.active_branch
  134. except TypeError: # Happens when HEAD is detached
  135. active = None
  136. if current_only:
  137. if not active:
  138. print(INDENT2, ERROR,
  139. "--current-only doesn't make sense with a detached HEAD.")
  140. return
  141. ref = active.tracking_branch()
  142. if not ref:
  143. print(INDENT2, ERROR, "no remote tracked by current branch.")
  144. return
  145. remotes = [repo.remotes[ref.remote_name]]
  146. else:
  147. remotes = repo.remotes
  148. if not remotes:
  149. print(INDENT2, ERROR, "no remotes configured to fetch.")
  150. return
  151. _fetch_remotes(remotes, prune)
  152. if not fetch_only:
  153. for branch in sorted(repo.heads, key=lambda b: b.name):
  154. _update_branch(repo, branch, branch == active)
  155. def _update_subdirectories(path, update_args):
  156. """Update all subdirectories that are git repos in a given directory."""
  157. repos = []
  158. for item in os.listdir(path):
  159. try:
  160. repo = Repo(os.path.join(path, item))
  161. except (exc.InvalidGitRepositoryError, exc.NoSuchPathError):
  162. continue
  163. repos.append(repo)
  164. suffix = "" if len(repos) == 1 else "s"
  165. print(BOLD + path, "({0} repo{1}):".format(len(repos), suffix))
  166. for repo in sorted(repos, key=lambda r: os.path.split(r.working_dir)[1]):
  167. _update_repository(repo, *update_args)
  168. def _update_directory(path, update_args):
  169. """Update a particular directory.
  170. Determine whether the directory is a git repo on its own, a directory of
  171. git repositories, or something invalid. If the first, update the single
  172. repository; if the second, update all repositories contained within; if the
  173. third, print an error.
  174. """
  175. try:
  176. repo = Repo(path)
  177. except exc.NoSuchPathError:
  178. print(ERROR, BOLD + path, "doesn't exist!")
  179. except exc.InvalidGitRepositoryError:
  180. if os.path.isdir(path):
  181. _update_subdirectories(path, update_args)
  182. else:
  183. print(ERROR, BOLD + path, "isn't a repository!")
  184. else:
  185. print(BOLD + repo.working_dir, "(1 repo):")
  186. _update_repository(repo, *update_args)
  187. def update_bookmarks(bookmarks, update_args):
  188. """Loop through and update all bookmarks."""
  189. if bookmarks:
  190. for path in bookmarks:
  191. _update_directory(path, update_args)
  192. else:
  193. print("You don't have any bookmarks configured! Get help with 'gitup -h'.")
  194. def update_directories(paths, update_args):
  195. """Update a list of directories supplied by command arguments."""
  196. for path in paths:
  197. full_path = os.path.abspath(path)
  198. _update_directory(full_path, update_args)