Created
November 23, 2019 04:48
-
-
Save pkkm/2a773816a21568bc8612a6b6a6fa54d5 to your computer and use it in GitHub Desktop.
Script for automatically committing, pushing and pulling with git
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Shortcuts for quick-and-dirty Git usage. Useful when you need backups and sync but not a pretty history. | |
import argparse | |
import os | |
import subprocess | |
import sys | |
## Utilities. | |
MESSAGE_PREFIX = os.path.basename(__file__) + ": " | |
def message(msg, file=sys.stdout): | |
print(MESSAGE_PREFIX + msg, file=file) | |
def error(msg): | |
message("ERROR: " + msg, file=sys.stderr) | |
def fatal_error(msg, exit_code=1): | |
error(msg) | |
sys.exit(exit_code) | |
def header(msg): | |
if not sys.stdout.isatty or os.getenv("TERM") == "dumb": | |
print("# " + msg) | |
else: | |
print("\033[1m{}\033[0m".format(msg)) # Bold. | |
def remove_prefix(string, prefix): | |
if string.startswith(prefix): | |
return string[len(prefix):] | |
return string | |
def simple_run(program, **kwargs): | |
process = subprocess.run( | |
program, **{ | |
"check": True, "stdout": subprocess.PIPE, | |
"encoding": "utf8", **kwargs}) | |
return process.stdout.rstrip(os.linesep) | |
## Command line parameters. | |
parser = argparse.ArgumentParser( | |
# Show argument defaults in help. | |
formatter_class=argparse.ArgumentDefaultsHelpFormatter) | |
parser.add_argument( | |
"extra_args", metavar="extra-args", nargs="*", | |
help="extra arguments") | |
parser.add_argument("--init", action="store_true") | |
parser.add_argument("--add", action="store_true") | |
parser.add_argument("--commit", action="store_true") | |
parser.add_argument("--commit-message") | |
parser.add_argument("--sync", action="store_true") | |
args = parser.parse_args() | |
## Store exit codes of failed git commands. | |
error_exit_codes = [] | |
def run_and_remember(command): | |
global error_exit_codes | |
process = subprocess.run(command) | |
if process.returncode != 0: | |
error_exit_codes.append(process.returncode) | |
return process | |
## Create a repo. | |
process = subprocess.run( | |
["git", "rev-parse", "--is-inside-work-tree"], | |
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
if process.returncode != 0: | |
if args.init: | |
header("Initializing repo") | |
subprocess.run(["git", "init"], check=True) | |
else: | |
fatal_error("Not in a git repository.") | |
## Add everything (including untracked files). | |
if args.add: | |
header("Adding") | |
run_and_remember(["git", "add", "-A"]) | |
## Commit changes (if there are any). | |
if args.commit: | |
header("Committing") | |
staged_files = subprocess.run( | |
["git", "diff", "--cached", "--name-only"], check=True, | |
stdout=subprocess.PIPE, encoding="utf8" | |
).stdout.splitlines() | |
if not staged_files: | |
message("No changes to commit.") | |
else: | |
if args.commit_message is not None: | |
commit_msg = args.commit_message | |
else: | |
commit_msg = ", ".join(staged_files) | |
# Use a summary instead of list of files if the mesage would be too long. | |
if len(commit_msg) > 80: | |
# Slightly worse alternative to all of this manual parsing: | |
# git diff --cached --find-renames --find-copies --shortstat | |
status_counts = {} | |
process = subprocess.Popen( | |
["git", "diff", "--cached", | |
"--find-renames", "--find-copies", "--name-status"], | |
stdout=subprocess.PIPE, encoding="utf-8") | |
for line in iter(process.stdout.readline, ""): | |
line = line.rstrip(os.linesep) | |
status_letter = line[0] | |
status_counts[status_letter] = \ | |
status_counts.get(status_letter, 0) + 1 | |
process.wait() | |
if process.returncode != 0: | |
fatal_error("git diff failed.") | |
STATUS_TYPES = { | |
"A": "added", | |
"C": "copied", | |
"D": "deleted", | |
"M": "modified", | |
"R": "renamed", | |
"T": "changed", | |
"U": "unmerged", | |
"X": "unknown", | |
"B": "broken", | |
} | |
commit_statuses = ( | |
"{} {}".format(count, STATUS_TYPES[key]) | |
for key, count | |
in sorted(status_counts.items(), key=lambda x: x[1], reverse=True) | |
if key in STATUS_TYPES) | |
commit_msg = "({})".format(", ".join(commit_statuses)) | |
#commit_description = subprocess.run( | |
# ["git", "diff", "--cached", "--find-renames", "--find-copies", "--name-status"], | |
# check=True, stdout=subprocess.PIPE, encoding="utf8").stdout | |
run_and_remember(["git", "commit", "-m", commit_msg]) | |
## Sync. | |
if args.sync: | |
# Ensure we're on a branch; get its name. | |
process = subprocess.run( | |
["git", "symbolic-ref", "-q", "HEAD"], | |
stdout=subprocess.PIPE, encoding="utf8") | |
if process.returncode != 0: | |
fatal_error("Not on a branch, cannot sync.") | |
branch_name = remove_prefix(process.stdout.rstrip(os.linesep), "refs/heads/") | |
header("Pulling") | |
run_and_remember(["git", "pull", "--rebase"]) | |
# Ensure no rebase is in progress and syncing is enabled. | |
header("Pushing") | |
git_dir = simple_run(["git", "rev-parse", "--git-dir"]) | |
if any(os.path.isdir(os.path.join(git_dir, name)) | |
for name in ["rebase-merge", "rebase-apply"]): | |
message("A rebase is in progress, not pushing.") | |
elif simple_run(["git", "config", "--get", "--bool", "branch.{}.sync".format( | |
branch_name)], check=False) != "true": | |
fatal_error(( | |
"Syncing not enabled on the current branch. " + | |
"To enable, use: git config --bool branch.{}.sync true" | |
).format(branch_name)) | |
else: | |
run_and_remember(["git", "push"]) | |
## Exit with the appropriate code. | |
if error_exit_codes: | |
sys.exit(error_exit_codes[-1]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment