1
0
This repository has been archived on 2024-10-13. You can view files and clone it, but cannot push or open issues or pull requests.
git-time-tracker/git_time_tracker/__init__.py

194 lines
4.9 KiB
Python
Raw Normal View History

2024-07-09 22:15:45 +00:00
"""# 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)
"""
2024-06-03 20:42:05 +00:00
import argparse
2024-06-03 21:01:45 +00:00
import datetime
2024-06-08 12:43:44 +00:00
import logging
import sys
from collections.abc import Iterator
2024-06-03 21:13:16 +00:00
from pathlib import Path
2024-06-03 20:42:05 +00:00
2024-08-25 22:57:51 +00:00
from .data import (
HIDDEN_LABEL_PREFIX,
HIDDEN_LABEL_TOTAL,
RealizedWorkSample,
WorkSample,
)
2024-08-25 22:26:04 +00:00
from .format import cli, icalendar
2024-09-20 22:32:19 +00:00
from .source import csv_file, git_repo
2024-06-03 22:15:18 +00:00
logger = logging.getLogger(__name__)
2024-06-08 12:43:44 +00:00
DEFAULT_ESTIMATED_DURATION = datetime.timedelta(hours=1)
2024-06-08 12:43:44 +00:00
ZERO_DURATION = datetime.timedelta(seconds=0)
HOUR = datetime.timedelta(hours=1)
2024-06-08 12:49:26 +00:00
MINUTE = datetime.timedelta(minutes=1)
2024-06-08 12:43:44 +00:00
2024-06-03 21:49:42 +00:00
2024-08-25 22:57:51 +00:00
def filter_samples(
2024-09-20 22:32:19 +00:00
samples: list[WorkSample],
sample_filter: set[str],
2024-08-25 22:57:51 +00:00
) -> list[WorkSample]:
assert len(sample_filter) > 0
2024-08-25 22:26:04 +00:00
return [s for s in samples if set(s.labels).intersection(sample_filter)]
2024-08-25 22:57:51 +00:00
def heuristically_realize_samples(
samples: list[WorkSample],
) -> Iterator[RealizedWorkSample]:
"""Secret sauce.
Guarentees that:
* No samples overlap.
"""
2024-09-26 22:03:41 +00:00
previous_sample_end = None
for sample in samples:
end_at = sample.end_at
2024-09-26 22:03:41 +00:00
if previous_sample_end is None:
if end_at.tzinfo:
previous_sample_end = datetime.datetime.fromtimestamp(0, datetime.UTC)
else:
previous_sample_end = datetime.datetime.fromtimestamp(0)
assert previous_sample_end <= end_at, 'Iterating in incorrect order'
# TODO: Allow end_at is None
start_at = sample.start_at
if start_at is None:
estimated_duration: datetime.timedelta = DEFAULT_ESTIMATED_DURATION
start_at = max(previous_sample_end, end_at - estimated_duration)
del estimated_duration
yield RealizedWorkSample(labels=sample.labels, end_at=end_at, start_at=start_at)
previous_sample_end = sample.end_at
del sample
2024-08-25 22:57:51 +00:00
def parse_arguments():
parser = argparse.ArgumentParser()
2024-08-25 21:41:42 +00:00
parser.add_argument(
'--git-repo',
action='extend',
nargs='+',
type=Path,
dest='repositories',
2024-08-27 19:04:37 +00:00
default=[],
)
parser.add_argument(
'--csv-file',
action='extend',
nargs='+',
type=Path,
dest='csv_files',
default=[],
2024-08-25 21:41:42 +00:00
)
parser.add_argument(
'--filter',
action='extend',
nargs='+',
type=str,
dest='sample_filter',
default=[],
)
2024-08-25 22:26:04 +00:00
parser.add_argument(
'--format',
action='store',
type=str,
dest='format_mode',
default='cli_report',
choices=['cli_report', 'icalendar'],
)
2024-09-26 22:03:41 +00:00
parser.add_argument(
'--out',
action='store',
type=Path,
dest='output_file',
default='output/samples.ics',
)
return parser.parse_args()
2024-09-20 22:32:19 +00:00
2024-09-26 22:03:41 +00:00
def load_samples(args) -> set[WorkSample]:
2024-08-27 19:04:37 +00:00
shared_time_stamps_set: set[WorkSample] = set()
# Git repositories
for repo_path in args.repositories:
logger.warning('Determine commits from %s', repo_path)
shared_time_stamps_set |= set(
git_repo.iterate_samples_from_git_repository(repo_path),
)
del repo_path
# CSV Files
for csv_path in args.csv_files:
logger.warning('Load samples from %s', csv_path)
shared_time_stamps_set |= set(
csv_file.iterate_samples_from_csv_file(csv_path),
)
del csv_path
return shared_time_stamps_set
2024-09-20 22:32:19 +00:00
2024-06-03 20:42:05 +00:00
def main():
2024-06-03 22:15:18 +00:00
logging.basicConfig()
2024-06-03 20:42:05 +00:00
args = parse_arguments()
2024-08-27 19:04:37 +00:00
# Determine samples
shared_time_stamps_set = load_samples(args)
2024-06-03 21:49:42 +00:00
2024-08-27 19:04:37 +00:00
# Sort samples
2024-08-25 22:57:51 +00:00
shared_time_stamps = sorted(shared_time_stamps_set, key=lambda s: s.end_at)
del shared_time_stamps_set
2024-08-27 19:04:37 +00:00
# Filter samples
sample_filter = args.sample_filter
if len(sample_filter) != 0:
logger.warning('Filtering %s samples', len(shared_time_stamps))
shared_time_stamps = filter_samples(shared_time_stamps, sample_filter)
logger.warning('Filtered down to %s samples', len(shared_time_stamps))
2024-08-27 19:04:37 +00:00
# Heuristic samples
logger.warning('Realizing %s samples', len(shared_time_stamps))
2024-08-25 22:26:04 +00:00
shared_time_stamps = list(heuristically_realize_samples(shared_time_stamps))
2024-06-03 21:49:42 +00:00
2024-08-27 19:04:37 +00:00
# Output format
2024-08-25 22:26:04 +00:00
if args.format_mode == 'cli_report':
for t in cli.generate_report(shared_time_stamps):
sys.stdout.write(t)
elif args.format_mode == 'icalendar':
2024-08-25 22:57:51 +00:00
icalendar.generate_icalendar_file(
2024-09-20 22:32:19 +00:00
shared_time_stamps,
2024-09-26 22:03:41 +00:00
file=args.output_file,
2024-08-25 22:57:51 +00:00
)