@@ -13,4 +13,4 @@ __license__ = "MIT License" | |||||
__version__ = "0.2.dev" | __version__ = "0.2.dev" | ||||
__email__ = "ben.kurtovic@gmail.com" | __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 | from __future__ import print_function | ||||
import argparse | import argparse | ||||
import ConfigParser as configparser | |||||
import os | |||||
import re | |||||
import shlex | |||||
import subprocess | |||||
from . import __version__, __email__ | 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(): | def main(): | ||||
"""Parse arguments and then call the appropriate function(s).""" | """Parse arguments and then call the appropriate function(s).""" | ||||
@@ -292,11 +60,11 @@ def main(): | |||||
if args.directories_to_update: | if args.directories_to_update: | ||||
update_directories(args.directories_to_update) | update_directories(args.directories_to_update) | ||||
if args.update: | if args.update: | ||||
update_bookmarks() | |||||
update_bookmarks(get_bookmarks()) | |||||
# If they did not tell us to do anything, automatically update bookmarks: | # If they did not tell us to do anything, automatically update bookmarks: | ||||
if not any(vars(args).values()): | if not any(vars(args).values()): | ||||
update_bookmarks() | |||||
update_bookmarks(get_bookmarks()) | |||||
def run(): | def run(): | ||||
"""Thin wrapper for main() that catches KeyboardInterrupts.""" | """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) |