Jump to…
snowfeat: given a new redesign and emulated terminal homepagerxkspqmsoknz1mo
Matt W1//! `df-store` — the storage abstraction (spec §3).
Matt W2//!
Matt W3//! **This is the most important design constraint in the project.** The point of
Matt W4//! Git-backed storage is that migrating to a jj-native store later is a swap of
Matt W5//! one implementation, not a rewrite of the product. The rules, enforced in
Matt W6//! review and by `scripts/check-store-boundary.sh` in CI:
Matt W7//!
Matt W8//! 1. No crate other than `df-store` may depend on `gix`.
Matt W9//! 2. [`RevId`] is opaque. Do not parse it, do not assume it is 40 hex
Matt W10//! characters, do not abbreviate it outside this crate.
Matt W11//! 3. Application logic keys off `ChangeId`. `RevId` appears in the UI and in
Matt W12//! URLs only where the user is explicitly looking at a specific revision.
Matt W13//! 4. The `GitStore` implementation lives in `git/`. A future `JjStore` lives
Matt W14//! beside it, and nothing above this trait changes.
Matt W15//!
Matt W16//! No `gix` type appears anywhere in this module's public API.
Matt W17
Matt W18use std::collections::HashMap;
Matt W19use std::path::Path;
Matt W20
Matt W21use async_trait::async_trait;
Matt W22
Matt W23pub mod git;
Matt W24pub mod path;
Matt W25pub mod refname;
Matt W26
Matt W27pub use git::GitStore;
Matt W28
Matt W29/// Errors the application is expected to handle.
Matt W30///
Matt W31/// Deliberately storage-agnostic: nothing here mentions Git, so a `JjStore`
Matt W32/// would produce the same set.
Matt W33#[derive(Debug, thiserror::Error)]
Matt W34pub enum StoreError {
Matt W35 #[error("repository not found")]
Matt W36 NoSuchRepo,
Matt W37 #[error("revision not found")]
Matt W38 NoSuchRevision,
Matt W39 #[error("path not found")]
Matt W40 NoSuchPath,
Matt W41 #[error("path is a directory, not a file")]
Matt W42 IsDirectory,
Matt W43 #[error("repository is empty")]
Matt W44 Empty,
Matt W45 #[error("invalid path: {0}")]
Matt W46 Path(#[from] path::PathError),
Matt W47 #[error("object too large: {size} bytes exceeds the {limit} byte limit")]
Matt W48 TooLarge { size: u64, limit: u64 },
Matt W49 #[error("operation timed out")]
Matt W50 Timeout,
Matt W51 #[error(transparent)]
Matt W52 Other(#[from] anyhow::Error),
Matt W53}
Matt W54
Matt W55pub type Result<T> = std::result::Result<T, StoreError>;
Matt W56
Matt W57/// A repository's identity. Storage location derives from this, never from a
Matt W58/// user-supplied name — so renaming a repo moves nothing on disk (spec §3).
Matt W59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
Matt W60pub struct RepoId(pub uuid::Uuid);
Matt W61
Matt W62impl std::fmt::Display for RepoId {
Matt W63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Matt W64 write!(f, "{}", self.0)
Matt W65 }
Matt W66}
Matt W67
Matt W68/// An opaque revision handle.
Matt W69///
Matt W70/// Callers must treat this as a token, not a SHA. It is currently a Git object
Matt W71/// id; it may not always be. There is deliberately no `len()`, no indexing, and
Matt W72/// no abbreviation method on this type.
Matt W73#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
Matt W74pub struct RevId(String);
Matt W75
Matt W76impl RevId {
Matt W77 /// Construct from a stored value. Only `df-store` and the database layer
Matt W78 /// should call this.
Matt W79 pub fn from_stored(s: impl Into<String>) -> Self {
Matt W80 RevId(s.into())
Matt W81 }
Matt W82
Matt W83 /// The token, for storage and URLs. Not for parsing.
Matt W84 pub fn as_str(&self) -> &str {
Matt W85 &self.0
Matt W86 }
Matt W87}
Matt W88
Matt W89impl RevId {
Matt W90 /// The abbreviated form for display.
Matt W91 ///
Matt W92 /// Rule 2 says callers must not abbreviate a `RevId` themselves, so the
Matt W93 /// abbreviation lives here — the one place that is allowed to know how long
Matt W94 /// a revision token is and whether truncating it is even meaningful. A
Matt W95 /// future `JjStore` changes this method rather than every call site.
Matt W96 pub fn short(&self) -> &str {
Matt W97 abbreviate_rev(&self.0)
Matt W98 }
Matt W99}
Matt W100
Matt W101/// Abbreviate a revision token that has come back from the database as a
Matt W102/// plain string.
Matt W103///
Matt W104/// The database stores `RevId` values as text, so rows read back are `String`,
Matt W105/// not `RevId`. This is the sanctioned way to shorten one for display; slicing
Matt W106/// it at the call site is what `scripts/check-store-boundary.sh` rejects.
Matt W107///
Matt W108/// Char-boundary safe: a token is expected to be ASCII, but truncating a
Matt W109/// multi-byte string by byte index would panic, and a display helper must not
Matt W110/// be able to take the process down.
Matt W111pub fn abbreviate_rev(rev: &str) -> &str {
Matt W112 const DISPLAY_LEN: usize = 12;
Matt W113 if rev.len() <= DISPLAY_LEN {
Matt W114 return rev;
Matt W115 }
Matt W116 match rev.char_indices().nth(DISPLAY_LEN) {
Matt W117 Some((idx, _)) => &rev[..idx],
Matt W118 None => rev,
Matt W119 }
Matt W120}
Matt W121
Matt W122impl std::fmt::Display for RevId {
Matt W123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Matt W124 f.write_str(&self.0)
Matt W125 }
Matt W126}
Matt W127
Matt W128/// A person, as recorded on a revision.
Matt W129#[derive(Debug, Clone, PartialEq, Eq)]
Matt W130pub struct Signature {
Matt W131 pub name: String,
Matt W132 pub email: String,
Matt W133 pub when: chrono::DateTime<chrono::Utc>,
Matt W134}
Matt W135
Matt W136/// A revision as the application understands it.
Matt W137#[derive(Debug, Clone)]
Matt W138pub struct Revision {
Matt W139 pub rev: RevId,
Matt W140 /// Extracted from jj metadata; absent for plain-git commits.
Matt W141 pub change_id: Option<String>,
Matt W142 pub parents: Vec<RevId>,
Matt W143 pub author: Signature,
Matt W144 pub committer: Signature,
Matt W145 pub message: String,
Matt W146 pub conflicted: bool,
Matt W147 /// Populated when `conflicted`; the constituent tree tokens.
Matt W148 pub conflict_sides: Vec<String>,
Matt W149 pub conflict_bases: Vec<String>,
Matt W150}
Matt W151
Matt W152impl Revision {
Matt W153 /// First line of the message, for list views.
Matt W154 pub fn summary(&self) -> &str {
Matt W155 self.message.lines().next().unwrap_or("").trim()
Matt W156 }
Matt W157
Matt W158 /// Everything after the first line.
Matt W159 pub fn body(&self) -> &str {
Matt W160 match self.message.split_once('\n') {
Matt W161 Some((_, rest)) => rest.trim_start_matches('\n'),
Matt W162 None => "",
Matt W163 }
Matt W164 }
Matt W165}
Matt W166
Matt W167/// What a tree entry is.
Matt W168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Matt W169pub enum EntryKind {
Matt W170 File,
Matt W171 Directory,
Matt W172 /// A symlink. Never followed when serving content (spec §9).
Matt W173 Symlink,
Matt W174 /// A nested repository (gitlink). Rendered as a stub.
Matt W175 Submodule,
Matt W176}
Matt W177
Matt W178#[derive(Debug, Clone)]
Matt W179pub struct TreeEntry {
Matt W180 pub name: String,
Matt W181 /// Repository-relative, normalised.
Matt W182 pub path: String,
Matt W183 pub kind: EntryKind,
Matt W184 /// `None` for directories and submodules.
Matt W185 pub size: Option<u64>,
Matt W186 pub executable: bool,
Matt W187}
Matt W188
Matt W189impl TreeEntry {
Matt W190 pub fn is_dir(&self) -> bool {
Matt W191 matches!(self.kind, EntryKind::Directory)
Matt W192 }
Matt W193}
Matt W194
Matt W195/// File content.
Matt W196#[derive(Debug, Clone)]
Matt W197pub struct Blob {
Matt W198 pub path: String,
Matt W199 /// An opaque content address for this blob's bytes.
Matt W200 ///
Matt W201 /// Two blobs with the same bytes have the same `content_id`, in this or any
Matt W202 /// other repository. That is what makes it a correct cache key for anything
Matt W203 /// derived purely from content — syntax highlighting, in particular (spec
Matt W204 /// §8: "cache the rendered HTML in Postgres keyed by blob OID").
Matt W205 ///
Matt W206 /// Opaque under the same rule as [`RevId`]: do not parse it, do not assume
Matt W207 /// it is a Git object id, do not abbreviate it.
Matt W208 pub content_id: String,
Matt W209 pub content: Vec<u8>,
Matt W210 pub size: u64,
Matt W211 /// Whether the content looks binary, decided by the store so every caller
Matt W212 /// agrees.
Matt W213 pub binary: bool,
Matt W214}
Matt W215
Matt W216impl Blob {
Matt W217 /// UTF-8 text, when the blob is text and valid UTF-8.
Matt W218 pub fn text(&self) -> Option<&str> {
Matt W219 if self.binary {
Matt W220 return None;
Matt W221 }
Matt W222 std::str::from_utf8(&self.content).ok()
Matt W223 }
Matt W224}
Matt W225
Matt W226/// A movable pointer. Cheap, disposable, not an identity (spec §4).
Matt W227#[derive(Debug, Clone)]
Matt W228pub struct Bookmark {
Matt W229 pub name: String,
Matt W230 pub target: RevId,
Matt W231}
Matt W232
Matt W233#[derive(Debug, Clone, Copy)]
Matt W234pub struct DiffOpts {
Matt W235 pub context_lines: u32,
Matt W236 pub max_files: usize,
Matt W237 pub max_lines: usize,
Matt W238}
Matt W239
Matt W240impl Default for DiffOpts {
Matt W241 fn default() -> Self {
Matt W242 DiffOpts {
Matt W243 // Spec §8: "Collapse unchanged regions to 3 lines of context."
Matt W244 context_lines: 3,
Matt W245 max_files: 5_000,
Matt W246 max_lines: 100_000,
Matt W247 }
Matt W248 }
Matt W249}
Matt W250
Matt W251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Matt W252pub enum ChangeKind {
Matt W253 Added,
Matt W254 Modified,
Matt W255 Deleted,
Matt W256 Renamed,
Matt W257}
Matt W258
Matt W259#[derive(Debug, Clone)]
Matt W260pub struct FileDiff {
Matt W261 pub path: String,
Matt W262 /// Set for renames.
Matt W263 pub old_path: Option<String>,
Matt W264 pub kind: ChangeKind,
Matt W265 pub binary: bool,
Matt W266 pub additions: usize,
Matt W267 pub deletions: usize,
Matt W268 pub hunks: Vec<Hunk>,
Matt W269}
Matt W270
Matt W271#[derive(Debug, Clone)]
Matt W272pub struct Hunk {
Matt W273 pub old_start: u32,
Matt W274 pub old_lines: u32,
Matt W275 pub new_start: u32,
Matt W276 pub new_lines: u32,
Matt W277 pub lines: Vec<DiffLine>,
Matt W278}
Matt W279
Matt W280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Matt W281pub enum DiffLineKind {
Matt W282 Context,
Matt W283 Added,
Matt W284 Deleted,
Matt W285}
Matt W286
Matt W287#[derive(Debug, Clone)]
Matt W288pub struct DiffLine {
Matt W289 pub kind: DiffLineKind,
Matt W290 pub old_lineno: Option<u32>,
Matt W291 pub new_lineno: Option<u32>,
Matt W292 pub content: String,
Matt W293 /// The line broken into runs, with the runs that actually differ from the
Matt W294 /// paired line marked (spec §8: "word-level intra-line diffing for changed
Matt W295 /// lines").
Matt W296 ///
Matt W297 /// Always covers the whole line: concatenating `spans` reproduces
Matt W298 /// `content`. Context lines get a single unemphasised span, so a renderer
Matt W299 /// can use `spans` uniformly and never has to fall back to `content`.
Matt W300 pub spans: Vec<DiffSpan>,
Matt W301}
Matt W302
Matt W303/// A run within a diff line.
Matt W304#[derive(Debug, Clone, PartialEq, Eq)]
Matt W305pub struct DiffSpan {
Matt W306 pub text: String,
Matt W307 /// Whether this run is part of what changed on this line.
Matt W308 pub emphasis: bool,
Matt W309}
Matt W310
Matt W311#[derive(Debug, Clone, Default)]
Matt W312pub struct Diff {
Matt W313 pub files: Vec<FileDiff>,
Matt W314 /// Set when the diff exceeded a limit and was not fully rendered. The UI
Matt W315 /// offers the patch download instead of taking the process down (spec §8).
Matt W316 pub truncated: bool,
Matt W317 pub total_additions: usize,
Matt W318 pub total_deletions: usize,
Matt W319}
Matt W320
Matt W321/// Which column of a conflict a piece of content came from.
Matt W322///
Matt W323/// jj's representation is `side₀ base₀ side₁ …`, and the labels matter to a
Matt W324/// reader: a base is the common ancestor the sides diverged from, not another
Matt W325/// candidate resolution.
Matt W326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Matt W327pub enum ConflictSide {
Matt W328 Side(usize),
Matt W329 Base(usize),
Matt W330}
Matt W331
Matt W332impl ConflictSide {
Matt W333 /// A short label for the UI.
Matt W334 pub fn label(&self) -> String {
Matt W335 match self {
Matt W336 ConflictSide::Side(n) => format!("side {}", n + 1),
Matt W337 ConflictSide::Base(n) => format!("base {}", n + 1),
Matt W338 }
Matt W339 }
Matt W340
Matt W341 pub fn is_base(&self) -> bool {
Matt W342 matches!(self, ConflictSide::Base(_))
Matt W343 }
Matt W344}
Matt W345
Matt W346/// One file in conflict, with the content each column holds.
Matt W347#[derive(Debug, Clone)]
Matt W348pub struct ConflictedFile {
Matt W349 pub path: String,
Matt W350 /// One entry per column, in jj's order. `None` means that column does not
Matt W351 /// contain the file at all — a delete/modify conflict, which is exactly the
Matt W352 /// case a viewer that only rendered text would silently misreport.
Matt W353 pub sides: Vec<(ConflictSide, Option<String>)>,
Matt W354}
Matt W355
Matt W356/// What happened when a change was landed.
Matt W357#[derive(Debug, Clone, PartialEq, Eq)]
Matt W358pub enum MergeOutcome {
Matt W359 /// The bookmark was already at or past this revision. Idempotent, so two
Matt W360 /// people clicking merge is a race rather than an error.
Matt W361 AlreadyMerged,
Matt W362 /// The bookmark was an ancestor, so it simply moved. No commit written.
Matt W363 FastForward { new_target: RevId },
Matt W364 /// A merge commit was written, with the bookmark as first parent.
Matt W365 Merged { new_target: RevId },
Matt W366 /// The three-way merge produced a textual conflict.
Matt W367 ///
Matt W368 /// Nothing was written. Spec §4: resolution happens in the user's working
Matt W369 /// copy, so the server must never resolve by writing conflict markers into
Matt W370 /// the target bookmark.
Matt W371 Conflicted,
Matt W372}
Matt W373
Matt W374/// What happened when a single-file edit was committed.
Matt W375#[derive(Debug, Clone, PartialEq, Eq)]
Matt W376pub enum EditOutcome {
Matt W377 Committed { rev: RevId },
Matt W378 /// The content is identical to what is already there. No commit is made —
Matt W379 /// pressing save on an unmodified file is not a change.
Matt W380 Unchanged,
Matt W381 /// The bookmark moved since the edit was composed. Nothing was written;
Matt W382 /// the caller shows the conflict rather than overwriting somebody's work.
Matt W383 Stale { current: RevId },
Matt W384}
Matt W385
Matt W386/// A single line of blame output.
Matt W387#[derive(Debug, Clone)]
Matt W388pub struct BlameLine {
Matt W389 /// The revision that last touched this line.
Matt W390 pub rev: RevId,
Matt W391 /// Author of that revision.
Matt W392 pub author: String,
Matt W393 /// When the revision was authored.
Matt W394 pub when: chrono::DateTime<chrono::Utc>,
Matt W395 /// First line of the commit message.
Matt W396 pub summary: String,
Matt W397 /// 1-based line number in the final file.
Matt W398 pub line_no: usize,
Matt W399}
Matt W400
Matt W401/// Everything the application knows about repository storage.
Matt W402///
Matt W403/// No `gix` type appears in this trait. No caller may depend on the underlying
Matt W404/// store being Git.
Matt W405#[async_trait]
Matt W406pub trait RepoStore: Send + Sync {
Matt W407 async fn create(&self, id: RepoId, default_bookmark: &str) -> Result<()>;
Matt W408
Matt W409 /// Ensure push-time validation is wired up for this repository.
Matt W410 ///
Matt W411 /// Deliberately named for the intent rather than the mechanism: the Git
Matt W412 /// implementation installs a `pre-receive` hook, but a jj-native store
Matt W413 /// would enforce the same rules some other way, and nothing above this
Matt W414 /// trait should have to care which.
Matt W415 async fn configure_receive_validation(&self, id: RepoId, hook_binary: &str) -> Result<()>;
Matt W416 async fn delete(&self, id: RepoId) -> Result<()>;
Matt W417 /// Whether the repository exists on disk.
Matt W418 async fn exists(&self, id: RepoId) -> bool;
Matt W419 /// Whether the repository has no commits yet.
Matt W420 async fn is_empty(&self, id: RepoId) -> Result<bool>;
Matt W421
Matt W422 async fn list_tree(&self, id: RepoId, rev: &RevId, path: &Path) -> Result<Vec<TreeEntry>>;
Matt W423 async fn read_blob(&self, id: RepoId, rev: &RevId, path: &Path) -> Result<Blob>;
Matt W424 async fn diff(&self, id: RepoId, from: &RevId, to: &RevId, opts: DiffOpts) -> Result<Diff>;
Matt W425
Matt W426 /// Diff a revision against its first parent.
Matt W427 ///
Matt W428 /// A root commit has no parent, so it is diffed against an empty tree —
Matt W429 /// which is what makes every commit, including the first, yield a patch.
Matt W430 /// The empty-tree handling lives here rather than in the caller because it
Matt W431 /// is a storage detail; nothing above this trait should know such a thing
Matt W432 /// exists.
Matt W433 async fn diff_from_parent(&self, id: RepoId, rev: &RevId, opts: DiffOpts) -> Result<Diff>;
Matt W434
Matt W435 /// Added/deleted line counts for many revisions, each against its first
Matt W436 /// parent.
Matt W437 ///
Matt W438 /// The change list needs a diffstat per row, and the list is the hottest
Matt W439 /// page in the product (spec §4). Calling [`Self::diff_from_parent`] in a
Matt W440 /// loop would open the repository once per row and materialise every hunk
Matt W441 /// of every patch to count two numbers; this opens it once and keeps only
Matt W442 /// the totals.
Matt W443 ///
Matt W444 /// Best-effort per revision: a revision the store cannot resolve yields
Matt W445 /// `None` rather than failing the batch, because one unreadable row must
Matt W446 /// not blank out the other ninety-nine.
Matt W447 async fn diff_stats(&self, id: RepoId, revs: &[RevId]) -> Result<Vec<Option<(usize, usize)>>>;
Matt W448
Matt W449 async fn log(&self, id: RepoId, from: &RevId, limit: usize) -> Result<Vec<Revision>>;
Matt W450 async fn revision(&self, id: RepoId, rev: &RevId) -> Result<Revision>;
Matt W451 async fn bookmarks(&self, id: RepoId) -> Result<Vec<Bookmark>>;
Matt W452 async fn merge_base(&self, id: RepoId, a: &RevId, b: &RevId) -> Result<Option<RevId>>;
Matt W453 async fn is_ancestor(&self, id: RepoId, a: &RevId, b: &RevId) -> Result<bool>;
Matt W454
Matt W455 /// Resolve a bookmark name or revision token to a concrete revision.
Matt W456 async fn resolve(&self, id: RepoId, spec: &str) -> Result<RevId>;
Matt W457
Matt W458 /// Replace one file on `bookmark` and commit the result.
Matt W459 ///
Matt W460 /// The write path behind in-browser editing. `expected_tip` is the revision
Matt W461 /// the edit was composed against: if the bookmark has moved, nothing is
Matt W462 /// written and [`EditOutcome::Stale`] comes back, so a concurrent edit is
Matt W463 /// reported rather than silently overwritten.
Matt W464 ///
Matt W465 /// Always a fast-forward by construction — one new commit whose parent is
Matt W466 /// the current tip.
Matt W467 #[allow(clippy::too_many_arguments)]
Matt W468 async fn commit_file(
Matt W469 &self,
Matt W470 id: RepoId,
Matt W471 bookmark: &str,
Matt W472 expected_tip: &RevId,
Matt W473 path: &str,
Matt W474 content: Vec<u8>,
Matt W475 message: &str,
Matt W476 author: &Signature,
Matt W477 ) -> Result<EditOutcome>;
Matt W478
Matt W479 /// Land `rev` onto `bookmark`.
Matt W480 ///
Matt W481 /// Fast-forwards where possible and writes a merge commit otherwise. The
Matt W482 /// reference update is a compare-and-swap, so a concurrent push cannot be
Matt W483 /// silently discarded. Refuses on conflict without writing anything.
Matt W484 async fn merge(
Matt W485 &self,
Matt W486 id: RepoId,
Matt W487 bookmark: &str,
Matt W488 rev: &RevId,
Matt W489 message: &str,
Matt W490 author: &Signature,
Matt W491 ) -> Result<MergeOutcome>;
Matt W492
Matt W493 /// The conflicted files in a revision, with each side's content.
Matt W494 ///
Matt W495 /// Empty for an unconflicted revision. Expressed in terms of *files and
Matt W496 /// sides* rather than trees so the conflict viewer never learns that the
Matt W497 /// underlying store represents a conflict as a list of trees (spec §3
Matt W498 /// rule 4).
Matt W499 async fn conflicts(&self, id: RepoId, rev: &RevId) -> Result<Vec<ConflictedFile>>;
Matt W500
Matt W501 /// Per-line blame for a file at a given revision.
Matt W502 ///
Matt W503 /// Returns one [`BlameLine`] per line of the file. Blame is expensive;
Matt W504 /// callers should only invoke this when the user explicitly requests it
Matt W505 /// (e.g. via a toggle).
Matt W506 async fn blame(&self, id: RepoId, rev: &RevId, path: &Path) -> Result<Vec<BlameLine>>;
Matt W507
Matt W508 /// The most recent revision that modified a specific file path.
Matt W509 ///
Matt W510 /// Walks history from `rev` and returns the first commit whose diff
Matt W511 /// against its parent touches `path`. Returns `None` if the path was
Matt W512 /// never modified (should not happen for paths that exist).
Matt W513 async fn last_commit_for_path(
Matt W514 &self,
Matt W515 id: RepoId,
Matt W516 rev: &RevId,
Matt W517 path: &Path,
Matt W518 ) -> Result<Option<Revision>>;
Matt W519
Matt W520 /// The most recent commit that touched each direct child of `dir`, in one
Matt W521 /// history walk.
Matt W522 ///
Matt W523 /// This is [`last_commit_for_path`](Self::last_commit_for_path) generalised
Matt W524 /// to a whole directory listing: rather than one history walk per entry —
Matt W525 /// which is what a naive per-file implementation of this would cost, and
Matt W526 /// what made a directory-listing "last commit" column too expensive to
Matt W527 /// ship the first time around — this walks history once and, at each
Matt W528 /// commit, checks its changed paths against every entry that has not yet
Matt W529 /// been resolved. An entry resolves the first time a changed path falls
Matt W530 /// under it; the walk stops early once every entry has resolved, or after
Matt W531 /// the same 500-commit bound the single-file lookup uses.
Matt W532 ///
Matt W533 /// `entries` are names relative to `dir` (not full paths). An entry
Matt W534 /// missing from the returned map was not modified within the walk bound —
Matt W535 /// callers render that as "no info" rather than treating it as an error.
Matt W536 async fn last_commits_in_dir(
Matt W537 &self,
Matt W538 id: RepoId,
Matt W539 rev: &RevId,
Matt W540 dir: &Path,
Matt W541 entries: &[String],
Matt W542 ) -> Result<HashMap<String, Revision>>;
Matt W543
Matt W544 /// Total on-disk size, for the repo settings page and quota reporting.
Matt W545 async fn size_bytes(&self, id: RepoId) -> Result<u64>;
Matt W546}
Matt W547
Matt W548#[cfg(test)]
Matt W549mod tests {
Matt W550 use super::*;
Matt W551
Matt W552 #[test]
Matt W553 fn revision_summary_and_body_split_on_the_first_line() {
Matt W554 let r = Revision {
Matt W555 rev: RevId::from_stored("x"),
Matt W556 change_id: None,
Matt W557 parents: vec![],
Matt W558 author: Signature {
Matt W559 name: "a".into(),
Matt W560 email: "b".into(),
Matt W561 when: chrono::Utc::now(),
Matt W562 },
Matt W563 committer: Signature {
Matt W564 name: "a".into(),
Matt W565 email: "b".into(),
Matt W566 when: chrono::Utc::now(),
Matt W567 },
Matt W568 message: "the summary\n\nthe body\nmore body\n".into(),
Matt W569 conflicted: false,
Matt W570 conflict_sides: vec![],
Matt W571 conflict_bases: vec![],
Matt W572 };
Matt W573 assert_eq!(r.summary(), "the summary");
Matt W574 assert_eq!(r.body(), "the body\nmore body\n");
Matt W575 }
Matt W576
Matt W577 #[test]
Matt W578 fn a_single_line_message_has_no_body() {
Matt W579 let mut r = Revision {
Matt W580 rev: RevId::from_stored("x"),
Matt W581 change_id: None,
Matt W582 parents: vec![],
Matt W583 author: Signature { name: "a".into(), email: "b".into(), when: chrono::Utc::now() },
Matt W584 committer: Signature { name: "a".into(), email: "b".into(), when: chrono::Utc::now() },
Matt W585 message: "only a summary".into(),
Matt W586 conflicted: false,
Matt W587 conflict_sides: vec![],
Matt W588 conflict_bases: vec![],
Matt W589 };
Matt W590 assert_eq!(r.summary(), "only a summary");
Matt W591 assert_eq!(r.body(), "");
Matt W592 r.message = String::new();
Matt W593 assert_eq!(r.summary(), "");
Matt W594 }
Matt W595
Matt W596 #[test]
Matt W597 fn abbreviation_lives_in_the_store_and_is_boundary_safe() {
Matt W598 assert_eq!(abbreviate_rev("0123456789abcdef0123"), "0123456789ab");
Matt W599 assert_eq!(abbreviate_rev("short"), "short");
Matt W600 assert_eq!(abbreviate_rev(""), "");
Matt W601 // A multi-byte token must not panic on truncation.
Matt W602 let multi = "\u{e9}".repeat(20);
Matt W603 let out = abbreviate_rev(&multi);
Matt W604 assert_eq!(out.chars().count(), 12);
Matt W605 assert_eq!(RevId::from_stored("0123456789abcdef").short(), "0123456789ab");
Matt W606 }
Matt W607
Matt W608 #[test]
Matt W609 fn binary_blobs_never_yield_text() {
Matt W610 let b = Blob {
Matt W611 path: "x".into(),
Matt W612 content_id: "cid".into(),
Matt W613 content: b"hello".to_vec(),
Matt W614 size: 5,
Matt W615 binary: true,
Matt W616 };
Matt W617 assert_eq!(b.text(), None, "a blob marked binary must not be rendered as text");
Matt W618 }
Matt W619}

619 lines · Rust