This repository has been archived on 2021-07-15. You can view files and clone it, but cannot push or open issues or pull requests.
tyop/tyop.py

265 lines
7.9 KiB
Python

"""Automate your mass typo updates."""
from argparse import ArgumentParser
from glob import glob
from logging import DEBUG, INFO, basicConfig, getLogger
from os.path import expanduser
from pathlib import Path
from shlex import split
from subprocess import check_output, run
from sys import exit
from ruamel.yaml import YAML
class Migration:
DIFF_LIMIT = 3
def __init__(self):
self.args = self._parse()
self.yaml = self._init_yaml()
self.log = self._init_logging()
self.commit_msg = None
self._validate()
if self.args.validate:
self._exit(code=0, output=False)
self.matches = glob(expanduser(self.GLOB))
self.paths = list(set([Path(p).parent for p in self.matches]))
self.log.debug(f"Discovered '{self.GLOB}' as user defined glob")
self.log.debug(f"Matched paths are: {[m for m in self.matches]}")
if self.args.reset:
self._clean()
self._exit(code=0, output=False)
try:
self.log.info("=" * 79)
self.log.info("RUNNING MIGRATIONS + CHECKS (ONLY GIT LOCAL CHANGES)")
self.log.info("=" * 79)
self._run()
except Exception as exception:
self._exit(msg=f"Failed to run migration, saw: {exception}")
def _init_yaml(self):
yaml = YAML(typ="rt")
yaml.preserve_quotes = True
yaml.indent(mapping=2, sequence=4, offset=2)
yaml.allow_duplicate_keys = True
yaml.width = 999999
return yaml
def _exit(self, msg="Bailing out on request...", code=1, output=True):
if output:
self.log.info(msg)
exit(code)
def _parse(self):
description = "Tyop: automate your mass typo updates"
parser = ArgumentParser(description=description)
parser.add_argument(
"-d",
"--debug",
help="enable verbose debug logs",
action="store_const",
dest="log",
const=DEBUG,
default=INFO,
)
parser.add_argument(
"-v",
"--validate",
default=False,
action="store_true",
dest="validate",
help="Validate end-user defined migrationa and exit",
)
parser.add_argument(
"-r",
"--reset",
default=False,
action="store_true",
dest="reset",
help="Reset changes without running migrations (git-checkout)",
)
parser.add_argument(
"-y",
"--yaml",
default=False,
action="store_true",
dest="yaml",
help="Expect YAML and load for parsing",
)
return parser.parse_args()
def _init_logging(self):
basicConfig(level=self.args.log, format="%(levelname)-8s %(message)s")
return getLogger(__name__)
def _shell(self, cmd, shell=False, check=True, **kwargs):
runner = check_output
args = [split(cmd)]
if shell:
args = [cmd]
kwargs = {"shell": shell}
if not check:
runner = run
try:
output = runner(*args, **kwargs)
if check:
return output.decode("utf-8").strip()
except Exception as exception:
self._exit(msg=f"Failed to run {cmd}, saw {str(exception)}")
def _confirm(self, bail=True, match=None):
answer = ""
while answer not in ["y", "n", "s"]:
answer = input("Does this look good? [y/n/s]? ").lower()
if answer == "s":
if match:
self._clean(match=match)
self.log.debug(f"Skipping changes to {match}...")
return answer
if not answer == "y" and bail:
self._exit()
return answer
def _message(self):
return input("Commit message? ")
def _commit(self):
for path in self.paths:
command = "git --no-pager diff"
output = self._shell(command, cwd=path)
if not output:
self.log.debug(f"No changes detected in {path}, continuing...")
continue
self.log.info("=" * 79)
self.log.info(f"{path}")
self.log.info("=" * 79)
self._shell(command, check=False, cwd=path)
if self._confirm() == "s":
self._shell("git checkout .", check=False, cwd=path)
self.log.debug(f"Skipping {path} as requested...")
continue
if not self.commit_msg:
self.commit_msg = self._message()
self._shell("git add .", check=False, cwd=path)
self._shell(f"git commit -m '{self.commit_msg}'", check=False, cwd=path)
self._shell("git push", check=False, cwd=path)
def _validate(self):
if not hasattr(self, "GLOB"):
self._exit(msg="Missing GLOB attribute!")
self.log.debug("Validation succeeded!")
def _diff(self, match, idx, check=True):
command = "git --no-pager diff"
root_path = Path(match).parent
if check:
output = self._shell(command, cwd=root_path)
if not output:
self.log.debug("No changes detected, moving on...")
return False
self.log.info("=" * 79)
self.log.info(f"{match.upper()}")
self.log.info("=" * 79)
self.log.debug(f"Diffing {root_path} ({idx+1}/{self.DIFF_LIMIT})")
self._shell(command, check=False, cwd=root_path)
return self._confirm(match=match)
def _clean(self, match=None, branch=False, pull=False):
if match:
_paths = [Path(match).parent]
else:
_paths = self.paths
for _path in _paths:
self.log.debug(f"Cleaning {_path} of local changes...")
self._shell("git checkout .", check=False, cwd=_path)
if pull:
self.log.debug(f"Pulling latest changes in {_path}...")
self._shell("git pull --rebase", check=False, cwd=_path)
if branch:
self.log.debug("Checking out the default branch...")
self._shell(
(
"git checkout main > /dev/null 2>&1 "
"|| git checkout master > /dev/null 2>&1"
),
check=False,
shell=True,
cwd=_path,
)
def _run(self):
idx = 0
for match in self.matches:
self._clean(match=match, branch=True)
with open(match, "r") as handle:
self.log.debug(f"Processing {match}...")
contents = handle.read()
if self.args.yaml:
self.log.debug("Attempting to load YAML...")
if "---" in contents:
self.yaml.explicit_start = True
else:
self.yaml.explicit_start = False
contents = self.yaml.load(contents)
migrated = self.migrate(contents)
self.log.debug(f"Migrated {match}...")
with open(match, "w") as handle:
if self.args.yaml:
self.yaml.dump(migrated, handle)
else:
handle.write(migrated)
if idx < self.DIFF_LIMIT:
if self._diff(match, idx=idx, check=True):
idx += 1
self.log.debug(f"Saved {match} back to the file system...")
self.log.debug("Finished migrating files...")
self.log.debug("Commencing change commit run...")
self.log.info("=" * 79)
self.log.info("COMMIT AND PUSH (CHANGES APPLIED)")
self.log.info("=" * 79)
self._commit()
self.log.debug("Finished committing changes...")
self.log.info("Finished! May your tyops be ever glorious!")