A console script that allows you to easily update multiple git repositories at once
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

223 рядки
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)