Back to Sapling

Pushrebase

eden/mononoke/docs/4.1-pushrebase.md

latest13.2 KB
Original Source

Pushrebase

This document explains pushrebase, a server-side rebasing mechanism that maintains linear commit history in high-throughput repositories. Pushrebase is a key feature used by both Sapling and Git clients when pushing commits to Mononoke.

What is Pushrebase?

Pushrebase is a server-side operation that automatically rebases commits during a push when the target bookmark has moved forward since the client's base commit. Rather than requiring clients to pull, rebase locally, and retry their push, the server performs the rebase operation automatically for conflict-free changes.

When a client pushes commits to a bookmark that has moved forward, Mononoke detects whether the changes can be safely rebased. If the pushed commits modify different files than the commits that moved the bookmark forward, the server rebases the pushed commits on top of the current bookmark position and updates the bookmark atomically.

The result is a linear commit history on the target bookmark without requiring coordination between clients or manual rebase operations.

Why Pushrebase Exists

In repositories with high commit rates and many concurrent developers, bookmark contention becomes a problem. Without pushrebase, when multiple developers attempt to push to the same bookmark:

  1. The first push succeeds
  2. Subsequent pushes fail because their base commit is no longer the bookmark's position
  3. Each developer must pull the new commits, rebase their changes, and retry
  4. With many concurrent pushes, developers may need multiple retry cycles before succeeding

This retry loop increases push latency and client-side complexity. Pushrebase addresses this by moving the rebase operation to the server, which can perform it atomically during the bookmark update. Clients pushing non-conflicting changes succeed on the first attempt.

The Pushrebase Process

Client Side

From the client's perspective, pushrebase is transparent. A client pushes commits to a bookmark using standard Sapling push, or via the Git pushrebase tool provided by Mononoke. The client's base commit may be behind the current bookmark position on the server.

Server-Side Operation

When Mononoke receives a push to a bookmark configured for pushrebase:

1. Identify the Rebased Set

The server identifies which commits need to be rebased:

  • The pushed set contains all commits sent by the client
  • The root is the common ancestor between the pushed commits and the current bookmark position
  • The rebased set contains the pushed commits that will be rebased on top of the bookmark, which may be smaller than the pushed set when merges are involved

2. Conflict Detection

The server checks whether the rebase can be performed automatically:

  • Collect all files modified in the rebased set
  • Collect all files modified between the root and the current bookmark position
  • Check for path conflicts between these two sets

A conflict exists if:

  • The same file is modified in both sets
  • A path in one set is a prefix of a path in the other set (file/directory conflicts)
  • Any other manifest-level conflicts exist (delete/modify conflicts)

3. Rebase Execution

If no conflicts are detected, the server performs the rebase:

  • For each commit in the rebased set (starting from the oldest)
  • Create a new commit with the same changes but with updated parents
  • Update commit metadata (timestamps may be adjusted based on configuration)
  • Store the new Bonsai changeset in the blobstore

4. Hook Execution

Before updating the bookmark, pushrebase runs configured hooks:

  • Pre-commit hooks validate the rebased commits
  • Hooks can reject the push if validation fails
  • Hooks can modify commits (e.g., adding tracking information)
  • Transaction hooks can perform additional database updates in the same transaction

5. Bookmark Update

If hooks pass, the server updates the bookmark:

  • Move the bookmark to point to the tip of the rebased commits
  • Update VCS mappings (Bonsai ↔ Git/Mercurial hashes) for the rebased commits
  • Record the bookmark movement in the update log
  • Commit the transaction atomically

6. Response to Client

The server returns the rebased commits to the client:

  • Sapling clients receive obsmarkers mapping original commits to rebased commits
  • Git clients receive the new commit hashes
  • Clients can update their local state without performing a separate fetch

Retry on Concurrent Updates

If the bookmark moves during the pushrebase operation (due to another concurrent push), the server retries the rebase with the new bookmark position. After a configured number of retries (typically 100), the server fails the push and the client must retry.

Conflict-Free Rebases

Pushrebase only handles conflict-free rebases where no file-level merging is required. This is a conservative approach that ensures correctness - if file contents conflict, human intervention is required.

The conflict detection is performed at the path level, not the content level. Two commits that modify the same file are considered conflicting even if the changes are to different lines. This conservative approach avoids potential semantic conflicts and unexpected behavior.

If a push contains conflicts, the server rejects it with a conflict error. The client must:

  1. Pull the latest bookmark position
  2. Rebase locally (resolving any conflicts manually)
  3. Push again (which may succeed via pushrebase if the rebase was clean)

Pushrebase Configuration

Pushrebase behavior is controlled by repository configuration in metaconfig/:

Per-Repository Settings:

  • pushrebase.block_merges - Reject pushes containing merge commits
  • pushrebase.casefolding_check - Check for case-folding conflicts (important for case-insensitive filesystems)
  • pushrebase.rewritedates - Update commit timestamps during rebase
  • pushrebase.recursion_limit - Maximum commit stack depth
  • pushrebase.forbid_p2_root_rebases - Disallow rebasing certain stacks based on merge commits

Per-Bookmark Settings:

  • Bookmarks can be configured to allow or forbid pushrebase
  • Some bookmarks may require direct fast-forward pushes instead

Pushrebase Hooks

Pushrebase has a hook mechanism that allows additional optional operations to happen during pushrebase. This is separate from Mononoke's bookmark hook system (documented in 4.3-hooks.md) although bookmarks hooks are also run during pushrebase. Pushrebase hooks run during rebase computation and before the bookmark is updated, allowing:

  • Adding commit tracking information
  • Recording the original commit hash (pushrebase mutation mapping)
  • Generating sequential commit numbers (GlobalRev)

Pushrebase hooks are defined in features/pushrebase/pushrebase_hook/ (the trait) and features/pushrebase/pushrebase_hooks/ (hook implementations). The get_pushrebase_hooks() function in features/pushrebase/pushrebase_hooks/ constructs the set of hooks to run based on repository configuration.

Implementation Architecture

Pushrebase is implemented across several components:

Core Pushrebase (features/pushrebase/src/):

  • do_pushrebase_bonsai() - Main entry point for pushrebase operation
  • Conflict detection and changeset rebasing logic
  • Retry loop for concurrent bookmark updates
  • Integration with hooks

Bookmark Movement (repo_attributes/bookmarks/bookmarks_movement/):

  • pushrebase_onto.rs - High-level pushrebase operation
  • Permission checking and authorization
  • Logging and observability
  • Integration with bookmark update log

Pushrebase Client (features/pushrebase/client/):

  • local.rs - Direct in-process pushrebase
  • hybrid.rs - Client that can use local or remote pushrebase
  • facebook/land_service.rs - Pushrebase via land service microservice

Pushrebase Hooks:

  • features/pushrebase/pushrebase_hook/ - Hook trait definitions
  • features/pushrebase/pushrebase_hooks/ - Hook implementations and registry

The pushrebase operation is a feature (as described in the Architecture Overview) that composes multiple repository facets:

  • Bookmarks - Reading and updating bookmarks
  • CommitGraph - Ancestry queries and graph traversal
  • RepoBlobstore - Storing rebased changesets
  • HookManager - Running hooks
  • RepoDerivedData - Triggering derived data computation

Use in Sapling Workflows

Sapling clients use pushrebase as the default push mode for most bookmarks:

Push Operation:

sl push --to master

If the local base is behind the server's master bookmark, the server automatically rebases the commits. The client receives the rebased commits and updates its local state.

Mutation Information: Sapling tracks commit rewrites (mutations) caused by pushrebase. When the server rebases commits, it returns obsmarkers that map the original commit hashes to the rebased hashes. This allows Sapling to understand that the local commits and server commits are related.

Commit Cloud Integration: Pushrebase does not currently integrate with Commit Cloud (Sapling's feature for sharing uncommitted work). When commits are pushrebased, the mutation information is not synchronized across a developer's machines, but rather each separate machine will discover that the commits have been landed. This is an opportunity for improvement.

Use in Git Workflows

Git clients can also use pushrebase when pushing to Mononoke:

Push with Pushrebase: Git clients push using the standard Git protocol. If the server is configured for pushrebase on the target branch, the push operation triggers a pushrebase.

Mapping to Git: When rebased commits are created:

  • Mononoke generates corresponding Git commits
  • Git commit hashes are computed and stored in bonsai_git_mapping
  • The server responds to the Git client with the new commit hashes
  • The client can update its branch pointers without fetching

Differences from Sapling: Git does not have a native mutation tracking mechanism like Sapling's obsmarkers. Git clients receive the new commit hashes but do not have a built-in way to track the relationship between original and rebased commits.

Pushrebase vs. Fast-Forward Push

Mononoke supports two primary push modes:

Fast-Forward Push:

  • Client's head must be a descendant of the current bookmark position
  • No rebasing occurs
  • Push fails if the bookmark has moved since the client's pull
  • Used for bookmarks requiring strict lineage

Pushrebase:

  • Client's head may be based on an outdated bookmark position
  • Server rebases conflict-free changes automatically
  • Push succeeds as long as there are no file-level conflicts
  • Used for high-throughput bookmarks with many concurrent contributors

The choice between these modes is configured per-bookmark in repository configuration.

Pushrebase Mutation Mapping

The PushrebaseMutationMapping facet (in repo_attributes/pushrebase_mutation_mapping/) tracks the relationship between original commits and their rebased versions:

  • Original commit hash → Rebased commit hash mapping
  • Stored in the metadata database
  • Used by Sapling to track commit rewrites
  • Queried by SCS and other tools for commit history analysis

This mapping is populated by a pushrebase transaction hook and is accessible through the SCS API (Source Control Service) for programmatic queries.

Observability

Pushrebase operations are instrumented with metrics and logging:

Metrics:

  • pushrebase.critical_section_success_duration_us - Time spent in successful critical section
  • pushrebase.critical_section_failure_duration_us - Time spent in failed critical section
  • pushrebase.critical_section_retries_failed - Count of retry failures
  • pushrebase.commits_rebased - Number of commits rebased

Logging:

  • Scuba logging for all pushrebase operations
  • Bookmark update log records all bookmark movements
  • Hook execution is logged for debugging and auditing

Debugging: The admin tool provides commands for testing and debugging pushrebase:

admin commit pushrebase --help

Limitations and Edge Cases

Merge Commits: Pushrebase can be configured to reject merge commits (block_merges configuration). Repositories that maintain strict linear history use this setting.

Large Stacks: Rebasing very large commit stacks can be slow. The recursion_limit configuration prevents unbounded stack depth.

Concurrent Updates: If a bookmark is updated very frequently, pushrebase may exhaust retries. The client must retry the push operation.

Case Conflicts: On case-insensitive filesystems, pushrebase can detect potential case conflicts that might cause issues for some clients.

Hook Failures: If any hook fails during pushrebase, the entire operation is rolled back. The client receives the hook error message.

The pushrebase implementation is in features/pushrebase/ and its integration with bookmark movement is in repo_attributes/bookmarks/bookmarks_movement/src/pushrebase_onto.rs.