'
return self.msg.split(':', 2)[0].strip()
@property
def change(self) -> str:
if ':' not in self.msg:
return self.msg.strip()
return self.msg.split(':', 2)[1].strip()
@property
def url(self) -> str:
return f'https://github.com/getsolus/packages/commit/{self.ref}'
@property
def is_pr(self) -> bool:
return self.pr is not None
@property
def pr(self) -> Optional[GitHubPR]:
match = re.search(r'\(#(\d+)\)$', self.msg.splitlines()[0])
if not match:
return None
return GitHubPR.get(int(match.group(1)))
def to_md(self) -> str:
return f'[{self.msg}]({self.url})'
def to_html(self) -> str:
return f'{html.escape(self.msg)}'
def to_tty(self) -> str:
return (f'{TTY.Yellow}{TTY.url(self.ref, self.url)}{TTY.Reset} '
f'{self.date} '
f'{TTY.Green}{self.package}: {TTY.Reset}{self.change} '
f'{TTY.Blue}[{self.author}]{TTY.Reset} ' +
TTY.url(str(Symbols.link), self.url, alt=''))
class Git:
_cmd = ['git', '-c', 'color.ui=always', 'log', '--date=iso-strict',
'--reverse', '--pretty=format:%h%x1e%ad%x1e%s%x1e%an']
def commits_by_pkg(self, start: datetime, end: datetime) -> Dict[str, List[Commit]]:
commits: Dict[str, List[Commit]] = {}
for commit in self.commits(start, end):
commits[commit.package] = commits.get(commit.package, []) + [commit]
return commits
def commits(self, start: datetime, end: datetime, merges: bool = False) -> List[Commit]:
return [Commit.from_line(line) for line in self._commits(start, end, merges)]
def prs(self, start: datetime, end: datetime) -> List[GitHubPR]:
return [commit.pr for commit in self.commits(start, end, True)
if commit.pr is not None]
def _commits(self, start: datetime, end: datetime, merges: bool) -> List[str]:
cmd = self._cmd.copy()
if not merges:
cmd.append('--no-merges')
out = subprocess.Popen(cmd + [f'--after={start.isoformat()}', f'--before={end.isoformat()}'],
stdout=subprocess.PIPE, stderr=sys.stderr).stdout
if out is None:
return []
lines = out.read().decode('utf8').strip().split("\n")
if lines == ['']:
return []
return lines
def parse_date(date: str) -> datetime:
out = subprocess.Popen(['date', '-u', '--iso-8601=s', '--date=' + date],
stdout=subprocess.PIPE).stdout
if out is None:
raise ValueError(f'invalid date: {repr(date)}')
return datetime.fromisoformat(out.read().decode('utf8').strip())
class Printer:
def __init__(self, after: str, before: str):
self.start = parse_date(after)
self.end = parse_date(before)
self.builds = Builds()
self.git = Git()
def print(self, kind: str, format: str, sort: bool = False) -> None:
items = self._items(kind)
if sort:
items = sorted(items, key=lambda item: (item.package, item.date))
if format == 'html':
print(f'{len(items)} {cli_args.command}:
\n')
else:
print(f'{len(items)} {cli_args.command}:')
self._print(items, format)
if format == 'html':
print('
')
def follow(self, kind: str, format: str) -> None:
while True:
self.end = datetime.now(timezone.utc)
items = self._items(kind)
self._print(items, format)
if len(items) > 0:
self.start = max([i.date for i in items])
time.sleep(10)
def _items(self, kind: str) -> Sequence[Listable]:
match kind:
case 'builds':
return self.builds.during(self.start, self.end)
case 'updates':
return self.builds.updates(self.start, self.end)
case 'security-updates':
return self.builds.updates(self.start, self.end, security=True)
case 'commits':
return self.git.commits(self.start, self.end)
case 'highlights':
return [pr for pr in self.git.prs(self.start, self.end)
if pr.include_in_sync_notes]
case _:
raise ValueError(f'unsupported log kind: {kind}')
@staticmethod
def _print(items: Sequence[Listable], fmt: str) -> None:
for item in items:
Printer._print_item(item, fmt)
@staticmethod
def _print_item(item: Listable, fmt: str) -> None:
match fmt:
case 'tty':
print(item.to_tty())
case 'md':
print(f'- {item.to_md()}')
case 'html':
print(f'{item.to_html()}')
case _:
raise ValueError(f'unsupported format: {fmt}')
if __name__ == '__main__':
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent(
'''
examples:
List builds from the last week:
./worklog.py builds '1 week ago' now
Follow builds from the build server as they happen:
./worklog.py builds now --follow
List builds between 8:00 and 18:00 hours local time:
./worklog.py builds 8:00 18:00
List builds between two ISO 8601 formatted dates in UTC:
./worklog.py builds 2024-02-21T00:00:00Z 2024-02-28T00:00:00Z
Create a sorted list of package updates since the last sync in Markdown:
./worklog.py updates 2024-03-14T02:08:07Z --sort --format=md
List commits to the packages repository in the last 24 hours:
./worklog.py commits '1 days ago'
'''
))
parser.add_argument('command', type=str,
choices=['builds', 'updates', 'security-updates', 'commits', 'highlights'],
help='Type of output to show. '
'`builds` shows the builds as produced by the build server, '
'`updates` shows per-package updates based on the build server log and GitHub metadata, '
'`security-updates` shows updates with security fixes, '
'`commits` shows the commits from your local copy of the `packages` repository, '
'`highlights` shows the flagged PR summaries from your local `packages` repository.')
parser.add_argument('after', type=str,
help='Show builds after this date. '
'The date can be specified in any format parsed by the `date` command.')
parser.add_argument('before', type=str, nargs='?', default=datetime.now(timezone.utc).isoformat(),
help='Show builds before this date. '
'The date can be specified in any format parsed by the `date` command. '
'Defaults to `now`.')
parser.add_argument('--format', '-f', type=str, choices=['md', 'tty', 'html'], default='tty',
help='Output format: Terminal (`tty`), Markdown (`md`) or HTML (`html`). '
'Defaults to `tty`.')
parser.add_argument('--sort', '-s', action='store_true',
help='Sort packages in lexically ascending order.')
parser.add_argument('--follow', '-F', action='store_true',
help='Wait for and output new entries when they are created.')
cli_args = parser.parse_args()
printer = Printer(cli_args.after, cli_args.before)
if cli_args.follow:
printer.follow(cli_args.command, cli_args.format)
else:
printer.print(cli_args.command, cli_args.format, cli_args.sort)