Skip to content

Instantly share code, notes, and snippets.

@Morreski
Created June 15, 2022 16:05
Show Gist options
  • Save Morreski/b6575e5d466236fa4f55b031d13610c9 to your computer and use it in GitHub Desktop.
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
#!/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