diff --git a/github_activity/cli.py b/github_activity/cli.py index 473268d..4a89c09 100644 --- a/github_activity/cli.py +++ b/github_activity/cli.py @@ -1,13 +1,13 @@ import argparse +import copy import os import sys -from subprocess import PIPE -from subprocess import run -from .git import _git_installed_check +from .git import _git_get_remotes, _git_installed_check from .github_activity import _parse_target from .github_activity import generate_activity_md from .github_activity import generate_all_activity_md +from .github_activity import TAGS_METADATA_BASE DESCRIPTION = "Generate a markdown changelog of GitHub activity within a date window." parser = argparse.ArgumentParser(description=DESCRIPTION) @@ -117,6 +117,48 @@ action="store_true", help=("""Whether to include all the GitHub tags"""), ) +parser.add_argument( + "--new-prefix", + action="extend", + nargs="+", + help=("""Prefix(es) for PRs with new features""") +) +parser.add_argument( + "--enhancement-prefix", + action="extend", + nargs="+", + help=("""Prefix(es) for PRs with enhancements""") +) +parser.add_argument( + "--bug-prefix", + action="extend", + nargs="+", + help=("""Prefix(es) for PRs with bugfixes""") +) +parser.add_argument( + "--maintenance-prefix", + action="extend", + nargs="+", + help=("""Prefix(es) for PRs with maintenance and upkeep improvements""") +) +parser.add_argument( + "--documentation-prefix", + action="extend", + nargs="+", + help=("""Prefix(es) for PRs with documentation improvements""") +) +parser.add_argument( + "--api-change-prefix", + action="extend", + nargs="+", + help=("""Prefix(es) for PRs with API and breaking changes""") +) +parser.add_argument( + "--deprecate-prefix", + action="extend", + nargs="+", + help=("""Prefix(es) for PRs with API and breaking changes""") +) def main(): @@ -131,17 +173,12 @@ def main(): if unknown and not args.target: args.target = unknown[0] - tags = args.tags.split(",") if args.tags is not None else args.tags + tags = args.tags.split(",") if args.tags is not None else None # Automatically detect the target from remotes if we haven't had one passed. if not args.target: err = "Could not automatically detect remote, and none was given." try: - out = run("git remote -v".split(), stdout=PIPE) - remotes = out.stdout.decode().split("\n") - remotes = [ii for ii in remotes if ii] - remotes = { - ii.split("\t")[0]: ii.split("\t")[1].split()[0] for ii in remotes - } + remotes = _git_get_remotes() if "upstream" in remotes: ref = remotes["upstream"] elif "origin" in remotes: @@ -159,6 +196,14 @@ def main(): except Exception: raise ValueError(err) + # Build mapping from tag to tag prefixes, if given + prefixes = {} + for tag in TAGS_METADATA_BASE: + tag_prefixes = getattr(args, f"{tag}_prefix") + if tag_prefixes is None: + continue + prefixes[tag] = tag_prefixes + common_kwargs = dict( kind=args.kind, auth=args.auth, @@ -167,6 +212,8 @@ def main(): include_opened=bool(args.include_opened), strip_brackets=bool(args.strip_brackets), branch=args.branch, + prefixes=prefixes + ) if args.all: diff --git a/github_activity/git.py b/github_activity/git.py index a38c5ed..82a4662 100644 --- a/github_activity/git.py +++ b/github_activity/git.py @@ -1,10 +1,54 @@ +import os.path +import re +import shlex import subprocess +from tempfile import TemporaryDirectory +from typing import Dict, List, Tuple - -def _git_installed_check(): +def _git_installed_check() -> bool: cmd = ["git", "--help"] try: subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return True except subprocess.CalledProcessError: return False + + +def _git_get_remotes() -> Dict[str, str]: + out = subprocess.run("git remote -v".split(), stdout=subprocess.PIPE) + remotes = out.stdout.decode().split("\n") + remotes = [ii for ii in remotes if ii] + return { + ii.split("\t")[0]: ii.split("\t")[1].split()[0] for ii in remotes + } + + +def _git_get_remote_sha_and_tags(repo: str, pattern: str) -> List[Tuple[str, str]]: + # Get the sha and tag name for each tag in the target repo + with TemporaryDirectory() as td: + + subprocess.run( + shlex.split(f"git clone {repo} repo"), cwd=td + ) + repo = os.path.join(td, "repo") + subprocess.run(shlex.split("git fetch origin --tags"), cwd=repo) + + cmd = 'git log --tags --simplify-by-decoration --pretty="format:%h | %D"' + data = ( + subprocess.check_output(shlex.split(cmd), cwd=repo) + .decode("utf-8") + .splitlines() + ) + + # Clean up the raw data + pattern = f"tag: {pattern}" + + def filter(datum): + _, tag = datum + # Handle the HEAD tag if it exists + if "," in tag: + tag = tag.split(", ")[1] + return re.match(pattern, tag) is not None + + data = [d.split(" | ") for (i, d) in enumerate(data)] + return [d for d in data if filter(d)] \ No newline at end of file diff --git a/github_activity/github_activity.py b/github_activity/github_activity.py index fbe6c5b..40f91a2 100644 --- a/github_activity/github_activity.py +++ b/github_activity/github_activity.py @@ -1,15 +1,12 @@ """Use the GraphQL api to grab issues/PRs that match a query.""" +import copy import datetime import os import re import shlex -import subprocess import sys -import urllib -from pathlib import Path from subprocess import PIPE from subprocess import run -from tempfile import TemporaryDirectory import dateutil import numpy as np @@ -18,9 +15,9 @@ import requests from .cache import _cache_data +from .git import _git_get_remote_sha_and_tags from .graphql import GitHubGraphQlQuery - # The tags and description to use in creating subsets of PRs TAGS_METADATA_BASE = { "new": { @@ -178,6 +175,7 @@ def generate_all_activity_md( include_opened=False, strip_brackets=False, branch=None, + prefixes=None, ): """Generate a full markdown changelog of GitHub activity of a repo based on release tags. @@ -212,40 +210,16 @@ def generate_all_activity_md( E.g., [MRG], [DOC], etc. branch : string | None The branch or reference name to filter pull requests by. + prefixes: dict | None + The prefixes for each tag, against which PR titles will be matched. Returns ------- entry: str The markdown changelog entry for all of the release tags in the repo. """ - # Get the sha and tag name for each tag in the target repo - with TemporaryDirectory() as td: - - subprocess.run( - shlex.split(f"git clone https://github.com/{target} repo"), cwd=td - ) - repo = os.path.join(td, "repo") - subprocess.run(shlex.split("git fetch origin --tags"), cwd=repo) - - cmd = 'git log --tags --simplify-by-decoration --pretty="format:%h | %D"' - data = ( - subprocess.check_output(shlex.split(cmd), cwd=repo) - .decode("utf-8") - .splitlines() - ) - - # Clean up the raw data - pattern = f"tag: {pattern}" - - def filter(datum): - _, tag = datum - # Handle the HEAD tag if it exists - if "," in tag: - tag = tag.split(", ")[1] - return re.match(pattern, tag) is not None - - data = [d.split(" | ") for (i, d) in enumerate(data)] - data = [d for d in data if filter(d)] + # Get the (SHA, tag) pairs for this target + data = _git_get_remote_sha_and_tags(f"https://github.com/{target}", pattern) # Generate a changelog entry for each version and sha range output = "" @@ -276,6 +250,8 @@ def filter(datum): include_opened=include_opened, strip_brackets=strip_brackets, branch=branch, + tags=tags, + prefixes=prefixes, ) if not md: @@ -303,6 +279,7 @@ def generate_activity_md( strip_brackets=False, heading_level=1, branch=None, + prefixes=None, ): """Generate a markdown changelog of GitHub activity within a date window. @@ -347,7 +324,8 @@ def generate_activity_md( With heading_level=2 those are increased to h2 and h3, respectively. branch : string | None The branch or reference name to filter pull requests by. - + prefixes: dict | None + The prefixes for each tag, against which PR titles will be matched. Returns ------- entry: str @@ -450,13 +428,30 @@ def generate_activity_md( # Define categories for a few labels if tags is None: tags = TAGS_METADATA_BASE.keys() - if not all(tag in TAGS_METADATA_BASE for tag in tags): + # We expect an (improper) subset of tags + if not set(tags) <= TAGS_METADATA_BASE.keys(): raise ValueError( "You provided an unsupported tag. Tags must be " f"one or more of {TAGS_METADATA_BASE.keys()}, You provided:\n" f"{tags}" ) - tags_metadata = {key: val for key, val in TAGS_METADATA_BASE.items() if key in tags} + # Define per-category prefixes + if prefixes is None: + prefixes = {} + + # Override default prefixes with user-specified ones + default_prefixes = {t: v['pre'] for t, v in TAGS_METADATA_BASE.items()} + prefixes = {**default_prefixes, **prefixes} + # Take the lowercase of the prefixes + prefixes = {t: [s.lower() for s in p] for t, p in prefixes.items()} + + # Build tag metadata + tags_metadata = {} + for tag in tags: + tags_metadata[tag] = { + **TAGS_METADATA_BASE[tag], + "pre": prefixes[tag] + } # Initialize our tags with empty metadata for key, vals in tags_metadata.items(): @@ -476,7 +471,7 @@ def generate_activity_md( ) # Now find PRs based on prefix mask_pre = closed_prs["title"].map( - lambda title: any(f"{ipre}:" in title for ipre in kindmeta["pre"]) + lambda title: any(f"{ipre}:" in title.lower() for ipre in kindmeta["pre"]) ) mask = mask | mask_pre