Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 57 additions & 10 deletions github_activity/cli.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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():
Expand All @@ -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:
Expand All @@ -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,
Expand All @@ -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:
Expand Down
48 changes: 46 additions & 2 deletions github_activity/git.py
Original file line number Diff line number Diff line change
@@ -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)]
69 changes: 32 additions & 37 deletions github_activity/github_activity.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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": {
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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 = ""
Expand Down Expand Up @@ -276,6 +250,8 @@ def filter(datum):
include_opened=include_opened,
strip_brackets=strip_brackets,
branch=branch,
tags=tags,
prefixes=prefixes,
)

if not md:
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand All @@ -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

Expand Down