A console script that allows you to easily update multiple git repositories at once
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

310 Zeilen
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.")