1
0
git-time-tracker/git_time_tracker/__init__.py
Jon Michael Aanes 41e574c971
All checks were successful
Test Python / Test (push) Successful in 22s
Ruff
2024-08-25 23:41:42 +02:00

162 lines
4.3 KiB
Python

"""# Git-based Time Tracker.
Quick and dirty time tracker on git histories.
Uses the simple heuristics that each commit takes precisely one hour of work.
It will automatically trim commits below one hour if another commit occurred
less than an hour ago.
Usage:
```
python -m git_time_tracker REPO1 REPO2...
```
# Obligatory
This tool reports:
```
project Jmaa/git-time-tracker.git 3h 33m (2024)
```
And the ([Hamster](https://github.com/projecthamster/hamster)) manual time tracker reports:
![](docs/obligatory-hamster.png)
"""
import argparse
import datetime
import logging
import sys
from collections.abc import Iterator
from pathlib import Path
from .data import HIDDEN_LABEL_PREFIX, HIDDEN_LABEL_TOTAL, WorkSample
from .source import git_repo
logger = logging.getLogger(__name__)
DEFAULT_EST_TIME = datetime.timedelta(hours=1)
ZERO_DURATION = datetime.timedelta(seconds=0)
HOUR = datetime.timedelta(hours=1)
MINUTE = datetime.timedelta(minutes=1)
def fmt_year_ranges_internal(years: list[int]) -> Iterator[str]:
years = sorted(years)
for idx, year in enumerate(years):
at_end = idx == len(years) - 1
range_before = idx > 0 and years[idx - 1] == year - 1
range_after = not at_end and years[idx + 1] == year + 1
if not range_before or not range_after:
yield str(year)
if not at_end:
if not range_before and range_after:
yield '-'
elif not range_after:
yield ','
def fmt_year_ranges(years: list[int]) -> str:
return ''.join(list(fmt_year_ranges_internal(years)))
def fmt_line(label_type: str, label: str, total_time: datetime.timedelta) -> str:
hours = int(total_time / HOUR)
minutes = int((total_time - hours * HOUR) / MINUTE)
return f' {label_type:10} {label:40} {hours:-4d}h {minutes:-2d}m'
def generate_report(
samples: list[WorkSample], sample_filter=frozenset(),
) -> Iterator[str]:
LABEL_FILTER = {}
# Time spent per label
time_per_label: dict[str, datetime.timedelta] = {}
years_per_label: dict[str, set[int]] = {}
prev_time = datetime.datetime.fromtimestamp(0, datetime.UTC)
for sample in samples:
est_time: datetime.timedelta = DEFAULT_EST_TIME
est_time = min(sample.registered_at - prev_time, est_time)
if len(sample_filter) == 0:
pass
elif not set(sample.labels).intersection(sample_filter):
continue
for label in sample.labels:
time_per_label.setdefault(label, ZERO_DURATION)
time_per_label[label] += est_time
years_per_label.setdefault(label, set()).add(sample.registered_at.year)
prev_time = sample.registered_at
del sample, est_time
time_and_label = [(duration, label) for label, duration in time_per_label.items()]
time_and_label.sort(reverse=True)
#
yield '-' * 66
yield '\n'
for total_time, label_and_type in time_and_label:
if label_and_type.startswith(HIDDEN_LABEL_PREFIX):
continue
label_type, label = label_and_type.split(':', 1)
if len(LABEL_FILTER) > 0 and label_type not in LABEL_FILTER:
continue
yield fmt_line(label_type, label, total_time)
yield ' ('
yield fmt_year_ranges(years_per_label.get(label_and_type, []))
yield ')'
yield '\n'
del label, total_time
yield '-' * 66
yield '\n'
yield fmt_line('', 'TOTAL', time_per_label.get(HIDDEN_LABEL_TOTAL, ZERO_DURATION))
yield '\n'
def parse_arguments():
parser = argparse.ArgumentParser()
parser.add_argument(
'--git-repo', action='extend', nargs='+', type=Path, dest='repositories',
)
parser.add_argument(
'--filter',
action='extend',
nargs='+',
type=str,
dest='sample_filter',
default=[],
)
return parser.parse_args()
def main():
logging.basicConfig()
args = parse_arguments()
shared_time_stamps: set[WorkSample] = set()
for repo_path in args.repositories:
logger.warning('Visit %s', repo_path)
shared_time_stamps |= set(
git_repo.iterate_samples_from_git_repository(repo_path),
)
shared_time_stamps = sorted(shared_time_stamps)
for t in generate_report(shared_time_stamps, sample_filter=args.sample_filter):
sys.stdout.write(t)