| @@ -13,4 +13,4 @@ __license__ = "MIT License" | |||
| __version__ = "0.2.dev" | |||
| __email__ = "ben.kurtovic@gmail.com" | |||
| from . import script | |||
| from . import script, update | |||
| @@ -0,0 +1,82 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | |||
| # See the LICENSE file for details. | |||
| import ConfigParser as configparser | |||
| import os | |||
| from .output import out, bold, yellow | |||
| __all__ = ["get_bookmarks", "add_bookmarks", "delete_bookmarks", | |||
| "list_bookmarks"] | |||
| _config_filename = os.path.join(os.path.expanduser("~"), ".gitup") | |||
| def _load_config_file(): | |||
| """Read the config file and return a SafeConfigParser() object.""" | |||
| config = configparser.SafeConfigParser() | |||
| # Don't lowercase option names, because we are storing paths there: | |||
| config.optionxform = str | |||
| config.read(_config_filename) | |||
| return config | |||
| def _save_config_file(config): | |||
| """Save config changes to the config file specified by _config_filename.""" | |||
| with open(_config_filename, "wb") as config_file: | |||
| config.write(config_file) | |||
| def get_bookmarks(): | |||
| """Get a list of all bookmarks, or an empty list if there are none.""" | |||
| config = _load_config_file() | |||
| try: | |||
| return config.items("bookmarks") | |||
| except configparser.NoSectionError: | |||
| return [] | |||
| def add_bookmarks(paths): | |||
| """Add a list of paths as bookmarks to the config file.""" | |||
| config = _load_config_file() | |||
| if not config.has_section("bookmarks"): | |||
| config.add_section("bookmarks") | |||
| out(0, yellow("Added bookmarks:")) | |||
| for path in paths: | |||
| path = os.path.abspath(path) # Convert relative to absolute path | |||
| if config.has_option("bookmarks", path): | |||
| out(1, "'{0}' is already bookmarked.".format(path)) | |||
| else: | |||
| path_name = os.path.split(path)[1] | |||
| config.set("bookmarks", path, path_name) | |||
| out(1, bold(path)) | |||
| _save_config_file(config) | |||
| def delete_bookmarks(paths): | |||
| """Remove a list of paths from the bookmark config file.""" | |||
| config = _load_config_file() | |||
| if config.has_section("bookmarks"): | |||
| out(0, yellow("Deleted bookmarks:")) | |||
| for path in paths: | |||
| path = os.path.abspath(path) # Convert relative to absolute path | |||
| config_was_changed = config.remove_option("bookmarks", path) | |||
| if config_was_changed: | |||
| out(1, bold(path)) | |||
| else: | |||
| out(1, "'{0}' is not bookmarked.".format(path)) | |||
| _save_config_file(config) | |||
| else: | |||
| out(0, "There are no bookmarks to delete!") | |||
| def list_bookmarks(): | |||
| """Print all of our current bookmarks.""" | |||
| bookmarks = get_bookmarks() | |||
| if bookmarks: | |||
| out(0, yellow("Current bookmarks:")) | |||
| for bookmark_path, bookmark_name in bookmarks: | |||
| out(1, bookmark_path) | |||
| else: | |||
| out(0, "You have no bookmarks to display.") | |||
| @@ -0,0 +1,41 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | |||
| # See the LICENSE file for details. | |||
| import re | |||
| __all__ = ["out", "bold", "red", "green", "yellow", "blue"] | |||
| # Text formatting functions: | |||
| bold = lambda t: _style_text(t, "bold") | |||
| red = lambda t: _style_text(t, "red") | |||
| green = lambda t: _style_text(t, "green") | |||
| yellow = lambda t: _style_text(t, "yellow") | |||
| blue = lambda t: _style_text(t, "blue") | |||
| def _style_text(text, effect): | |||
| """Give a text string a certain effect, such as boldness, or a color.""" | |||
| ansi = { # ANSI escape codes to make terminal output fancy | |||
| "reset": "\x1b[0m", | |||
| "bold": "\x1b[1m", | |||
| "red": "\x1b[1m\x1b[31m", | |||
| "green": "\x1b[1m\x1b[32m", | |||
| "yellow": "\x1b[1m\x1b[33m", | |||
| "blue": "\x1b[1m\x1b[34m", | |||
| } | |||
| try: # Pad text with effect, unless effect does not exist | |||
| return ansi[effect] + text + ansi["reset"] | |||
| except KeyError: | |||
| return text | |||
| def out(indent, msg): | |||
| """Print a message at a given indentation level.""" | |||
| width = 4 # Amount to indent at each level | |||
| if indent == 0: | |||
| spacing = "\n" | |||
| else: | |||
| spacing = " " * width * indent | |||
| msg = re.sub(r"\s+", " ", msg) # Collapse multiple spaces into one | |||
| print(spacing + msg) | |||
| @@ -6,244 +6,12 @@ | |||
| from __future__ import print_function | |||
| import argparse | |||
| import ConfigParser as configparser | |||
| import os | |||
| import re | |||
| import shlex | |||
| import subprocess | |||
| from . import __version__, __email__ | |||
| config_filename = os.path.join(os.path.expanduser("~"), ".gitup") | |||
| # Text formatting functions: | |||
| bold = lambda t: _style_text(t, "bold") | |||
| red = lambda t: _style_text(t, "red") | |||
| green = lambda t: _style_text(t, "green") | |||
| yellow = lambda t: _style_text(t, "yellow") | |||
| blue = lambda t: _style_text(t, "blue") | |||
| def _style_text(text, effect): | |||
| """Give a text string a certain effect, such as boldness, or a color.""" | |||
| ansi = { # ANSI escape codes to make terminal output fancy | |||
| "reset": "\x1b[0m", | |||
| "bold": "\x1b[1m", | |||
| "red": "\x1b[1m\x1b[31m", | |||
| "green": "\x1b[1m\x1b[32m", | |||
| "yellow": "\x1b[1m\x1b[33m", | |||
| "blue": "\x1b[1m\x1b[34m", | |||
| } | |||
| try: # Pad text with effect, unless effect does not exist | |||
| return ansi[effect] + text + ansi["reset"] | |||
| except KeyError: | |||
| return text | |||
| def out(indent, msg): | |||
| """Print a message at a given indentation level.""" | |||
| width = 4 # Amount to indent at each level | |||
| if indent == 0: | |||
| spacing = "\n" | |||
| else: | |||
| spacing = " " * width * indent | |||
| msg = re.sub(r"\s+", " ", msg) # Collapse multiple spaces into one | |||
| print(spacing + msg) | |||
| def exec_shell(command): | |||
| """Execute a shell command and get the output.""" | |||
| command = shlex.split(command) | |||
| result = subprocess.check_output(command, stderr=subprocess.STDOUT) | |||
| if result: | |||
| result = result[:-1] # Strip newline if command returned anything | |||
| return result | |||
| def directory_is_git_repo(directory_path): | |||
| """Check if a directory is a git repository.""" | |||
| if os.path.isdir(directory_path): | |||
| git_subfolder = os.path.join(directory_path, ".git") | |||
| if os.path.isdir(git_subfolder): # Check for path/to/repository/.git | |||
| return True | |||
| return False | |||
| def update_repository(repo_path, repo_name): | |||
| """Update a single git repository by pulling from the remote.""" | |||
| out(1, bold(repo_name) + ":") | |||
| # cd into our folder so git commands target the correct repo: | |||
| os.chdir(repo_path) | |||
| try: | |||
| # Check if there is anything to pull, but don't do it yet: | |||
| dry_fetch = exec_shell("git fetch --dry-run") | |||
| except subprocess.CalledProcessError: | |||
| out(2, red("Error: ") + "cannot fetch; do you have a remote " \ | |||
| "repository configured correctly?") | |||
| return | |||
| try: | |||
| last_commit = exec_shell("git log -n 1 --pretty=\"%ar\"") | |||
| except subprocess.CalledProcessError: | |||
| last_commit = "never" # Couldn't get a log, so no commits | |||
| if not dry_fetch: # No new changes to pull | |||
| out(2, blue("No new changes.") + | |||
| " Last commit was {0}.".format(last_commit)) | |||
| else: # Stuff has happened! | |||
| out(2, "There are new changes upstream...") | |||
| status = exec_shell("git status") | |||
| if status.endswith("nothing to commit, working directory clean"): | |||
| out(2, green("Pulling new changes...")) | |||
| result = exec_shell("git pull") | |||
| out(2, "The following changes have been made since {0}:".format( | |||
| last_commit)) | |||
| print(result) | |||
| else: | |||
| out(2, red("Warning: ") + | |||
| "you have uncommitted changes in this repository!") | |||
| out(2, "Ignoring.") | |||
| def update_directory(dir_path, dir_name, is_bookmark=False): | |||
| """Update a particular directory. | |||
| First, make sure the specified object is actually a directory, then | |||
| determine whether the directory is a git repo on its own or a directory | |||
| of git repositories. If the former, update the single repository; if the | |||
| latter, update all repositories contained within. | |||
| """ | |||
| if is_bookmark: | |||
| dir_type = "bookmark" # Where did we get this directory from? | |||
| else: | |||
| dir_type = "directory" | |||
| dir_long_name = "{0} '{1}'".format(dir_type, bold(dir_path)) | |||
| try: | |||
| os.listdir(dir_path) # Test if we can access this directory | |||
| except OSError: | |||
| out(0, red("Error: ") + | |||
| "cannot enter {0}; does it exist?".format(dir_long_name)) | |||
| return | |||
| if not os.path.isdir(dir_path): | |||
| if os.path.exists(dir_path): | |||
| out(0, red("Error: ") + dir_long_name + " is not a directory!") | |||
| else: | |||
| out(0, red("Error: ") + dir_long_name + " does not exist!") | |||
| return | |||
| if directory_is_git_repo(dir_path): | |||
| out(0, dir_long_name.capitalize() + " is a git repository:") | |||
| update_repository(dir_path, dir_name) | |||
| else: | |||
| repositories = [] | |||
| dir_contents = os.listdir(dir_path) # Get potential repos in directory | |||
| for item in dir_contents: | |||
| repo_path = os.path.join(dir_path, item) | |||
| repo_name = os.path.join(dir_name, item) | |||
| if directory_is_git_repo(repo_path): # Filter out non-repositories | |||
| repositories.append((repo_path, repo_name)) | |||
| num_of_repos = len(repositories) | |||
| if num_of_repos == 1: | |||
| out(0, dir_long_name.capitalize() + " contains 1 git repository:") | |||
| else: | |||
| out(0, dir_long_name.capitalize() + | |||
| " contains {0} git repositories:".format(num_of_repos)) | |||
| repositories.sort() # Go alphabetically instead of randomly | |||
| for repo_path, repo_name in repositories: | |||
| update_repository(repo_path, repo_name) | |||
| def update_directories(paths): | |||
| """Update a list of directories supplied by command arguments.""" | |||
| for path in paths: | |||
| path = os.path.abspath(path) # Convert relative to absolute path | |||
| path_name = os.path.split(path)[1] # Dir name ("x" in /path/to/x/) | |||
| update_directory(path, path_name, is_bookmark=False) | |||
| def update_bookmarks(): | |||
| """Loop through and update all bookmarks.""" | |||
| try: | |||
| bookmarks = load_config_file().items("bookmarks") | |||
| except configparser.NoSectionError: | |||
| bookmarks = [] | |||
| if bookmarks: | |||
| for bookmark_path, bookmark_name in bookmarks: | |||
| update_directory(bookmark_path, bookmark_name, is_bookmark=True) | |||
| else: | |||
| out(0, "You don't have any bookmarks configured! " \ | |||
| "Get help with 'gitup -h'.") | |||
| def load_config_file(): | |||
| """Read the config file and return a SafeConfigParser() object.""" | |||
| config = configparser.SafeConfigParser() | |||
| # Don't lowercase option names, because we are storing paths there: | |||
| config.optionxform = str | |||
| config.read(config_filename) | |||
| return config | |||
| def save_config_file(config): | |||
| """Save config changes to the config file specified by config_filename.""" | |||
| with open(config_filename, "wb") as config_file: | |||
| config.write(config_file) | |||
| def add_bookmarks(paths): | |||
| """Add a list of paths as bookmarks to the config file.""" | |||
| config = load_config_file() | |||
| if not config.has_section("bookmarks"): | |||
| config.add_section("bookmarks") | |||
| out(0, yellow("Added bookmarks:")) | |||
| for path in paths: | |||
| path = os.path.abspath(path) # Convert relative to absolute path | |||
| if config.has_option("bookmarks", path): | |||
| out(1, "'{0}' is already bookmarked.".format(path)) | |||
| else: | |||
| path_name = os.path.split(path)[1] | |||
| config.set("bookmarks", path, path_name) | |||
| out(1, bold(path)) | |||
| save_config_file(config) | |||
| def delete_bookmarks(paths): | |||
| """Remove a list of paths from the bookmark config file.""" | |||
| config = load_config_file() | |||
| if config.has_section("bookmarks"): | |||
| out(0, yellow("Deleted bookmarks:")) | |||
| for path in paths: | |||
| path = os.path.abspath(path) # Convert relative to absolute path | |||
| config_was_changed = config.remove_option("bookmarks", path) | |||
| if config_was_changed: | |||
| out(1, bold(path)) | |||
| else: | |||
| out(1, "'{0}' is not bookmarked.".format(path)) | |||
| save_config_file(config) | |||
| else: | |||
| out(0, "There are no bookmarks to delete!") | |||
| def list_bookmarks(): | |||
| """Print all of our current bookmarks.""" | |||
| config = load_config_file() | |||
| try: | |||
| bookmarks = config.items("bookmarks") | |||
| except configparser.NoSectionError: | |||
| bookmarks = [] | |||
| if bookmarks: | |||
| out(0, yellow("Current bookmarks:")) | |||
| for bookmark_path, bookmark_name in bookmarks: | |||
| out(1, bookmark_path) | |||
| else: | |||
| out(0, "You have no bookmarks to display.") | |||
| from .config import (get_bookmarks, add_bookmarks, delete_bookmarks, | |||
| list_bookmarks) | |||
| from .output import out, bold | |||
| from .update import update_bookmarks, update_directories | |||
| def main(): | |||
| """Parse arguments and then call the appropriate function(s).""" | |||
| @@ -292,11 +60,11 @@ def main(): | |||
| if args.directories_to_update: | |||
| update_directories(args.directories_to_update) | |||
| if args.update: | |||
| update_bookmarks() | |||
| update_bookmarks(get_bookmarks()) | |||
| # If they did not tell us to do anything, automatically update bookmarks: | |||
| if not any(vars(args).values()): | |||
| update_bookmarks() | |||
| update_bookmarks(get_bookmarks()) | |||
| def run(): | |||
| """Thin wrapper for main() that catches KeyboardInterrupts.""" | |||
| @@ -0,0 +1,138 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com> | |||
| # See the LICENSE file for details. | |||
| import os | |||
| import shlex | |||
| import subprocess | |||
| from .output import out, bold, red, green, blue | |||
| __all__ = ["update_bookmarks", "update_directories"] | |||
| def _exec_shell(command): | |||
| """Execute a shell command and get the output.""" | |||
| command = shlex.split(command) | |||
| result = subprocess.check_output(command, stderr=subprocess.STDOUT) | |||
| if result: | |||
| result = result[:-1] # Strip newline if command returned anything | |||
| return result | |||
| def _directory_is_git_repo(directory_path): | |||
| """Check if a directory is a git repository.""" | |||
| if os.path.isdir(directory_path): | |||
| git_subfolder = os.path.join(directory_path, ".git") | |||
| if os.path.isdir(git_subfolder): # Check for path/to/repository/.git | |||
| return True | |||
| return False | |||
| def _update_repository(repo_path, repo_name): | |||
| """Update a single git repository by pulling from the remote.""" | |||
| out(1, bold(repo_name) + ":") | |||
| # cd into our folder so git commands target the correct repo: | |||
| os.chdir(repo_path) | |||
| try: | |||
| # Check if there is anything to pull, but don't do it yet: | |||
| dry_fetch = _exec_shell("git fetch --dry-run") | |||
| except subprocess.CalledProcessError: | |||
| out(2, red("Error: ") + "cannot fetch; do you have a remote " \ | |||
| "repository configured correctly?") | |||
| return | |||
| try: | |||
| last_commit = _exec_shell("git log -n 1 --pretty=\"%ar\"") | |||
| except subprocess.CalledProcessError: | |||
| last_commit = "never" # Couldn't get a log, so no commits | |||
| if not dry_fetch: # No new changes to pull | |||
| out(2, blue("No new changes.") + | |||
| " Last commit was {0}.".format(last_commit)) | |||
| else: # Stuff has happened! | |||
| out(2, "There are new changes upstream...") | |||
| status = _exec_shell("git status") | |||
| if status.endswith("nothing to commit, working directory clean"): | |||
| out(2, green("Pulling new changes...")) | |||
| result = _exec_shell("git pull") | |||
| out(2, "The following changes have been made since {0}:".format( | |||
| last_commit)) | |||
| print(result) | |||
| else: | |||
| out(2, red("Warning: ") + | |||
| "you have uncommitted changes in this repository!") | |||
| out(2, "Ignoring.") | |||
| def _update_directory(dir_path, dir_name, is_bookmark=False): | |||
| """Update a particular directory. | |||
| First, make sure the specified object is actually a directory, then | |||
| determine whether the directory is a git repo on its own or a directory | |||
| of git repositories. If the former, update the single repository; if the | |||
| latter, update all repositories contained within. | |||
| """ | |||
| if is_bookmark: | |||
| dir_type = "bookmark" # Where did we get this directory from? | |||
| else: | |||
| dir_type = "directory" | |||
| dir_long_name = "{0} '{1}'".format(dir_type, bold(dir_path)) | |||
| try: | |||
| os.listdir(dir_path) # Test if we can access this directory | |||
| except OSError: | |||
| out(0, red("Error: ") + | |||
| "cannot enter {0}; does it exist?".format(dir_long_name)) | |||
| return | |||
| if not os.path.isdir(dir_path): | |||
| if os.path.exists(dir_path): | |||
| out(0, red("Error: ") + dir_long_name + " is not a directory!") | |||
| else: | |||
| out(0, red("Error: ") + dir_long_name + " does not exist!") | |||
| return | |||
| if _directory_is_git_repo(dir_path): | |||
| out(0, dir_long_name.capitalize() + " is a git repository:") | |||
| _update_repository(dir_path, dir_name) | |||
| else: | |||
| repositories = [] | |||
| dir_contents = os.listdir(dir_path) # Get potential repos in directory | |||
| for item in dir_contents: | |||
| repo_path = os.path.join(dir_path, item) | |||
| repo_name = os.path.join(dir_name, item) | |||
| if _directory_is_git_repo(repo_path): # Filter out non-repositories | |||
| repositories.append((repo_path, repo_name)) | |||
| num_of_repos = len(repositories) | |||
| if num_of_repos == 1: | |||
| out(0, dir_long_name.capitalize() + " contains 1 git repository:") | |||
| else: | |||
| out(0, dir_long_name.capitalize() + | |||
| " contains {0} git repositories:".format(num_of_repos)) | |||
| repositories.sort() # Go alphabetically instead of randomly | |||
| for repo_path, repo_name in repositories: | |||
| _update_repository(repo_path, repo_name) | |||
| def update_bookmarks(bookmarks): | |||
| """Loop through and update all bookmarks.""" | |||
| if bookmarks: | |||
| for bookmark_path, bookmark_name in bookmarks: | |||
| _update_directory(bookmark_path, bookmark_name, is_bookmark=True) | |||
| else: | |||
| out(0, "You don't have any bookmarks configured! " \ | |||
| "Get help with 'gitup -h'.") | |||
| def update_directories(paths): | |||
| """Update a list of directories supplied by command arguments.""" | |||
| for path in paths: | |||
| path = os.path.abspath(path) # Convert relative to absolute path | |||
| path_name = os.path.split(path)[1] # Dir name ("x" in /path/to/x/) | |||
| _update_directory(path, path_name, is_bookmark=False) | |||