A console script that allows you to easily update multiple git repositories at once
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

307 rader
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.")