eden/mononoke/docs/2.2-repository-facets.md
This document explains the facet pattern and how repositories are composed in Mononoke. Facets are trait-based components that provide specific repository capabilities. Understanding facets is essential for working with Mononoke, as they form the foundation of how code accesses repository functionality.
A facet is a trait-based component that provides a specific capability for a repository. Rather than having a single repository class with all possible methods, Mononoke breaks repository functionality into discrete facets that can be composed together. Each facet encapsulates a single responsibility and may contain state that forms part of the repository.
For example, RepoIdentity provides repository name and ID, RepoBlobstore provides access to immutable blob storage, and CommitGraph provides commit graph traversal operations. Functions declare their requirements explicitly by specifying which facets they need through trait bounds.
Facets are defined in the repo_attributes/ directory and are used throughout Mononoke's codebase as the standard way to access repository functionality. As explained in the Architecture Overview, facets form the composition layer between base components (storage, types, utilities) and higher-level features (pushrebase, cross-repo sync).
Facets are defined using the #[facet::facet] macro, which generates the necessary infrastructure for the facet pattern. Here's a simple example from repo_attributes/repo_identity/src/lib.rs:
#[facet::facet]
#[derive(Clone, Hash, PartialEq, Eq)]
pub struct RepoIdentity {
id: RepositoryId,
name: String,
}
impl RepoIdentity {
pub fn id(&self) -> RepositoryId {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
}
The #[facet::facet] macro generates:
RepoIdentityRef, RepoIdentityArc) for accessing the facet from generic containersArcRepoIdentity)Facets can hold state (like configuration, cached data, or storage handles) or simply provide access to underlying systems. More complex facets like CommitGraph and RepoBlobstore wrap storage backends and provide domain-specific operations.
Functions declare their facet requirements using trait bounds. This makes dependencies explicit and allows the compiler to verify that the necessary capabilities are available. Here's an example:
use commit_graph::CommitGraph;
use repo_blobstore::RepoBlobstore;
use repo_identity::RepoIdentity;
async fn example_operation(
ctx: &CoreContext,
repo: &(impl CommitGraph + RepoBlobstore + RepoIdentity),
cs_id: ChangesetId,
) -> Result<()> {
// Access facets through generated accessor methods
let repo_name = repo.repo_identity().name();
let blobstore = repo.repo_blobstore();
let parents = repo.commit_graph()
.changeset_parents(ctx, cs_id)
.await?;
// Function only has access to the three declared facets
Ok(())
}
This pattern has several characteristics:
Explicit Dependencies - The function signature declares exactly which repository capabilities are required. A function needing only identity and blobstore access cannot accidentally depend on commit graph operations.
Composability - Different repository types can provide different combinations of facets. Test repositories can provide mock implementations of specific facets.
Compile-Time Verification - If a function attempts to access a facet not declared in its trait bounds, the code will not compile.
For functions requiring many facets, trait aliases can reduce verbosity:
pub trait MyOperationRepo = CommitGraph
+ RepoBlobstore
+ RepoIdentity
+ RepoDerivedData;
async fn complex_operation(
ctx: &CoreContext,
repo: &impl MyOperationRepo,
) -> Result<()> {
// Function can use all four facets
}
Mononoke provides approximately 35 facets, organized into several functional categories. Each facet is implemented in its own directory under repo_attributes/ or related top-level directories.
RepoIdentity (repo_attributes/repo_identity/)
RepoBookmarkAttrs (repo_attributes/repo_bookmark_attrs/)
RepoBlobstore (repo_attributes/repo_blobstore/)
Filestore (repo_attributes/filestore/)
MutableBlobstore (repo_attributes/mutable_blobstore/)
CommitGraph (repo_attributes/commit_graph/commit_graph/)
Phases (repo_attributes/phases/)
RepoDerivedData (repo_attributes/repo_derived_data/)
RepoDerivationQueues (repo_attributes/repo_derivation_queues/)
These facets map between Bonsai (Mononoke's internal format) and external VCS identities:
BonsaiHgMapping (repo_attributes/bonsai_hg_mapping/)
BonsaiGitMapping (repo_attributes/bonsai_git_mapping/)
BonsaiGlobalrevMapping (repo_attributes/bonsai_globalrev_mapping/)
BonsaiSvnrevMapping (repo_attributes/bonsai_svnrev_mapping/)
BonsaiBlobMapping (repo_attributes/bonsai_blob_mapping/)
BonsaiTagMapping (repo_attributes/bonsai_tag_mapping/)
Bookmarks (repo_attributes/bookmarks/bookmarks/)
BookmarkUpdateLog (within repo_attributes/bookmarks/)
BookmarksCache (within repo_attributes/bookmarks/)
GitSymbolicRefs (repo_attributes/git_symbolic_refs/)
GitRefContentMapping (repo_attributes/git_ref_content_mapping/)
GitSourceOfTruth (repo_attributes/git_source_of_truth/)
Filenodes (repo_attributes/filenodes/)
Newfilenodes (repo_attributes/newfilenodes/)
MutableCounters (repo_attributes/mutable_counters/)
MutableRenames (repo_attributes/mutable_renames/)
RepoPermissionChecker (repo_attributes/repo_permission_checker/)
HookManager (repo_attributes/hook_manager/)
RepoCrossRepo (repo_attributes/repo_cross_repo/)
RepoLock (repo_attributes/repo_lock/)
RepoSparseProfiles (repo_attributes/repo_sparse_profiles/)
RestrictedPaths (repo_attributes/restricted_paths/)
PushrebaseMutationMapping (repo_attributes/pushrebase_mutation_mapping/)
DeletionLog (repo_attributes/deletion_log/)
RepoMetadataCheckpoint (repo_attributes/repo_metadata_checkpoint/)
RepoEventPublisher (repo_attributes/repo_event_publisher/)
SqlQueryConfig (repo_attributes/sql_query_config/)
The repo_factory/ directory contains the code responsible for constructing repositories from configuration. The factory:
The resulting repository object implements all the accessor traits, allowing code to access any facet through the appropriate trait bound.
Test code uses TestRepoFactory to create repositories with test-appropriate backends. For example:
let factory = TestRepoFactory::new()?;
let repo = factory
.with_name("test_repo")
.with_id(RepositoryId::new(1))
.build()
.await?;
Test repositories typically use in-memory storage (memblob, SQLite) rather than production backends. Individual facets can be mocked or replaced for testing specific behaviors.
As described in the Architecture Overview, features sit one layer above facets. Features are implemented as functions that combine multiple facets to provide high-level source control operations. Unlike facets, features do not hold state—they orchestrate operations across repository attributes.
For example, the pushrebase feature (in features/pushrebase/) combines facets like CommitGraph, Bookmarks, RepoBlobstore, HookManager, and RepoDerivedData to implement server-side rebasing. The feature function declares its facet requirements through trait bounds and uses those facets to perform the operation.
This separation allows features to be reused across different repository types and contexts. A pushrebase function can be called from the main server (via the API layer), from the land service (directly), or from the admin tool (for testing), all using the same implementation.
Several patterns are consistent across facet implementations:
Single Responsibility - Each facet encapsulates one aspect of repository functionality. RepoIdentity provides identity, CommitGraph provides graph operations, and so on.
State Encapsulation - Facets can hold state (configuration, storage handles, caches) but expose this state through well-defined methods rather than public fields.
Storage Abstraction - Facets abstract over storage details. CommitGraph can use SQL storage, in-memory storage, or cached storage without changing its interface.
Async Operations - Operations that perform I/O are async, allowing concurrent access and efficient resource usage.
Arc-Wrapped Types - Facets are typically used through Arc (atomic reference counting) to enable shared ownership across async operations. The macro generates Arc* type aliases for convenience.
All repository attribute facets are located in repo_attributes/. Each facet has its own subdirectory containing:
src/ - Rust source codeBUCK - Build definitionssql_bookmarks/)The Navigating the Codebase guide provides additional detail on finding and understanding facet implementations.
When writing code that uses repositories:
Declare Minimal Requirements - Only require the facets actually needed. A function that only reads repository identity should require impl RepoIdentity, not all possible facets.
Use Trait Aliases for Complex Requirements - If many functions need the same set of facets, define a trait alias to reduce duplication.
Avoid Storing Facets Directly - Use Arc wrappers when facets need to be stored. The generated Arc* type aliases make this straightforward.
Access Through Trait Methods - Use the generated accessor methods (e.g., repo.repo_identity()) rather than attempting to downcast or access internals.
When adding new facets:
Follow the Pattern - Use #[facet::facet] macro and follow existing facet structure.
Place in repo_attributes - New facets should be subdirectories of repo_attributes/.
Document the Interface - Add rustdoc comments explaining what the facet provides.
Register with Factory - Update repo_factory/ to construct the new facet when creating repositories.
This document covered the facet pattern and how repositories are composed from facets. To dive deeper:
RepoDerivedData facet manages derived data typesRepoBlobstore and metadata storage facets workThe facet pattern is used consistently throughout Mononoke. Understanding how to declare facet requirements and access facet functionality is fundamental to working with the codebase.