Created
June 15, 2022 16:05
-
-
Save Morreski/b6575e5d466236fa4f55b031d13610c9 to your computer and use it in GitHub Desktop.
Quick and dirty script that print TODOs and FIXMEs in a git repository sorted by commit date
This file contains hidden or 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/python3 | |
import os | |
import sys | |
import subprocess | |
import argparse | |
import dataclasses | |
from typing import Iterable, Optional | |
from datetime import datetime, timezone | |
@dataclasses.dataclass | |
class Todo: | |
filepath: str | |
line: str | |
line_number: int | |
time: datetime | |
type_: str | |
def __post_init__(self) -> None: | |
if self.time is None: | |
self.time = datetime.now(timezone.utc) | |
def parse_args(): | |
parser = argparse.ArgumentParser() | |
return parser.parse_args() | |
def main(): | |
dirpath = '.' # TODO: make this a program arg | |
assert_git_directory(dirpath) | |
blacklist = load_blacklist() | |
todos = search_for_todos(dirpath, blacklist) | |
display_todos(todos) | |
def assert_git_directory(dirpath: str) -> bool: | |
is_git = os.path.exists(os.path.join(dirpath, '.git')) | |
if not is_git: | |
print(f"{sys.argv[0]} must be run in git directory") | |
sys.exit(1) | |
def load_blacklist() -> Iterable[str]: | |
submodules = subprocess.check_output(['git', 'submodule', 'status']).decode().split('\n') | |
blacklist = [] | |
for line in submodules: | |
if line == '': | |
continue | |
blacklist.append(line.strip().split(' ')[1]) | |
return blacklist | |
def is_in_blacklist(filepath: str, blacklist: Iterable[str]) -> bool: | |
return any(filepath.strip('./').startswith(bad.strip('./')) for bad in blacklist) | |
def search_for_todos(dirpath: str, blacklist: Iterable[str] = None) -> Iterable[Todo]: | |
res = subprocess.check_output(['rg', '-n', 'TODO|FIXME', dirpath]) | |
return parse_rg_output(res, blacklist) | |
def parse_rg_output(data: bytes, blacklist: Iterable[str]) -> Iterable[Todo]: | |
for line in data.decode().split(os.linesep): | |
if line == '': | |
continue | |
filepath, remaining = line.split(":", 1) | |
if is_in_blacklist(filepath, blacklist): | |
continue | |
line_number, line_content = remaining.split(":", 1) | |
date = get_last_edit_date_from_line(filepath, line_number) | |
yield Todo(filepath, line_content.strip(), int(line_number), date, type_="FIXME" if "FIXME" in line else "TODO") | |
def get_last_edit_date_from_line(filepath: str, line_number: int) -> Optional[datetime]: | |
sed = subprocess.Popen(['sed', '-n', f"{line_number}p"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) | |
blame = subprocess.run(['git', 'blame', '-c', filepath], stdout=sed.stdin, stderr=subprocess.PIPE) | |
sed_out, _ = sed.communicate() | |
if blame.returncode == 128: | |
return None # This file is probably not versionned yet (or gitignored) | |
try: | |
raw_date = sed_out.decode().split('\t')[2] | |
date_str = make_shit_iso(raw_date) | |
except IndexError: | |
print(f"Warning: could not get date for todo: {filepath} at line {line_number}", file=sys.stderr) | |
return None | |
return datetime.fromisoformat(date_str) | |
def make_shit_iso(rawdate: str) -> str: | |
partial = rawdate.replace(' ', 'T', 1).replace(' ', '') | |
return partial[:-2] + ':' + partial[-2:] | |
def display_todos(todos: Iterable[Todo]) -> None: | |
if sys.stdout.isatty(): | |
orange = "\033[93m" | |
default_color = "\033[0m" | |
else: | |
orange = '' | |
default_color = '' | |
for todo in sorted(todos, key=lambda t: t.time): | |
formatted_time = todo.time.astimezone().strftime('%Y-%m-%d %H:%M:%S') | |
color = orange if todo.type_ == "FIXME" else default_color | |
print(f"{color}{formatted_time}\t{todo.filepath}:{todo.line_number}\t{todo.line}{default_color}") | |
if __name__ == "__main__": | |
args = parse_args() | |
main(**args.__dict__) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment