File size: 4,563 Bytes
8a37e0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import re
from argparse import ArgumentParser, RawTextHelpFormatter
from typing import Any

import requests
from attr import dataclass
from tqdm import tqdm


def get_author(commit: dict[str, Any]) -> str:
    """Gets the author of a commit.

    If the author is not present, the committer is used instead and an asterisk appended to the name."""
    return commit["author"]["login"] if commit["author"] else f"{commit['commit']['author']['name']}*"


@dataclass
class CommitInfo:
    sha: str
    url: str
    author: str
    is_username: bool
    message: str
    data: dict[str, Any]

    def __str__(self) -> str:
        return f"{self.sha}: {self.author}{'*' if not self.is_username else ''} - {self.message} ({self.url})"

    @classmethod
    def from_data(cls, commit: dict[str, Any]) -> "CommitInfo":
        return CommitInfo(
            sha=commit["sha"],
            url=commit["url"],
            author=commit["author"]["login"] if commit["author"] else commit["commit"]["author"]["name"],
            is_username=bool(commit["author"]),
            message=commit["commit"]["message"].split("\n")[0],
            data=commit,
        )


def fetch_commits_between_tags(
    org_name: str, repo_name: str, from_ref: str, to_ref: str, token: str
) -> list[CommitInfo]:
    """Fetches all commits between two tags in a GitHub repository."""

    commit_info: list[CommitInfo] = []
    headers = {"Authorization": f"token {token}"} if token else None

    # Get the total number of pages w/ an intial request - a bit hacky but it works...
    response = requests.get(
        f"https://api.github.com/repos/{org_name}/{repo_name}/compare/{from_ref}...{to_ref}?page=1&per_page=100",
        headers=headers,
    )
    last_page_match = re.search(r'page=(\d+)&per_page=\d+>; rel="last"', response.headers["Link"])
    last_page = int(last_page_match.group(1)) if last_page_match else 1

    pbar = tqdm(range(1, last_page + 1), desc="Fetching commits", unit="page", leave=False)

    for page in pbar:
        compare_url = f"https://api.github.com/repos/{org_name}/{repo_name}/compare/{from_ref}...{to_ref}?page={page}&per_page=100"
        response = requests.get(compare_url, headers=headers)
        commits = response.json()["commits"]
        commit_info.extend([CommitInfo.from_data(c) for c in commits])

    return commit_info


def main():
    description = """Fetch external contributions between two tags in the InvokeAI GitHub repository. Useful for generating a list of contributors to include in release notes.

When the GitHub username for a commit is not available, the committer name is used instead and an asterisk appended to the name.

Example output (note the second commit has an asterisk appended to the name):
171f2aa20ddfefa23c5edbeb2849c4bd601fe104: rohinish404 - fix(ui): image not getting selected (https://api.github.com/repos/invoke-ai/InvokeAI/commits/171f2aa20ddfefa23c5edbeb2849c4bd601fe104)
0bb0e226dcec8a17e843444ad27c29b4821dad7c: Mark E. Shoulson* - Flip default ordering of workflow library; #5477 (https://api.github.com/repos/invoke-ai/InvokeAI/commits/0bb0e226dcec8a17e843444ad27c29b4821dad7c)
"""

    parser = ArgumentParser(description=description, formatter_class=RawTextHelpFormatter)
    parser.add_argument("--token", dest="token", type=str, default=None, help="The GitHub token to use")
    parser.add_argument("--from", dest="from_ref", type=str, help="The start reference (commit, tag, etc)")
    parser.add_argument("--to", dest="to_ref", type=str, help="The end reference (commit, tag, etc)")

    args = parser.parse_args()

    org_name = "invoke-ai"
    repo_name = "InvokeAI"

    # List of members of the organization, including usernames and known display names,
    # any of which may be used in the commit data. Used to filter out commits.
    org_members = [
        "blessedcoolant",
        "brandonrising",
        "chainchompa",
        "ebr",
        "Eugene Brodsky",
        "hipsterusername",
        "Kent Keirsey",
        "lstein",
        "Lincoln Stein",
        "maryhipp",
        "Mary Hipp Rogers",
        "Mary Hipp",
        "psychedelicious",
        "RyanJDick",
        "Ryan Dick",
    ]

    all_commits = fetch_commits_between_tags(
        org_name=org_name,
        repo_name=repo_name,
        from_ref=args.from_ref,
        to_ref=args.to_ref,
        token=args.token,
    )
    filtered_commits = filter(lambda x: x.author not in org_members, all_commits)

    for commit in filtered_commits:
        print(commit)


if __name__ == "__main__":
    main()