1
0

Compare commits

..

2 Commits

Author SHA1 Message Date
41e574c971
Ruff
All checks were successful
Test Python / Test (push) Successful in 22s
2024-08-25 23:41:42 +02:00
64980e46ac
Restructure to split sources into own submodule.
Preparation for addition of extra sources.
2024-08-25 23:39:53 +02:00
5 changed files with 103 additions and 70 deletions

View File

@ -26,67 +26,18 @@ And the ([Hamster](https://github.com/projecthamster/hamster)) manual time track
""" """
import argparse import argparse
import dataclasses
import datetime import datetime
import logging import logging
import sys import sys
import time from collections.abc import Iterator
from collections.abc import Iterator, Sequence
from pathlib import Path from pathlib import Path
import git from .data import HIDDEN_LABEL_PREFIX, HIDDEN_LABEL_TOTAL, WorkSample
from .source import git_repo
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@dataclasses.dataclass(frozen=True, order=True)
class WorkSample:
registered_at: datetime.datetime
labels: Sequence[str]
def determine_default(repo: git.Repo):
try:
repo.commit('main')
return 'main'
except:
return 'master'
HIDDEN_LABEL_PREFIX = '__'
HIDDEN_LABEL_TOTAL = HIDDEN_LABEL_PREFIX + 'TOTAL'
def determine_project_name(repo: git.Repo) -> str:
remotes = repo.remotes
if len(remotes) > 0:
return remotes.origin.url.removeprefix('git@gitfub.space:')
return Path(repo.working_tree_dir).name
def get_samples_from_project(repo: git.Repo) -> Iterator[WorkSample]:
project_name = determine_project_name(repo)
assert project_name is not None
# TODO: Branch on main or master or default
repo.commit()
for commit in repo.iter_commits(determine_default(repo)):
labels = [HIDDEN_LABEL_TOTAL]
labels.append('project:' + project_name)
labels.append('author:' + commit.author.email)
yield WorkSample(
datetime.datetime.fromtimestamp(commit.authored_date, tz=datetime.UTC),
tuple(labels),
)
yield WorkSample(
datetime.datetime.fromtimestamp(commit.committed_date, tz=datetime.UTC),
tuple(labels),
)
del labels
DEFAULT_EST_TIME = datetime.timedelta(hours=1) DEFAULT_EST_TIME = datetime.timedelta(hours=1)
ZERO_DURATION = datetime.timedelta(seconds=0) ZERO_DURATION = datetime.timedelta(seconds=0)
@ -98,8 +49,8 @@ def fmt_year_ranges_internal(years: list[int]) -> Iterator[str]:
years = sorted(years) years = sorted(years)
for idx, year in enumerate(years): for idx, year in enumerate(years):
at_end = idx == len(years) - 1 at_end = idx == len(years) - 1
range_before = idx > 0 and years[idx-1] == year - 1 range_before = idx > 0 and years[idx - 1] == year - 1
range_after = not at_end 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: if not range_before or not range_after:
yield str(year) yield str(year)
@ -110,15 +61,20 @@ def fmt_year_ranges_internal(years: list[int]) -> Iterator[str]:
elif not range_after: elif not range_after:
yield ',' yield ','
def fmt_year_ranges(years: list[int]) -> str: def fmt_year_ranges(years: list[int]) -> str:
return ''.join(list(fmt_year_ranges_internal(years))) return ''.join(list(fmt_year_ranges_internal(years)))
def fmt_line(label_type: str, label: str, total_time: datetime.timedelta) -> str: def fmt_line(label_type: str, label: str, total_time: datetime.timedelta) -> str:
hours = int(total_time / HOUR) hours = int(total_time / HOUR)
minutes = int((total_time - hours*HOUR)/MINUTE) minutes = int((total_time - hours * HOUR) / MINUTE)
return f' {label_type:10} {label:40} {hours:-4d}h {minutes:-2d}m' return f' {label_type:10} {label:40} {hours:-4d}h {minutes:-2d}m'
def generate_report(samples: list[WorkSample], sample_filter = frozenset()) -> Iterator[str]:
def generate_report(
samples: list[WorkSample], sample_filter=frozenset(),
) -> Iterator[str]:
LABEL_FILTER = {} LABEL_FILTER = {}
# Time spent per label # Time spent per label
@ -137,7 +93,7 @@ def generate_report(samples: list[WorkSample], sample_filter = frozenset()) -> I
for label in sample.labels: for label in sample.labels:
time_per_label.setdefault(label, ZERO_DURATION) time_per_label.setdefault(label, ZERO_DURATION)
time_per_label[label] += est_time time_per_label[label] += est_time
years_per_label.setdefault(label,set()).add(sample.registered_at.year) years_per_label.setdefault(label, set()).add(sample.registered_at.year)
prev_time = sample.registered_at prev_time = sample.registered_at
del sample, est_time del sample, est_time
@ -159,7 +115,7 @@ def generate_report(samples: list[WorkSample], sample_filter = frozenset()) -> I
yield fmt_line(label_type, label, total_time) yield fmt_line(label_type, label, total_time)
yield ' (' yield ' ('
yield fmt_year_ranges(years_per_label.get(label_and_type,[])) yield fmt_year_ranges(years_per_label.get(label_and_type, []))
yield ')' yield ')'
yield '\n' yield '\n'
del label, total_time del label, total_time
@ -173,8 +129,17 @@ def generate_report(samples: list[WorkSample], sample_filter = frozenset()) -> I
def parse_arguments(): def parse_arguments():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('repositories', action='extend', nargs='+', type=Path) parser.add_argument(
parser.add_argument('--filter', action='extend', nargs='+', type=str, dest='sample_filter', default=[]) '--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() return parser.parse_args()
@ -185,13 +150,10 @@ def main():
shared_time_stamps: set[WorkSample] = set() shared_time_stamps: set[WorkSample] = set()
for repo_path in args.repositories: for repo_path in args.repositories:
try:
repo = git.Repo(repo_path)
except git.exc.InvalidGitRepositoryError:
logger.warning('Ignoring non-repo %s', repo_path)
continue
logger.warning('Visit %s', repo_path) logger.warning('Visit %s', repo_path)
shared_time_stamps |= set(get_samples_from_project(repo)) shared_time_stamps |= set(
git_repo.iterate_samples_from_git_repository(repo_path),
)
shared_time_stamps = sorted(shared_time_stamps) shared_time_stamps = sorted(shared_time_stamps)

12
git_time_tracker/data.py Normal file
View File

@ -0,0 +1,12 @@
import dataclasses
import datetime
from collections.abc import Sequence
HIDDEN_LABEL_PREFIX = '__'
HIDDEN_LABEL_TOTAL = HIDDEN_LABEL_PREFIX + 'TOTAL'
@dataclasses.dataclass(frozen=True, order=True)
class WorkSample:
registered_at: datetime.datetime
labels: Sequence[str]

View File

View File

@ -0,0 +1,55 @@
import datetime
import logging
from collections.abc import Iterator
from pathlib import Path
import git
from ..data import HIDDEN_LABEL_TOTAL, WorkSample
logger = logging.getLogger(__name__)
def determine_default_branch(repo: git.Repo):
try:
repo.commit('main')
return 'main'
except:
return 'master'
def determine_project_name(repo: git.Repo) -> str:
remotes = repo.remotes
if len(remotes) > 0:
return remotes.origin.url.removeprefix('git@gitfub.space:')
return Path(repo.working_tree_dir).name
def get_samples_from_project(repo: git.Repo) -> Iterator[WorkSample]:
project_name = determine_project_name(repo)
assert project_name is not None
# TODO: Branch on main or master or default
repo.commit()
for commit in repo.iter_commits(determine_default_branch(repo)):
labels = [HIDDEN_LABEL_TOTAL]
labels.append('project:' + project_name)
labels.append('author:' + commit.author.email)
yield WorkSample(
datetime.datetime.fromtimestamp(commit.authored_date, tz=datetime.UTC),
tuple(labels),
)
yield WorkSample(
datetime.datetime.fromtimestamp(commit.committed_date, tz=datetime.UTC),
tuple(labels),
)
del labels
def iterate_samples_from_git_repository(repo_path: Path) -> Iterator[WorkSample]:
try:
yield from get_samples_from_project(git.Repo(repo_path))
except git.exc.InvalidGitRepositoryError:
logger.warning('Ignoring non-repo %s', repo_path)

View File

@ -1,13 +1,17 @@
import git_time_tracker import git_time_tracker
def test_year_ranges_1(): def test_year_ranges_1():
assert git_time_tracker.fmt_year_ranges([1,2,3]) == '1-3' assert git_time_tracker.fmt_year_ranges([1, 2, 3]) == '1-3'
def test_year_ranges_2(): def test_year_ranges_2():
assert git_time_tracker.fmt_year_ranges([1,3]) == '1,3' assert git_time_tracker.fmt_year_ranges([1, 3]) == '1,3'
def test_year_ranges_3(): def test_year_ranges_3():
assert git_time_tracker.fmt_year_ranges([1,2,4]) == '1-2,4' assert git_time_tracker.fmt_year_ranges([1, 2, 4]) == '1-2,4'
def test_year_ranges_4(): def test_year_ranges_4():
assert git_time_tracker.fmt_year_ranges([1,2,4,5]) == '1-2,4-5' assert git_time_tracker.fmt_year_ranges([1, 2, 4, 5]) == '1-2,4-5'