2021-07-06 11:54:26 +00:00
|
|
|
"""Automate your mass typo updates."""
|
2021-07-06 11:21:31 +00:00
|
|
|
|
2021-07-10 11:05:51 +00:00
|
|
|
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
|
2021-07-06 11:21:31 +00:00
|
|
|
from sys import exit
|
|
|
|
|
2021-07-10 11:05:51 +00:00
|
|
|
from ruamel.yaml import YAML
|
|
|
|
|
2021-07-06 11:21:31 +00:00
|
|
|
|
|
|
|
class Migration:
|
2021-07-10 11:05:51 +00:00
|
|
|
|
|
|
|
DIFF_LIMIT = 3
|
|
|
|
|
2021-07-06 11:21:31 +00:00
|
|
|
def __init__(self):
|
2021-07-10 11:05:51 +00:00
|
|
|
self.args = self._parse()
|
2021-07-10 12:21:34 +00:00
|
|
|
self.yaml = self._init_yaml()
|
2021-07-10 11:05:51 +00:00
|
|
|
self.log = self._init_logging()
|
2021-07-10 12:21:34 +00:00
|
|
|
self.commit_msg = None
|
2021-07-10 11:05:51 +00:00
|
|
|
|
2021-07-10 12:21:34 +00:00
|
|
|
self._validate()
|
2021-07-10 11:05:51 +00:00
|
|
|
if self.args.validate:
|
2021-07-10 13:18:43 +00:00
|
|
|
self._exit(code=0, output=False)
|
2021-07-10 11:05:51 +00:00
|
|
|
|
|
|
|
self.matches = glob(expanduser(self.GLOB))
|
2021-07-10 12:21:34 +00:00
|
|
|
self.paths = list(set([Path(p).parent for p in self.matches]))
|
|
|
|
|
2021-07-10 13:18:43 +00:00
|
|
|
self.log.debug(f"Discovered '{self.GLOB}' as user defined glob")
|
|
|
|
self.log.debug(f"Matched paths are: {[m for m in self.matches]}")
|
2021-07-10 11:05:51 +00:00
|
|
|
|
|
|
|
if self.args.reset:
|
2021-07-10 12:21:34 +00:00
|
|
|
self._clean()
|
2021-07-10 13:18:43 +00:00
|
|
|
self._exit(code=0, output=False)
|
2021-07-10 11:05:51 +00:00
|
|
|
|
|
|
|
try:
|
2021-07-10 13:35:48 +00:00
|
|
|
self.log.info("=" * 79)
|
|
|
|
self.log.info("RUNNING MIGRATIONS + CHECKS (ONLY GIT LOCAL CHANGES)")
|
|
|
|
self.log.info("=" * 79)
|
2021-07-10 11:05:51 +00:00
|
|
|
self._run()
|
|
|
|
except Exception as exception:
|
2021-07-10 12:21:34 +00:00
|
|
|
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
|
2021-07-10 12:41:30 +00:00
|
|
|
yaml.width = 999999
|
2021-07-10 12:21:34 +00:00
|
|
|
return yaml
|
|
|
|
|
2021-07-10 13:18:43 +00:00
|
|
|
def _exit(self, msg="Bailing out on request...", code=1, output=True):
|
|
|
|
if output:
|
|
|
|
self.log.info(msg)
|
2021-07-10 12:21:34 +00:00
|
|
|
exit(code)
|
2021-07-10 11:05:51 +00:00
|
|
|
|
|
|
|
def _parse(self):
|
2021-07-10 12:21:34 +00:00
|
|
|
description = "Tyop: automate your mass typo updates"
|
|
|
|
parser = ArgumentParser(description=description)
|
2021-07-10 11:05:51 +00:00
|
|
|
|
2021-07-10 12:21:34 +00:00
|
|
|
parser.add_argument(
|
2021-07-10 11:05:51 +00:00
|
|
|
"-d",
|
|
|
|
"--debug",
|
|
|
|
help="enable verbose debug logs",
|
|
|
|
action="store_const",
|
|
|
|
dest="log",
|
|
|
|
const=DEBUG,
|
|
|
|
default=INFO,
|
|
|
|
)
|
2021-07-10 12:21:34 +00:00
|
|
|
parser.add_argument(
|
2021-07-10 11:05:51 +00:00
|
|
|
"-v",
|
|
|
|
"--validate",
|
|
|
|
default=False,
|
|
|
|
action="store_true",
|
|
|
|
dest="validate",
|
|
|
|
help="Validate end-user defined migrationa and exit",
|
|
|
|
)
|
2021-07-10 12:21:34 +00:00
|
|
|
parser.add_argument(
|
2021-07-10 11:05:51 +00:00
|
|
|
"-r",
|
|
|
|
"--reset",
|
|
|
|
default=False,
|
|
|
|
action="store_true",
|
|
|
|
dest="reset",
|
|
|
|
help="Reset changes without running migrations (git-checkout)",
|
|
|
|
)
|
2021-07-10 12:21:34 +00:00
|
|
|
parser.add_argument(
|
2021-07-10 11:05:51 +00:00
|
|
|
"-y",
|
|
|
|
"--yaml",
|
|
|
|
default=False,
|
|
|
|
action="store_true",
|
|
|
|
dest="yaml",
|
|
|
|
help="Expect YAML and load for parsing",
|
|
|
|
)
|
|
|
|
|
2021-07-10 12:21:34 +00:00
|
|
|
return parser.parse_args()
|
2021-07-10 11:05:51 +00:00
|
|
|
|
|
|
|
def _init_logging(self):
|
|
|
|
basicConfig(level=self.args.log, format="%(levelname)-8s %(message)s")
|
2021-07-10 12:21:34 +00:00
|
|
|
return getLogger(__name__)
|
2021-07-10 11:05:51 +00:00
|
|
|
|
|
|
|
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:
|
2021-07-10 12:21:34 +00:00
|
|
|
self._exit(msg=f"Failed to run {cmd}, saw {str(exception)}")
|
2021-07-06 11:21:31 +00:00
|
|
|
|
2021-07-10 13:18:43 +00:00
|
|
|
def _confirm(self, bail=True, match=None):
|
2021-07-10 11:05:51 +00:00
|
|
|
answer = ""
|
2021-07-10 12:21:34 +00:00
|
|
|
|
2021-07-10 13:18:43 +00:00
|
|
|
while answer not in ["y", "n", "s"]:
|
|
|
|
answer = input("Does this look good? [y/n/s]? ").lower()
|
2021-07-10 12:21:34 +00:00
|
|
|
|
2021-07-10 13:18:43 +00:00
|
|
|
if answer == "s":
|
|
|
|
if match:
|
|
|
|
self._clean(match=match)
|
|
|
|
self.log.debug(f"Skipping changes to {match}...")
|
|
|
|
return answer
|
2021-07-10 12:21:34 +00:00
|
|
|
|
2021-07-10 13:18:43 +00:00
|
|
|
if not answer == "y" and bail:
|
2021-07-10 12:21:34 +00:00
|
|
|
self._exit()
|
|
|
|
|
2021-07-10 13:18:43 +00:00
|
|
|
return answer
|
2021-07-10 11:05:51 +00:00
|
|
|
|
|
|
|
def _message(self):
|
2021-07-10 12:21:34 +00:00
|
|
|
return input("Commit message? ")
|
2021-07-10 11:05:51 +00:00
|
|
|
|
2021-07-10 12:41:30 +00:00
|
|
|
def _commit(self):
|
2021-07-10 13:18:43 +00:00
|
|
|
for path in self.paths:
|
2021-07-10 13:35:48 +00:00
|
|
|
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)
|
2021-07-10 13:18:43 +00:00
|
|
|
|
|
|
|
if self._confirm() == "s":
|
2021-07-10 13:35:48 +00:00
|
|
|
self._shell("git checkout .", check=False, cwd=path)
|
2021-07-10 13:18:43 +00:00
|
|
|
self.log.debug(f"Skipping {path} as requested...")
|
|
|
|
continue
|
2021-07-10 12:41:30 +00:00
|
|
|
|
2021-07-10 12:21:34 +00:00
|
|
|
if not self.commit_msg:
|
|
|
|
self.commit_msg = self._message()
|
2021-07-10 12:41:30 +00:00
|
|
|
|
|
|
|
self._shell("git add .", check=False, cwd=path)
|
|
|
|
self._shell(f"git commit -m '{self.commit_msg}'", check=False, cwd=path)
|
2021-07-10 13:18:43 +00:00
|
|
|
self._shell("git push", check=False, cwd=path)
|
2021-07-10 11:05:51 +00:00
|
|
|
|
|
|
|
def _validate(self):
|
2021-07-06 11:21:31 +00:00
|
|
|
if not hasattr(self, "GLOB"):
|
2021-07-10 12:21:34 +00:00
|
|
|
self._exit(msg="Missing GLOB attribute!")
|
2021-07-10 13:18:43 +00:00
|
|
|
self.log.debug("Validation succeeded!")
|
2021-07-10 11:05:51 +00:00
|
|
|
|
2021-07-10 12:21:34 +00:00
|
|
|
def _diff(self, match, idx, check=True):
|
|
|
|
command = "git --no-pager diff"
|
2021-07-10 11:05:51 +00:00
|
|
|
root_path = Path(match).parent
|
2021-07-06 11:21:31 +00:00
|
|
|
|
2021-07-10 12:21:34 +00:00
|
|
|
if check:
|
|
|
|
output = self._shell(command, cwd=root_path)
|
|
|
|
if not output:
|
2021-07-10 13:18:43 +00:00
|
|
|
self.log.debug("No changes detected, moving on...")
|
2021-07-10 12:41:30 +00:00
|
|
|
return False
|
2021-07-10 11:05:51 +00:00
|
|
|
|
2021-07-10 13:18:43 +00:00
|
|
|
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})")
|
2021-07-10 12:21:34 +00:00
|
|
|
self._shell(command, check=False, cwd=root_path)
|
2021-07-10 11:05:51 +00:00
|
|
|
|
2021-07-10 13:18:43 +00:00
|
|
|
return self._confirm(match=match)
|
2021-07-10 12:21:34 +00:00
|
|
|
|
2021-07-10 13:35:48 +00:00
|
|
|
def _clean(self, match=None, branch=False, pull=False):
|
2021-07-10 12:21:34 +00:00
|
|
|
if match:
|
|
|
|
_paths = [Path(match).parent]
|
|
|
|
else:
|
|
|
|
_paths = self.paths
|
|
|
|
|
|
|
|
for _path in _paths:
|
2021-07-10 13:18:43 +00:00
|
|
|
self.log.debug(f"Cleaning {_path} of local changes...")
|
2021-07-10 12:21:34 +00:00
|
|
|
|
|
|
|
self._shell("git checkout .", check=False, cwd=_path)
|
|
|
|
|
2021-07-10 13:35:48 +00:00
|
|
|
if pull:
|
2021-07-15 14:09:05 +00:00
|
|
|
self.log.debug(f"Pulling latest changes in {_path}...")
|
2021-07-10 13:35:48 +00:00
|
|
|
self._shell("git pull --rebase", check=False, cwd=_path)
|
|
|
|
|
2021-07-10 12:21:34 +00:00
|
|
|
if branch:
|
2021-07-10 13:18:43 +00:00
|
|
|
self.log.debug("Checking out the default branch...")
|
2021-07-10 12:21:34 +00:00
|
|
|
self._shell(
|
|
|
|
(
|
|
|
|
"git checkout main > /dev/null 2>&1 "
|
|
|
|
"|| git checkout master > /dev/null 2>&1"
|
|
|
|
),
|
|
|
|
check=False,
|
|
|
|
shell=True,
|
|
|
|
cwd=_path,
|
|
|
|
)
|
2021-07-10 11:05:51 +00:00
|
|
|
|
|
|
|
def _run(self):
|
|
|
|
idx = 0
|
|
|
|
|
2021-07-10 12:21:34 +00:00
|
|
|
for match in self.matches:
|
2021-07-15 22:05:20 +00:00
|
|
|
self._clean(match=match, branch=True)
|
2021-07-10 11:05:51 +00:00
|
|
|
|
|
|
|
with open(match, "r") as handle:
|
2021-07-10 13:18:43 +00:00
|
|
|
self.log.debug(f"Processing {match}...")
|
2021-07-10 11:05:51 +00:00
|
|
|
contents = handle.read()
|
|
|
|
|
|
|
|
if self.args.yaml:
|
|
|
|
self.log.debug("Attempting to load YAML...")
|
2021-07-10 12:21:34 +00:00
|
|
|
|
|
|
|
if "---" in contents:
|
|
|
|
self.yaml.explicit_start = True
|
|
|
|
else:
|
|
|
|
self.yaml.explicit_start = False
|
|
|
|
|
2021-07-10 11:05:51 +00:00
|
|
|
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)
|
|
|
|
|
2021-07-10 13:35:48 +00:00
|
|
|
if idx < self.DIFF_LIMIT:
|
2021-07-10 12:41:30 +00:00
|
|
|
if self._diff(match, idx=idx, check=True):
|
|
|
|
idx += 1
|
2021-07-10 11:05:51 +00:00
|
|
|
|
2021-07-10 12:21:34 +00:00
|
|
|
self.log.debug(f"Saved {match} back to the file system...")
|
|
|
|
|
2021-07-10 13:18:43 +00:00
|
|
|
self.log.debug("Finished migrating files...")
|
|
|
|
self.log.debug("Commencing change commit run...")
|
2021-07-10 11:05:51 +00:00
|
|
|
|
2021-07-10 13:35:48 +00:00
|
|
|
self.log.info("=" * 79)
|
|
|
|
self.log.info("COMMIT AND PUSH (CHANGES APPLIED)")
|
|
|
|
self.log.info("=" * 79)
|
2021-07-10 12:41:30 +00:00
|
|
|
self._commit()
|
2021-07-10 11:05:51 +00:00
|
|
|
|
2021-07-10 13:18:43 +00:00
|
|
|
self.log.debug("Finished committing changes...")
|
2021-07-10 11:05:51 +00:00
|
|
|
self.log.info("Finished! May your tyops be ever glorious!")
|