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.

317 lines
11 KiB

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