A console script that allows you to easily update multiple git repositories at once
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

307 líneas
11 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 argparse
  7. import ConfigParser as configparser
  8. import os
  9. import re
  10. import shlex
  11. import subprocess
  12. from . import __version__, __email__
  13. config_filename = os.path.join(os.path.expanduser("~"), ".gitup")
  14. # Text formatting functions:
  15. bold = lambda t: _style_text(t, "bold")
  16. red = lambda t: _style_text(t, "red")
  17. green = lambda t: _style_text(t, "green")
  18. yellow = lambda t: _style_text(t, "yellow")
  19. blue = lambda t: _style_text(t, "blue")
  20. def _style_text(text, effect):
  21. """Give a text string a certain effect, such as boldness, or a color."""
  22. ansi = { # ANSI escape codes to make terminal output fancy
  23. "reset": "\x1b[0m",
  24. "bold": "\x1b[1m",
  25. "red": "\x1b[1m\x1b[31m",
  26. "green": "\x1b[1m\x1b[32m",
  27. "yellow": "\x1b[1m\x1b[33m",
  28. "blue": "\x1b[1m\x1b[34m",
  29. }
  30. try: # Pad text with effect, unless effect does not exist
  31. return ansi[effect] + text + ansi["reset"]
  32. except KeyError:
  33. return text
  34. def out(indent, msg):
  35. """Print a message at a given indentation level."""
  36. width = 4 # Amount to indent at each level
  37. if indent == 0:
  38. spacing = "\n"
  39. else:
  40. spacing = " " * width * indent
  41. msg = re.sub(r"\s+", " ", msg) # Collapse multiple spaces into one
  42. print(spacing + msg)
  43. def exec_shell(command):
  44. """Execute a shell command and get the output."""
  45. command = shlex.split(command)
  46. result = subprocess.check_output(command, stderr=subprocess.STDOUT)
  47. if result:
  48. result = result[:-1] # Strip newline if command returned anything
  49. return result
  50. def directory_is_git_repo(directory_path):
  51. """Check if a directory is a git repository."""
  52. if os.path.isdir(directory_path):
  53. git_subfolder = os.path.join(directory_path, ".git")
  54. if os.path.isdir(git_subfolder): # Check for path/to/repository/.git
  55. return True
  56. return False
  57. def update_repository(repo_path, repo_name):
  58. """Update a single git repository by pulling from the remote."""
  59. out(1, bold(repo_name) + ":")
  60. # cd into our folder so git commands target the correct repo:
  61. os.chdir(repo_path)
  62. try:
  63. # Check if there is anything to pull, but don't do it yet:
  64. dry_fetch = exec_shell("git fetch --dry-run")
  65. except subprocess.CalledProcessError:
  66. out(2, red("Error: ") + "cannot fetch; do you have a remote " \
  67. "repository configured correctly?")
  68. return
  69. try:
  70. last_commit = exec_shell("git log -n 1 --pretty=\"%ar\"")
  71. except subprocess.CalledProcessError:
  72. last_commit = "never" # Couldn't get a log, so no commits
  73. if not dry_fetch: # No new changes to pull
  74. out(2, blue("No new changes.") +
  75. " Last commit was {0}.".format(last_commit))
  76. else: # Stuff has happened!
  77. out(2, "There are new changes upstream...")
  78. status = exec_shell("git status")
  79. if status.endswith("nothing to commit, working directory clean"):
  80. out(2, green("Pulling new changes..."))
  81. result = exec_shell("git pull")
  82. out(2, "The following changes have been made since {0}:".format(
  83. last_commit))
  84. print(result)
  85. else:
  86. out(2, red("Warning: ") +
  87. "you have uncommitted changes in this repository!")
  88. out(2, "Ignoring.")
  89. def update_directory(dir_path, dir_name, is_bookmark=False):
  90. """Update a particular directory.
  91. First, make sure the specified object is actually a directory, then
  92. determine whether the directory is a git repo on its own or a directory
  93. of git repositories. If the former, update the single repository; if the
  94. latter, update all repositories contained within.
  95. """
  96. if is_bookmark:
  97. dir_type = "bookmark" # Where did we get this directory from?
  98. else:
  99. dir_type = "directory"
  100. dir_long_name = "{0} '{1}'".format(dir_type, bold(dir_path))
  101. try:
  102. os.listdir(dir_path) # Test if we can access this directory
  103. except OSError:
  104. out(0, red("Error: ") +
  105. "cannot enter {0}; does it exist?".format(dir_long_name))
  106. return
  107. if not os.path.isdir(dir_path):
  108. if os.path.exists(dir_path):
  109. out(0, red("Error: ") + dir_long_name + " is not a directory!")
  110. else:
  111. out(0, red("Error: ") + dir_long_name + " does not exist!")
  112. return
  113. if directory_is_git_repo(dir_path):
  114. out(0, dir_long_name.capitalize() + " is a git repository:")
  115. update_repository(dir_path, dir_name)
  116. else:
  117. repositories = []
  118. dir_contents = os.listdir(dir_path) # Get potential repos in directory
  119. for item in dir_contents:
  120. repo_path = os.path.join(dir_path, item)
  121. repo_name = os.path.join(dir_name, item)
  122. if directory_is_git_repo(repo_path): # Filter out non-repositories
  123. repositories.append((repo_path, repo_name))
  124. num_of_repos = len(repositories)
  125. if num_of_repos == 1:
  126. out(0, dir_long_name.capitalize() + " contains 1 git repository:")
  127. else:
  128. out(0, dir_long_name.capitalize() +
  129. " contains {0} git repositories:".format(num_of_repos))
  130. repositories.sort() # Go alphabetically instead of randomly
  131. for repo_path, repo_name in repositories:
  132. update_repository(repo_path, repo_name)
  133. def update_directories(paths):
  134. """Update a list of directories supplied by command arguments."""
  135. for path in paths:
  136. path = os.path.abspath(path) # Convert relative to absolute path
  137. path_name = os.path.split(path)[1] # Dir name ("x" in /path/to/x/)
  138. update_directory(path, path_name, is_bookmark=False)
  139. def update_bookmarks():
  140. """Loop through and update all bookmarks."""
  141. try:
  142. bookmarks = load_config_file().items("bookmarks")
  143. except configparser.NoSectionError:
  144. bookmarks = []
  145. if bookmarks:
  146. for bookmark_path, bookmark_name in bookmarks:
  147. update_directory(bookmark_path, bookmark_name, is_bookmark=True)
  148. else:
  149. out(0, "You don't have any bookmarks configured! " \
  150. "Get help with 'gitup -h'.")
  151. def load_config_file():
  152. """Read the config file and return a SafeConfigParser() object."""
  153. config = configparser.SafeConfigParser()
  154. # Don't lowercase option names, because we are storing paths there:
  155. config.optionxform = str
  156. config.read(config_filename)
  157. return config
  158. def save_config_file(config):
  159. """Save config changes to the config file specified by config_filename."""
  160. with open(config_filename, "wb") as config_file:
  161. config.write(config_file)
  162. def add_bookmarks(paths):
  163. """Add a list of paths as bookmarks to the config file."""
  164. config = load_config_file()
  165. if not config.has_section("bookmarks"):
  166. config.add_section("bookmarks")
  167. out(0, yellow("Added bookmarks:"))
  168. for path in paths:
  169. path = os.path.abspath(path) # Convert relative to absolute path
  170. if config.has_option("bookmarks", path):
  171. out(1, "'{0}' is already bookmarked.".format(path))
  172. else:
  173. path_name = os.path.split(path)[1]
  174. config.set("bookmarks", path, path_name)
  175. out(1, bold(path))
  176. save_config_file(config)
  177. def delete_bookmarks(paths):
  178. """Remove a list of paths from the bookmark config file."""
  179. config = load_config_file()
  180. if config.has_section("bookmarks"):
  181. out(0, yellow("Deleted bookmarks:"))
  182. for path in paths:
  183. path = os.path.abspath(path) # Convert relative to absolute path
  184. config_was_changed = config.remove_option("bookmarks", path)
  185. if config_was_changed:
  186. out(1, bold(path))
  187. else:
  188. out(1, "'{0}' is not bookmarked.".format(path))
  189. save_config_file(config)
  190. else:
  191. out(0, "There are no bookmarks to delete!")
  192. def list_bookmarks():
  193. """Print all of our current bookmarks."""
  194. config = load_config_file()
  195. try:
  196. bookmarks = config.items("bookmarks")
  197. except configparser.NoSectionError:
  198. bookmarks = []
  199. if bookmarks:
  200. out(0, yellow("Current bookmarks:"))
  201. for bookmark_path, bookmark_name in bookmarks:
  202. out(1, bookmark_path)
  203. else:
  204. out(0, "You have no bookmarks to display.")
  205. def main():
  206. """Parse arguments and then call the appropriate function(s)."""
  207. parser = argparse.ArgumentParser(
  208. description="""Easily pull to multiple git repositories at once.""",
  209. epilog="""
  210. Both relative and absolute paths are accepted by all arguments.
  211. Questions? Comments? Email the author at {0}.""".format(__email__),
  212. add_help=False)
  213. group_u = parser.add_argument_group("updating repositories")
  214. group_b = parser.add_argument_group("bookmarking")
  215. group_m = parser.add_argument_group("miscellaneous")
  216. group_u.add_argument(
  217. 'directories_to_update', nargs="*", metavar="path",
  218. help="""update all repositories in this directory (or the directory
  219. itself, if it is a repo)""")
  220. group_u.add_argument(
  221. '-u', '--update', action="store_true", help="""update all bookmarks
  222. (default behavior when called without arguments)""")
  223. group_b.add_argument(
  224. '-a', '--add', dest="bookmarks_to_add", nargs="+", metavar="path",
  225. help="add directory(s) as bookmarks")
  226. group_b.add_argument(
  227. '-d', '--delete', dest="bookmarks_to_del", nargs="+", metavar="path",
  228. help="delete bookmark(s) (leaves actual directories alone)")
  229. group_b.add_argument(
  230. '-l', '--list', dest="list_bookmarks", action="store_true",
  231. help="list current bookmarks")
  232. group_m.add_argument(
  233. '-h', '--help', action="help", help="show this help message and exit")
  234. group_m.add_argument(
  235. '-v', '--version', action="version",
  236. version="gitup version " + __version__)
  237. args = parser.parse_args()
  238. print(bold("gitup") + ": the git-repo-updater")
  239. if args.bookmarks_to_add:
  240. add_bookmarks(args.bookmarks_to_add)
  241. if args.bookmarks_to_del:
  242. delete_bookmarks(args.bookmarks_to_del)
  243. if args.list_bookmarks:
  244. list_bookmarks()
  245. if args.directories_to_update:
  246. update_directories(args.directories_to_update)
  247. if args.update:
  248. update_bookmarks()
  249. # If they did not tell us to do anything, automatically update bookmarks:
  250. if not any(vars(args).values()):
  251. update_bookmarks()
  252. def run():
  253. """Thin wrapper for main() that catches KeyboardInterrupts."""
  254. try:
  255. main()
  256. except KeyboardInterrupt:
  257. out(0, "Stopped by user.")