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:

This MR closes:

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 wraps git shortlog to stream unique contributor triplets (name, email, commit count) with sorting by commits/name/email and grouping by author or committer
    • list_contributors.go
  • Rails client + repository helper to call the RPC via Repository#all_contributors, exposed through a Projects::Contributor persistence layer and a background refresh worker so we never hit the repository on page load
    • gitlab/app/models/repository.rb
    • gitlab/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.haml
    • gitlab/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: ListContributors added to proto/commit.proto, generated stubs, and command metadata marks shortlog as non-ref-mutating (internal/git/gitcmd/command_description.go).
  • Implementation uses git shortlog -s -e with:
    • --all when 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 Go sort.Slice.
    • Contributor grouping: --group=author (default) or --group=committer per request enum.
    • Output parsed via regex and streamed using chunk.New to avoid huge single messages.
  • Validation: repository is validated, each revision checked via git.ValidateRevision (allows pseudo refs but blocks --/NUL), errors wrapped with structerr.
  • Tests (internal/gitaly/service/commit/list_contributors_test.go) cover sorting modes, revision filtering, empty repo behavior, and invalid input.

Rails integration

  • Repository#all_contributors is a thin wrapper around the client, deciding whether to pass --all or 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 via Gitlab::ErrorTracking.
  • User popover bootstrap: sidebar links embed commit_count_text into data attributes so user_popovers.js can seed the Vue popover with the commit-count row without extra API calls.
  • Branch push integration: BranchHooksService#enqueue_contributors_cache_refresh enqueues 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 in db/migrate/20251129180216_create_project_contributors.rb and 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.
  • Model Projects::Contributor:
    • Scopes for project/branch and ordering by commits/email.
    • refresh_from_repository fetches via all_contributors (authors only, commit-order), and update_for_project_branch lowercases/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_executed with if_deduplicated: :reschedule_once (1m TTL) to avoid stampeding on busy projects.
    • Marked idempotent!, low urgency, and defers on DB health signals for project_contributors.
    • Queue registered in app/workers/all_queues.yml.

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
email 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 via Gitlab::ErrorTracking.
  • Avatars:
    • If an email matches a GitLab user (find_user_by_email), we link to the profile and seed the user popover dataset with commit_count_text (pluralized).
    • Otherwise we render email avatars with tooltips; the last visible avatar shows a “+N” pill for the remainder (abbreviated to 1.5k style once ≥1000).
  • “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 commitCountText and 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 matches git shortlog -s -e semantics (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

Merge request reports

Loading