Draft: feat: add total project contributors panel
What does MR do and why?
The feature adds a contributors panel to the project homepage sidebar, displaying:
- Top contributors ranked by commit count
- User avatars with hover popovers showing commit statistics
- Total contributor count with "View all" link to full analytics
High-Level Overview
This MR is one of the two paired MRs:
- Rails MR: Draft: feat: add total project contributors panel (!214624)
- Gitaly MR: Draft: feat: add ListContributors RPC for contr... (gitaly!8310)
This MR closes:
- closes contributor count project side panel feature (#582483)
- closes Show total contributors numbers on project home... (#259793)
At a high level, the feature works by creating a new Gitaly RPC ListContributors that wraps git shortlog to stream unique contributor triplets (name, email, commit count). Rails uses this RPC to fetch the project's contributors and displays them in the sidebar. The data is fetched in a background sidekiq job and persisted to a new database table project_contributors to avoid blocking the page load.
Three components power the feature:
- New Gitaly RPC
ListContributors(commit service) that wrapsgit shortlogto stream unique contributor triplets (name, email, commit count) with sorting by commits/name/email and grouping by author or committerlist_contributors.go
- Rails client + repository helper to call the RPC via
Repository#all_contributors, exposed through aProjects::Contributorpersistence layer and a background refresh worker so we never hit the repository on page loadgitlab/app/models/repository.rbgitlab/app/models/projects/contributor.rb-
gitlab/app/workers/projects/contributors_cache_worker.rb).
- A contributors sidebar panel that reads the cached data and augments user popovers with a commit-count row
gitlab/app/views/projects/_contributors_panel.html.hamlgitlab/app/assets/javascripts/user_popovers.js-
gitlab/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue).
Data Flow Diagram
flowchart LR
subgraph Push ["On Git Push"]
A[git push] --> B[BranchHooksService]
B --> C[ContributorsCacheWorker]
end
subgraph Refresh ["Background Refresh"]
C --> D[Projects::Contributor]
D --> E[Repository]
E --> F[Gitaly RPC]
F --> G[git shortlog]
G --> F
F --> D
D --> H[(project_contributors)]
end
subgraph Render ["On Page View"]
I[User visits project] --> J[contributors_panel.haml]
J --> K[ProjectsHelper]
K --> H
H --> J
J --> L[User Popover]
end
Gitaly side
- RPC definition:
ListContributorsadded toproto/commit.proto, generated stubs, and command metadata marksshortlogas non-ref-mutating (internal/git/gitcmd/command_description.go). - Implementation uses
git shortlog -s -ewith:-
--allwhen no revisions are specified, or specific revisions/branches from the request. - Sorting: default/COMMITS uses
-n, NAME uses shortlog’s default, EMAIL is post-processed with Gosort.Slice. - Contributor grouping:
--group=author(default) or--group=committerper request enum. - Output parsed via regex and streamed using
chunk.Newto avoid huge single messages.
-
- Validation: repository is validated, each revision checked via
git.ValidateRevision(allows pseudo refs but blocks--/NUL), errors wrapped withstructerr. - Tests (
internal/gitaly/service/commit/list_contributors_test.go) cover sorting modes, revision filtering, empty repo behavior, and invalid input.
Rails integration
-
Repository#all_contributorsis a thin wrapper around the client, deciding whether to pass--allor a single ref; it short-circuits on empty repos and is used only by background refreshers (no blocking page loads). - Project helpers (
project_contributors,project_contributors_count) read only from the cached table, pick the branch key (branch.presence || default_branch || __all__), and rescue/report errors viaGitlab::ErrorTracking. - User popover bootstrap: sidebar links embed
commit_count_textinto data attributes souser_popovers.jscan seed the Vue popover with the commit-count row without extra API calls. - Branch push integration:
BranchHooksService#enqueue_contributors_cache_refreshenqueues a refresh on every branch create/update so sidebar data trails the repo by the background job latency instead of page load latency.
Persistence and refresh
- New table
project_contributors(migrations indb/migrate/20251129180216_create_project_contributors.rband FK in...17_add_project_contributors_project_fk.rb) with:- Columns:
project_id,branch(includes special__all__aggregate),email,commits, timestamps. - Indexes:
(project_id, branch, commits DESC)for “top N” queries and unique(project_id, branch, email). - DB docs entry
db/docs/project_contributors.yml.
- Columns:
- Model
Projects::Contributor:- Scopes for project/branch and ordering by commits/email.
-
refresh_from_repositoryfetches viaall_contributors(authors only, commit-order), andupdate_for_project_branchlowercases/deduplicates by email, sums commits, deletes missing rows, and upserts in batches of 1000.
- Background worker
Projects::ContributorsCacheWorker:- Enqueued on every branch update/create via
BranchHooksService#enqueue_contributors_cache_refresh. - Uses
deduplicate :until_executedwithif_deduplicated: :reschedule_once(1m TTL) to avoid stampeding on busy projects. - Marked
idempotent!, low urgency, and defers on DB health signals forproject_contributors. - Queue registered in
app/workers/all_queues.yml.
- Enqueued on every branch update/create via
project_contributors table at a glance
| Column | Type | Null? | Notes / indexes |
|---|---|---|---|
| id | bigint | no | PK |
| project_id | bigint | no | FK → projects (cascade delete) |
| branch | text(1024) | no | Stores real branch or __all__ aggregate key |
| text(1024) | no | Lowercased; uniqueness scoped to project + branch | |
| commits | integer | no | Non-negative commit count |
| created_at | timestamptz | no | |
| updated_at | timestamptz | no | |
| index (project_id, branch, commits desc) | — | — | Top-N by branch, descending commits |
| unique index (project_id, branch, email) | — | — | Ensures 1 row per email per branch |
UI/UX path
- Sidebar panel is injected into
app/views/projects/_sidebar.html.haml; guards: user must have:read_code, repository must exist and not be empty, and cached contributors must be present. - Helpers (
project_contributors,project_contributors_count) pull cached rows for the project’s default branch (or__all__fallback) and swallow/report errors viaGitlab::ErrorTracking. - Avatars:
- If an email matches a GitLab user (
find_user_by_email), we link to the profile and seed the user popover dataset withcommit_count_text(pluralized). - Otherwise we render email avatars with tooltips; the last visible avatar shows a “+N” pill for the remainder (abbreviated to
1.5kstyle once ≥1000).
- If an email matches a GitLab user (
- “View all” links to the existing contributors graph page (
project_graph_path). - Translations added for “Contributors”, “View all”, and the “and %d more contributor(s)” tooltip strings (
locale/gitlab.pot). - User popover: the dataset now accepts
commitCountTextand the Vue component renders a commit icon + text block above email/bio/location when provided.
Notes
- The UI never shells out to Git; all requests are served from
project_contributors, which is refreshed asynchronously. A newly pushed contributor may be invisible until the worker finishes. - The RPC is read-only (
scNoRefUpdates) and uses the standard shortlog parser, so behavior matchesgit shortlog -s -esemantics (e.g., author identity as stored in commits). - Stats are keyed by lowercased email; name differences are intentionally collapsed. Branch-specific rows are stored under the provided branch name; an
__all__aggregate is used when no branch is provided.
Edited by Michael Angelo Rivera