Skip to content

Instantly share code, notes, and snippets.

@pkkm
Created November 23, 2019 04:48
Show Gist options
  • Save pkkm/2a773816a21568bc8612a6b6a6fa54d5 to your computer and use it in GitHub Desktop.
Save pkkm/2a773816a21568bc8612a6b6a6fa54d5 to your computer and use it in GitHub Desktop.
Script for automatically committing, pushing and pulling with git
#!/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