diff --git a/scripts/get_external_contributions.py b/scripts/get_external_contributions.py new file mode 100644 index 0000000000..75c481e148 --- /dev/null +++ b/scripts/get_external_contributions.py @@ -0,0 +1,104 @@ +from argparse import ArgumentParser, RawTextHelpFormatter +from typing import Any + +import requests +from attr import dataclass + + +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.""" + compare_url = f"https://api.github.com/repos/{org_name}/{repo_name}/compare/{from_ref}...{to_ref}" + headers = {"Authorization": f"token {token}"} if token else None + response = requests.get(compare_url, headers=headers) + commits = response.json()["commits"] + return [CommitInfo.from_data(c) for c in commits] + + +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()