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
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,9 @@ jobs:
| ------------------ | ------------------- | ------------------ |
| 1 | 143 | 0% |

| Username | All Time Contribution Count | New Contributor | Commits between 2021-01-01 and 2023-10-10 |
| --------- | --------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| @zkoppert | 143 | False | [super-linter/super-linter](https://github.com/super-linter/super-linter/commits?author=zkoppert&since=2021-01-01&until=2023-10-10) |
| Username | Company | All Time Contribution Count | New Contributor | Commits between 2021-01-01 and 2023-10-10 |
| --------- | ------- | --------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| @zkoppert | @github | 143 | False | [super-linter/super-linter](https://github.com/super-linter/super-linter/commits?author=zkoppert&since=2021-01-01&until=2023-10-10) |
```

## Example Markdown output with no dates supplied
Expand All @@ -252,9 +252,9 @@ jobs:
| ------------------ | ------------------- | ------------------ |
| 1 | 1913 | 0% |

| Username | All Time Contribution Count | New Contributor | Sponsor URL | Commits between 2021-09-01 and 2023-09-30 |
| --------- | --------------------------- | --------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| @zkoppert | 1913 | False | [Sponsor Link](https://github.com/sponsors/zkoppert) | [super-linter/super-linter](https://github.com/super-linter/super-linter/commits?author=zkoppert&since=2021-09-01&until=2023-09-30) |
| Username | Company | All Time Contribution Count | New Contributor | Sponsor URL | Commits between 2021-09-01 and 2023-09-30 |
| --------- | ------- | --------------------------- | --------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| @zkoppert | @github | 1913 | False | [Sponsor Link](https://github.com/sponsors/zkoppert) | [super-linter/super-linter](https://github.com/super-linter/super-linter/commits?author=zkoppert&since=2021-09-01&until=2023-09-30) |
```

## GitHub Actions Job Summary
Expand Down
8 changes: 8 additions & 0 deletions contributor_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# [
# {
# "username" : "zkoppert",
# "company" : "@github",
# "new_contributor" : "False",
# "avatar_url" : "https://avatars.githubusercontent.com/u/29484535?v=4",
# "contribution_count" : 1261,
Expand All @@ -23,6 +24,7 @@ class ContributorStats:

Attributes:
username (str): The username of the contributor
company (str): The company listed on the contributor's GitHub profile
new_contributor (bool): Whether the contributor is new or returning
avatar_url (str): The url of the contributor's avatar
contribution_count (int): The number of contributions the contributor has made
Expand All @@ -38,6 +40,7 @@ def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument
def __init__(
self,
username: str,
company: str,
new_contributor: bool,
avatar_url: str,
contribution_count: int,
Expand All @@ -47,6 +50,7 @@ def __init__(
"""Initialize the contributor_stats object"""
new_contributor = False
self.username = username
self.company = company
self.new_contributor = new_contributor
self.avatar_url = avatar_url
Comment on lines 50 to 55
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContributorStats.__init__ unconditionally overwrites the new_contributor argument (new_contributor = False), so callers can’t initialize this field even though it’s part of the constructor signature. Either store the passed-in value on self.new_contributor, or remove the parameter and set the field internally to avoid a misleading API.

Copilot uses AI. Check for mistakes.
self.contribution_count = contribution_count
Expand All @@ -57,6 +61,7 @@ def __repr__(self) -> str:
"""Return the representation of the contributor_stats object"""
return (
f"contributor_stats(username={self.username}, "
f"company={self.company}, "
f"new_contributor={self.new_contributor}, "
f"avatar_url={self.avatar_url}, "
f"contribution_count={self.contribution_count}, "
Expand All @@ -68,6 +73,7 @@ def __eq__(self, other) -> bool:
"""Check if two contributor_stats objects are equal"""
return (
self.username == other.username
and self.company == other.company
and self.new_contributor == other.new_contributor
and self.avatar_url == other.avatar_url
and self.contribution_count == other.contribution_count
Expand Down Expand Up @@ -122,6 +128,8 @@ def merge_contributors(contributors: list) -> list:
merged_contributor.new_contributor
or contributor.new_contributor
)
if not merged_contributor.company and contributor.company:
merged_contributor.company = contributor.company

else:
merged_contributors.append(contributor)
Expand Down
2 changes: 2 additions & 0 deletions contributors.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,10 @@ def get_contributors(repo: object, start_date: str, end_date: str, ghe: str):
commit_url = f"{endpoint}/{repo.full_name}/commits?author={user.login}&since={start_date}&until={end_date}"
else:
commit_url = f"{endpoint}/{repo.full_name}/commits?author={user.login}"
company = getattr(user, "company", "") or ""
contributor = contributor_stats.ContributorStats(
user.login,
company,
False,
Comment on lines 181 to 188
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GitHub “list contributors” API response doesn’t include the user company field; with the current getattr(user, "company", "") approach, company will likely always be empty unless an extra user lookup is performed. To actually implement this feature, fetch the full user profile (e.g., GET /users/{login}) and consider caching results to avoid N+1 requests / rate-limit issues.

Copilot uses AI. Check for mistakes.
user.avatar_url,
user.contributions_count,
Expand Down
28 changes: 14 additions & 14 deletions markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@ def write_to_markdown(
"""
This function writes a list of collaborators to a markdown file in table format
and optionally to GitHub Actions Job Summary if running in a GitHub Actions environment.
Each collaborator is represented as a dictionary with keys 'username',
'contribution_count', 'new_contributor', and 'commits'.
Each collaborator is represented as a ContributorStats object with fields
'username', 'company', 'contribution_count', 'new_contributor', and 'commit_url'.

Args:
collaborators (list): A list of dictionaries, where each dictionary
represents a collaborator. Each dictionary should
have the keys 'username', 'contribution_count',
and 'commits'.
collaborators (list): A list of ContributorStats objects. Each object should
have the fields 'username', 'company', 'contribution_count',
'new_contributor', and 'commit_url'.
filename (str): The path of the markdown file to which the table will
be written.
start_date (str): The start date of the date range for the contributor
Expand Down Expand Up @@ -166,8 +165,9 @@ def get_summary_table(collaborators, start_date, end_date, total_contributions):
This function returns a string containing a markdown table of the summary statistics.

Args:
collaborators (list): A list of dictionaries, where each dictionary represents a collaborator.
Each dictionary should have the keys 'username', 'contribution_count', and 'commits'.
collaborators (list): A list of ContributorStats objects.
Each object should have the fields 'username', 'company',
'contribution_count', and 'new_contributor'.
start_date (str): The start date of the date range for the contributor list.
end_date (str): The end date of the date range for the contributor list.
total_contributions (int): The total number of contributions made by all of the contributors.
Expand Down Expand Up @@ -212,8 +212,9 @@ def get_contributor_table(
This function returns a string containing a markdown table of the contributors and the total contribution count.

Args:
collaborators (list): A list of dictionaries, where each dictionary represents a collaborator.
Each dictionary should have the keys 'username', 'contribution_count', and 'commits'.
collaborators (list): A list of ContributorStats objects.
Each object should have the fields 'username', 'company',
'contribution_count', 'commit_url', and 'new_contributor'.
start_date (str): The start date of the date range for the contributor list.
end_date (str): The end date of the date range for the contributor list.
organization (str): The organization for which the contributors are being listed.
Expand All @@ -230,7 +231,7 @@ def get_contributor_table(
sponsor_info = _is_truthy(sponsor_info)
show_avatar = _is_truthy(show_avatar)
link_to_profile = _is_truthy(link_to_profile)
columns = ["Username", "All Time Contribution Count"]
columns = ["Username", "Company", "All Time Contribution Count"]
if show_avatar:
columns.insert(0, "Avatar")
if start_date and end_date:
Expand All @@ -252,6 +253,7 @@ def get_contributor_table(
total_contributions += collaborator.contribution_count
username = collaborator.username
contribution_count = collaborator.contribution_count
company = collaborator.company or "-"
if repository:
commit_urls = collaborator.commit_url
if organization:
Expand All @@ -275,9 +277,7 @@ def get_contributor_table(
else ""
)
row += f"{avatar_cell} | "
row += (
f"{'' if not link_to_profile else '@'}{username} | {contribution_count} |"
)
row += f"{'' if not link_to_profile else '@'}{username} | {company} | {contribution_count} |"
if "New Contributor" in columns:
row += f" {new_contributor} |"
if "Sponsor URL" in columns:
Expand Down
14 changes: 14 additions & 0 deletions test_contributor_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def setUp(self):
"""
self.contributor = ContributorStats(
"zkoppert",
"@github",
False,
"https://avatars.githubusercontent.com/u/29484535?v=4",
1261,
Expand All @@ -34,6 +35,7 @@ def test_init(self):
Test the __init__ method of the ContributorStats class.
"""
self.assertEqual(self.contributor.username, "zkoppert")
self.assertEqual(self.contributor.company, "@github")
self.assertFalse(self.contributor.new_contributor)
self.assertEqual(
self.contributor.avatar_url,
Expand All @@ -49,6 +51,7 @@ def test_repr(self):
"""Test the __repr__ method includes key fields."""
expected = (
"contributor_stats(username=zkoppert, "
"company=@github, "
"new_contributor=False, "
"avatar_url=https://avatars.githubusercontent.com/u/29484535?v=4, "
"contribution_count=1261, "
Expand All @@ -63,6 +66,7 @@ def test_merge_contributors(self):
"""
contributor1 = ContributorStats(
"user1",
"@company1",
False,
"https://avatars.githubusercontent.com/u/29484535?v=4",
100,
Expand All @@ -71,6 +75,7 @@ def test_merge_contributors(self):
)
contributor2 = ContributorStats(
"user2",
"@company2",
False,
"https://avatars.githubusercontent.com/u/29484535?v=4",
200,
Expand All @@ -79,6 +84,7 @@ def test_merge_contributors(self):
)
contributor3 = ContributorStats(
"user1",
"@company1",
False,
"https://avatars.githubusercontent.com/u/29484535?v=4",
150,
Expand All @@ -95,6 +101,7 @@ def test_merge_contributors(self):
expected_result = [
ContributorStats(
"user1",
"@company1",
False,
"https://avatars.githubusercontent.com/u/29484535?v=4",
250,
Expand All @@ -103,6 +110,7 @@ def test_merge_contributors(self):
),
ContributorStats(
"user2",
"@company2",
False,
"https://avatars.githubusercontent.com/u/29484535?v=4",
200,
Expand All @@ -123,6 +131,7 @@ def test_is_new_contributor_true(self):
returning_contributors = [
ContributorStats(
username="user1",
company="",
new_contributor=False,
avatar_url="https://avatars.githubusercontent.com/u/",
contribution_count=100,
Expand All @@ -131,6 +140,7 @@ def test_is_new_contributor_true(self):
),
ContributorStats(
username="user2",
company="",
new_contributor=False,
avatar_url="https://avatars.githubusercontent.com/u/",
contribution_count=200,
Expand All @@ -151,6 +161,7 @@ def test_is_new_contributor_false(self):
returning_contributors = [
ContributorStats(
username="user1",
company="",
new_contributor=False,
avatar_url="https://avatars.githubusercontent.com/u/",
contribution_count=100,
Expand All @@ -159,6 +170,7 @@ def test_is_new_contributor_false(self):
),
ContributorStats(
username="user2",
company="",
new_contributor=False,
avatar_url="https://avatars.githubusercontent.com/u/",
contribution_count=200,
Expand Down Expand Up @@ -189,6 +201,7 @@ def test_fetch_sponsor_info(self, mock_post):
returning_contributors = [
ContributorStats(
username=user,
company="",
new_contributor=False,
avatar_url="https://avatars.githubusercontent.com/u/",
contribution_count=100,
Expand Down Expand Up @@ -237,6 +250,7 @@ def test_fetch_sponsor_info_raises_on_error(self, mock_post):
contributors = [
ContributorStats(
username="user1",
company="",
new_contributor=False,
avatar_url="https://avatars.githubusercontent.com/u/",
contribution_count=100,
Expand Down
15 changes: 15 additions & 0 deletions test_contributors.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def test_get_contributors(self, mock_contributor_stats):
mock_user.login = "user"
mock_user.avatar_url = "https://avatars.githubusercontent.com/u/12345678?v=4"
mock_user.contributions_count = 100
mock_user.company = "@company"
mock_repo.contributors.return_value = [mock_user]
mock_repo.full_name = "owner/repo"
mock_repo.commits.return_value = iter([object()])
Expand All @@ -34,6 +35,7 @@ def test_get_contributors(self, mock_contributor_stats):
)
mock_contributor_stats.assert_called_once_with(
"user",
"@company",
False,
"https://avatars.githubusercontent.com/u/12345678?v=4",
100,
Expand All @@ -54,6 +56,7 @@ def test_get_all_contributors_with_organization(self, mock_get_contributors):
mock_get_contributors.return_value = [
ContributorStats(
"user",
"@company",
False,
"https://avatars.githubusercontent.com/u/29484535?v=4",
100,
Expand All @@ -72,6 +75,7 @@ def test_get_all_contributors_with_organization(self, mock_get_contributors):
[
ContributorStats(
"user",
"@company",
False,
"https://avatars.githubusercontent.com/u/29484535?v=4",
200,
Expand All @@ -93,6 +97,7 @@ def test_get_all_contributors_with_repository(self, mock_get_contributors):
mock_get_contributors.return_value = [
ContributorStats(
"user",
"@company",
False,
"https://avatars.githubusercontent.com/u/29484535?v=4",
100,
Expand All @@ -111,6 +116,7 @@ def test_get_all_contributors_with_repository(self, mock_get_contributors):
[
ContributorStats(
"user",
"@company",
False,
"https://avatars.githubusercontent.com/u/29484535?v=4",
100,
Expand All @@ -133,10 +139,12 @@ def test_get_contributors_skip_users_with_no_commits(self, mock_contributor_stat
mock_user.login = "user"
mock_user.avatar_url = "https://avatars.githubusercontent.com/u/12345678?v=4"
mock_user.contributions_count = 100
mock_user.company = "@company"
mock_user2 = MagicMock()
mock_user2.login = "user2"
mock_user2.avatar_url = "https://avatars.githubusercontent.com/u/12345679?v=4"
mock_user2.contributions_count = 102
mock_user2.company = "@company2"

mock_repo.contributors.return_value = [mock_user, mock_user2]
mock_repo.full_name = "owner/repo"
Expand All @@ -156,6 +164,7 @@ def test_get_contributors_skip_users_with_no_commits(self, mock_contributor_stat
)
mock_contributor_stats.assert_called_once_with(
"user",
"@company",
False,
"https://avatars.githubusercontent.com/u/12345678?v=4",
100,
Expand All @@ -173,6 +182,7 @@ def test_get_contributors_skip_bot(self, mock_contributor_stats):
mock_user.login = "[bot]"
mock_user.avatar_url = "https://avatars.githubusercontent.com/u/12345678?v=4"
mock_user.contributions_count = 100
mock_user.company = "@company"

mock_repo.contributors.return_value = [mock_user]
mock_repo.full_name = "owner/repo"
Expand All @@ -194,6 +204,7 @@ def test_get_contributors_no_commit_end_date(self, mock_contributor_stats):
mock_user.login = "user"
mock_user.avatar_url = "https://avatars.githubusercontent.com/u/12345678?v=4"
mock_user.contributions_count = 100
mock_user.company = "@company"

mock_repo.contributors.return_value = [mock_user]
mock_repo.full_name = "owner/repo"
Expand All @@ -204,6 +215,7 @@ def test_get_contributors_no_commit_end_date(self, mock_contributor_stats):
mock_repo.commits.assert_not_called()
mock_contributor_stats.assert_called_once_with(
"user",
"@company",
False,
"https://avatars.githubusercontent.com/u/12345678?v=4",
100,
Expand Down Expand Up @@ -311,6 +323,7 @@ def test_main_sets_new_contributor_flag(self):
"""Test main sets new_contributor when start/end dates are provided."""
contributor = ContributorStats(
"user1",
"",
False,
"https://avatars.githubusercontent.com/u/1",
10,
Expand Down Expand Up @@ -361,6 +374,7 @@ def test_main_fetches_sponsor_info_when_enabled(self):
"""Test main fetches sponsor information when sponsor_info is enabled."""
contributor = ContributorStats(
"user1",
"",
False,
"https://avatars.githubusercontent.com/u/1",
10,
Expand All @@ -369,6 +383,7 @@ def test_main_fetches_sponsor_info_when_enabled(self):
)
sponsored_contributor = ContributorStats(
"user1",
"",
False,
"https://avatars.githubusercontent.com/u/1",
10,
Expand Down
Loading
Loading