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.

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