Contributor analysis for GitHub repositories, using PyGithub.
-
make install-dev
link the local
.venv(editable) for installation with shell completion -
make install
use pipx (editable) for installation with shell completion
-
make init
initialize the local environment. All commands work with
uv run.
The token only needs read access to the analysed repositories:
| Case | Needed |
|---|---|
| Public repositories | No scopes; a token just raises the rate limit |
| Private repositories, classic token | repo scope |
| Private repositories, fine-grained token | Select the repositories; read-only Pull requests (Metadata read-only is added automatically) |
GitHub Actions GITHUB_TOKEN |
permissions: pull-requests: read and contents: read |
gh-community contributors all # sorted by merged pull requests
gh-community contributors all --sort commits_total
gh-community contributors all owner/name other/repo --token ... # overrides the configOutput is a Markdown table with right-aligned numbers (the default columns of
gh-community result, below). Some details of the numbers: the number of
comments does not matter for issues_commented and prs_commented, only on how
many issues or pull requests there is one (comments and comments on the changes
for pull requests, never on one's own). days_last is the newest activity in
the repositories: any of the data read: a commit, pull request, issue, comment,
review, commit comment or an action on an issue or pull request such as closing,
merging or labelling.
--sort takes a variable (below), +variable for ascending and -variable for
descending order (--sort=-days_last, or --sort -days_last). Without a sign
the handle and the days_ variables are ascending (so the newest activity is
first) and the rest descending (the highest first); the default is prs_merged.
user_association is sorted by privilege. Ties are sorted by handle.
Progress and the API calls used are printed to stderr. Responses are cached in
$TMPDIR/gh-community-cache-<uid> using ETags: repeating an analysis only
downloads what changed, and requests answered with 304 Not Modified do not
count against the rate limit. The first analysis of a repository needs one
request per contributor (latest commit) and one per pull request (reviews).
Reviews and latest commits are not even requested again while the pull request's
updated_at or the contributor's number of commits is the same.
--no-cache skips the cache, --cache-dir moves it. Pages and the per-contributor and
per-pull-request requests are sent in parallel (--jobs, default 4).
Every contributors all stores its result as org-repo-yyyy-mm-dd-hh-mm-ss.csv
(several repositories are joined with _) in the directory of the config file,
for later analysis; --no-save skips it. The CSV holds the raw numbers with the
names of the variables below (date_first and date_last as ISO dates). What
can be calculated from the columns is not stored, but can be used in
expressions: prs_total, prs_closed, issues_closed, user_bot, days_first
and days_last. Files from before the columns were renamed or existed can be
read, with 0 for what they lack.
gh-community result --list # list the names of the stored results
gh-community result NAME # NAME completes with the shell completion
gh-community result NAME --columns user_handle+prs_merged+prs_open+days_first
gh-community result NAME --columns '+prs_open-commits_total' # change the default
gh-community result NAME --sort=-days_last # the oldest activity first
gh-community result NAME --csv --columns user_handle+prs_merged # CSV, not a table
gh-community result NAME 'days_last < 30' # only those with activity in 30 days
gh-community result NAME --remove 'days_last > 90' # the same, inverted: remove them
gh-community result NAME --remove 'prs_merged<10 and (days_last < 100 or issues_opened > 0)'
gh-community result NAME --remove '"[bot]" in user_handle'The result is shown as a Markdown table, followed by a line such as
Showing 3 of 10 contributors.; the file is not changed. With --csv (also
for contributors all) it is printed as CSV instead, for the same columns,
filters and sorting: the header is the names of the columns as in the table (merged PRs, last activity) and the values are
only numbers and strings, so the days are numbers (empty if unknown), user_bot
is 1 or 0 and there is no X/total; the line with the count goes to stderr. Names are grouped by
prefix, with the unit first (days_, months_), and have no aliases.
--columns takes the columns to show, split by + and -, never by a comma.
a+b shows exactly the columns a and b, in that order; a specification that
starts with a sign changes the default columns: +x-y adds x and removes y
(write --columns=-y if it starts with -). Columns complete, too. Columns and
variables have the same names. The pull request and
issue columns that are a part of the pull requests or issues opened show it as
X/total, for example prs_merged as 245/269; the variable is the number.
days_first and days_last show N days (today for 0), the variables the number.
Default columns:
| Column | Meaning |
|---|---|
user_handle |
text: the GitHub login |
commits_total |
commits in the contributors list |
prs_merged |
pull requests opened and merged, shown as X/prs_total (variable prs_merged: the number) |
issues_opened |
issues opened |
issues_commented |
issues of others commented on |
prs_commented |
pull requests of others commented on |
prs_reviewed |
pull requests of others reviewed |
days_last |
days since the last activity, shown as N days (variable days_last: N) |
Additional columns:
| Column | Meaning |
|---|---|
actions_closes |
issues and pull requests closed by them, other than by merging |
actions_labels |
labels set by them |
actions_merges |
pull requests merged by them |
comments_total |
all comments written |
days_first |
days since the first activity, shown as N days (variable days_first: N) |
days_to_merge |
median days until their pull requests were merged |
issues_closed |
issues opened and closed, shown as X/issues_opened (variable issues_closed: the number) |
issues_open |
issues opened and still open, shown as X/issues_opened (variable issues_open: the number) |
months_active |
months with any activity |
prs_approved |
pull requests of others approved |
prs_changes_requested |
pull requests of others with changes requested |
prs_closed |
pull requests opened and closed (merged + unmerged), shown as X/prs_total (variable prs_closed: the number) |
prs_open |
pull requests opened and still open, shown as X/prs_total (variable prs_open: the number) |
prs_total |
pull requests opened, in total |
prs_unmerged |
pull requests opened and closed without merging, shown as X/prs_total (variable prs_unmerged: the number) |
user_association |
text, always lowercase, ordered by privilege: owner > member > collaborator > contributor > first_timer > first_time_contributor > mannequin > none > empty; the most privileged seen |
user_bot |
True if the handle contains [bot] or is Copilot, else False |
An expression after the name shows only the contributors for which it is true;
--remove takes an expression too and removes those for which it is true (it
can be repeated). Both can be combined. The variables (also in
gh-community result --help):
| Variable | Meaning |
|---|---|
actions_closes |
issues and pull requests closed by them, other than by merging |
actions_labels |
labels set by them |
actions_merges |
pull requests merged by them |
comments_total |
all comments written |
commits_total |
commits in the contributors list |
days_first |
days since the first activity |
days_last |
days since the last activity |
days_to_merge |
median days until their pull requests were merged |
issues_closed |
issues opened and closed |
issues_commented |
issues of others commented on |
issues_open |
issues opened and still open |
issues_opened |
issues opened |
months_active |
months with any activity |
prs_approved |
pull requests of others approved |
prs_changes_requested |
pull requests of others with changes requested |
prs_closed |
pull requests opened and closed (merged + unmerged) |
prs_commented |
pull requests of others commented on |
prs_merged |
pull requests opened and merged |
prs_open |
pull requests opened and still open |
prs_reviewed |
pull requests of others reviewed |
prs_total |
pull requests opened, in total |
prs_unmerged |
pull requests opened and closed without merging |
user_association |
text, always lowercase, ordered by privilege: owner > member > collaborator > contributor > first_timer > first_time_contributor > mannequin > none > empty; the most privileged seen |
user_bot |
True if the handle contains [bot] or is Copilot, else False |
user_handle |
text: the GitHub login |
The numbers are whole numbers, days_first, days_last and days_to_merge are
infinite if there is no data, user_bot is True or False. Comparisons of numbers
are >=, <=, ==, !=, > and < (also chained). Text is compared in any
case with == and !=, and in and not in tell whether the text on the left
is part of the one on the right: "[bot]" in user_handle, or
user_association in "owner member" (an empty association is part of any
text). user_association is ordered by privilege: owner > member >
collaborator > contributor > first_timer > first_time_contributor >
mannequin > none > unknown, so user_association >= association_member and
user_association < "contributor" work. The names are written as text or with
the prefix association_ (association_owner, association_member, ...,
association_none). An unknown
name is reported with the closest ones ("Did you mean ...?") and all the names,
sorted. Combine with and, or, not and parentheses; not binds tightest,
then and, then or. There are no functions.
gh-community response # the repositories of the config
gh-community response owner/name --no-cache
gh-community response --since 2026-01-01 # only items created on or afterTwo bar charts, one for the pull requests and one for the issues, show how long
they waited for their first response: one row for each time up to 1h 3h 6h 12h 24h 2d 3d 5d 1w 2w 3w (and > 3w). For each of three classes, the first
response of that class counts, so a bot that answers first does not hide the
others and one pull request or issue can be counted in several classes. A row
shows the number of each class, right aligned, and the stacked bars in their
colours:
- a bot (
[bot]in the handle, Copilot, or GitHub's user type Bot), - an external person: not the author, and no owner, member or collaborator (association contributor, first timer, none, ...),
- a collaborator or higher (owner, member or collaborator), not the author.
A response is a comment, a comment on the changes or a submitted review by someone other than the author (handles are compared in any case). Below the chart is a table with three rows for each class that has responses (n responses), aligned to compare: the average, its standard error, the median and the 95th percentile, for
- measured: the average and median of the times and the standard error of the average (the standard deviation s / √n), no 95th percentile,
- normal: the average and standard deviation s of the times; the standard error is s / √number, the median is the average, the 95th percentile is the average + 1.645 s (no standard error for a single response).
- exponential: the average is the same (the maximum likelihood rate is 1 / average), the standard error is average / √number, the median is the average × ln 2 and the 95th percentile the average × ln 20.
Pull requests or issues nobody responded to are counted in the title. A - is
a value that cannot be calculated, such as a standard error for one response.
Bars longer than 40 characters are scaled down.
The items are read with the GraphQL API: 50 pull requests or issues with their
first comments and reviews in one request, newest changes first (a repository with
2000 pull requests needs about 40 requests, not thousands). The token needs the
same read access. With the cache (--cache-dir, --no-cache) what was read is
remembered per repository with the time of the newest change, so the next run
only reads the items changed since, usually one request for pull requests and one
for issues. With --since, reading stops at that date (an item created since was changed
since, too); a later run with an earlier date reads again, with the same or a
later date it uses what was read. --token and --config work as for
contributors all. The GraphQL API
has its own rate limit, shown in the progress. This cache is not the one of the ETags:
a run that is stopped before it ends has stored nothing.
gh-community config create # asks for token and repositories
gh-community config token set|show|delete # show masks the token; --reveal prints it
gh-community config repo add|list|delete owner/name # delete and contributors also accept a short nameToken precedence: --token (accepted by every command that uses the API), $GITHUB_TOKEN (as
provided in GitHub Actions), then token: in the config.
The config is read from --config, ./config.yaml or
$XDG_CONFIG_HOME/gh-community/config.yaml (default ~/.config/...), which is also where config create writes.
GNU Affero General Public License v3.0 or later. See LICENSE.