2022-12-10 07:44:10 +00:00
|
|
|
# Architecture
|
|
|
|
|
|
|
|
## Data model
|
|
|
|
|
|
|
|
The commit data model is similar
|
|
|
|
to [Git's object model](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects)
|
|
|
|
, but with some differences.
|
|
|
|
|
|
|
|
## Separation of library from UI
|
|
|
|
|
2023-06-28 14:12:40 +00:00
|
|
|
The `jj` binary consists of two Rust crates: the library crate (`jj-lib`) and
|
|
|
|
the CLI crate (`jj-cli`). The library crate is currently only used by the CLI
|
|
|
|
crate, but it is meant to also be usable from a GUI or TUI, or in a server
|
|
|
|
serving requests from multiple users. As a result, the library should avoid
|
|
|
|
interacting directly with the user via the terminal or by other means; all
|
|
|
|
input/output is handled by the CLI crate [^1]. Since the library crate is meant
|
|
|
|
to usable in a server, it also cannot read configuration from the user's home
|
|
|
|
directory, or from user-specific environment variables.
|
2022-12-10 07:44:10 +00:00
|
|
|
|
|
|
|
[^1]: There are a few exceptions, such as for messages printed during automatic
|
|
|
|
upgrades of the repo format
|
|
|
|
|
|
|
|
A lot of thought has gone into making the library crate's API easy to use, but
|
|
|
|
not much has gone into "details" such as which collection types are used, or
|
|
|
|
which symbols are exposed in the API.
|
|
|
|
|
|
|
|
## Storage-independent APIs
|
|
|
|
|
|
|
|
One overarching principle in the design is that it should be easy to change
|
|
|
|
where data is stored. The goal was to be able to put storage on local-disk by
|
|
|
|
default but also be able to move storage to the cloud at Google
|
|
|
|
(and for anyone). To that end, commits (and trees, files, etc.) are stored by
|
|
|
|
the commit backend, operations (and views) are stored by the operation backend,
|
|
|
|
the heads of the operation log are stored by the "op heads" backend, the commit
|
|
|
|
index is stored by the index backend, and the working copy is stored by the
|
|
|
|
working copy backend. The interfaces are defined in terms of plain Rust data
|
2023-08-13 13:55:41 +00:00
|
|
|
types, not tied to a specific format. The working copy doesn't have its own
|
2022-12-10 07:44:10 +00:00
|
|
|
trait defined yet, but its interface is small and easy to create traits for when
|
|
|
|
needed.
|
|
|
|
|
|
|
|
The commit backend to use when loading a repo is specified in
|
2023-08-13 13:55:41 +00:00
|
|
|
the `.jj/repo/store/type` file. There are similar files for the other backends
|
|
|
|
(`.jj/repo/index/type`, `.jj/repo/op_store/type`, `.jj/repo/op_heads/type`).
|
2022-12-10 07:44:10 +00:00
|
|
|
|
|
|
|
## Design of the library crate
|
|
|
|
|
|
|
|
### Overview
|
|
|
|
|
|
|
|
Here's a diagram showing some important types in the library crate. The
|
|
|
|
following sections describe each component.
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
graph TD;
|
|
|
|
ReadonlyRepo-->Store;
|
|
|
|
ReadonlyRepo-->OpStore;
|
|
|
|
ReadonlyRepo-->OpHeadsStore;
|
|
|
|
ReadonlyRepo-->ReadonlyIndex
|
|
|
|
MutableIndex-->ReadonlyIndex;
|
|
|
|
Store-->Backend;
|
|
|
|
GitBackend-->Backend;
|
|
|
|
LocalBackend-->Backend;
|
|
|
|
LocalBackend-->StackedTable;
|
|
|
|
MutableRepo-->ReadonlyRepo;
|
|
|
|
MutableRepo-->MutableIndex;
|
|
|
|
Transaction-->MutableRepo;
|
|
|
|
WorkingCopy-->TreeState;
|
|
|
|
Workspace-->WorkingCopy;
|
|
|
|
Workspace-->RepoLoader;
|
|
|
|
RepoLoader-->Store;
|
|
|
|
RepoLoader-->OpStore;
|
|
|
|
RepoLoader-->OpHeadsStore;
|
|
|
|
RepoLoader-->ReadonlyRepo;
|
|
|
|
Git-->GitBackend;
|
|
|
|
GitBackend-->StackedTable;
|
|
|
|
```
|
|
|
|
|
|
|
|
### Backend
|
|
|
|
|
2023-08-13 01:38:57 +00:00
|
|
|
The `Backend` trait defines the interface each
|
2022-12-10 07:44:10 +00:00
|
|
|
commit backend needs to implement. The current in-tree commit backends
|
2023-08-13 01:38:57 +00:00
|
|
|
are `GitBackend`
|
|
|
|
and `LocalBackend`.
|
2022-12-10 07:44:10 +00:00
|
|
|
|
|
|
|
Since there are non-commit backends, the `Backend` trait should probably be
|
|
|
|
renamed to `CommitBackend`.
|
|
|
|
|
|
|
|
### GitBackend
|
|
|
|
|
|
|
|
The `GitBackend` stores commits in a Git repository. It uses `libgit2` to read
|
|
|
|
and write commits and refs.
|
|
|
|
|
|
|
|
To prevent GC from deleting commits that are still reachable from the operation
|
|
|
|
log, the `GitBackend` stores a ref for each commit in the operation log in
|
|
|
|
the `refs/jj/keep/` namespace.
|
|
|
|
|
|
|
|
Commit data that is available in Jujutsu's model but not in Git's model is
|
|
|
|
stored in a `StackedTable` in `.jj/repo/store/extra/`. That is currently the
|
|
|
|
change ID and the list of predecessors. For commits that don't have any data in
|
|
|
|
that table, which is any commit created by `git`, we use an empty list as
|
|
|
|
predecessors, and the bit-reversed commit ID as change ID.
|
|
|
|
|
|
|
|
Because we use the Git Object ID as commit ID, two commits that differ only in
|
|
|
|
their change ID, for example, will get the same commit ID, so we error out when
|
|
|
|
trying to write the second one of them.
|
|
|
|
|
|
|
|
### LocalBackend
|
|
|
|
|
|
|
|
The `LocalBackend` is just a proof of concept. It stores objects addressed by
|
|
|
|
their hash, with one file per object.
|
|
|
|
|
|
|
|
### Store
|
|
|
|
|
|
|
|
The `Store` type wraps the `Backend` and returns wrapped types for commits and
|
|
|
|
trees to make them easier to use. The wrapped objects have a reference to
|
|
|
|
the `Store` itself, so you can do e.g. `commit.parents()` without having to
|
|
|
|
provide the `Store` as an argument.
|
|
|
|
|
|
|
|
The `Store` type also provides caching of commits and trees.
|
|
|
|
|
|
|
|
### ReadonlyRepo
|
|
|
|
|
|
|
|
A `ReadonlyRepo` represents the state of a repo at a specific operation. It
|
|
|
|
keeps the view object associated with that operation.
|
|
|
|
|
|
|
|
The repository doesn't know where on disk any working copies live. It knows, via
|
|
|
|
the view object, which commit is supposed to be the current working-copy commit
|
|
|
|
in each workspace.
|
|
|
|
|
|
|
|
### MutableRepo
|
|
|
|
|
|
|
|
A `MutableRepo` is a mutable version of `ReadonlyRepo`. It has a reference to
|
|
|
|
its base `ReadonlyRepo`, but it has its own copy of the view object and lets the
|
|
|
|
caller modify it.
|
|
|
|
|
|
|
|
### Transaction
|
|
|
|
|
|
|
|
The `Transaction` object has a `MutableRepo` and metadata that will go into the
|
|
|
|
operation log. When the transaction commits, the `MutableRepo` becomes a view
|
|
|
|
object in the operation log on disk, and the `Transaction` object becomes an
|
|
|
|
operation object. In memory, `Transaction::commit()` returns a
|
|
|
|
new `ReadonlyRepo`.
|
|
|
|
|
|
|
|
### RepoLoader
|
|
|
|
|
|
|
|
The `RepoLoader` represents a repository at an unspecified operation. You can
|
|
|
|
think of as a pointer to the `.jj/repo/` directory. It can create
|
|
|
|
a `ReadonlyRepo` given an operation ID.
|
|
|
|
|
|
|
|
### TreeState
|
|
|
|
|
|
|
|
The `TreeState` type represents the state of the files in a working copy. It
|
|
|
|
keep track of the mtime and size for each tracked file. It knows the `TreeId`
|
|
|
|
that the working copy represents. It has a `snapshot()` method that will use the
|
|
|
|
recorded mtimes and sizes and detect changes in the working copy. If anything
|
|
|
|
changed, it will return a new `TreeId`. It also has `checkout()` for updating
|
|
|
|
the files on disk to match a requested `TreeId`.
|
|
|
|
|
|
|
|
The `TreeState` type supports sparse checkouts. In fact, all working copies are
|
|
|
|
sparse; they simply track the full repo in most cases.
|
|
|
|
|
|
|
|
### WorkingCopy
|
|
|
|
|
|
|
|
The `WorkingCopy` type has a `TreeState` but also knows which `WorkspaceId` it
|
|
|
|
has and at which operation it was most recently updated.
|
|
|
|
|
|
|
|
### Workspace
|
|
|
|
|
|
|
|
The `Workspace` type represents the combination of a repo and a working copy (
|
|
|
|
like Git's 'worktree' concept).
|
|
|
|
|
|
|
|
The repo view at the current operation determines the desired working-copy
|
|
|
|
commit in each workspace. The `WorkingCopy` determines what is actually in the
|
|
|
|
working copy. The working copy can become stale if the working-copy commit was
|
|
|
|
changed from another workspace (or if the process updating the working copy
|
|
|
|
crashed, for example).
|
|
|
|
|
|
|
|
### Git
|
|
|
|
|
|
|
|
The `git` module contains functionality for interoperating with a Git repo, at a
|
|
|
|
higher level than the `GitBackend`. The `GitBackend` is restricted by
|
|
|
|
the `Backend` trait; the `git` module is specifically for Git-backed repos. It
|
|
|
|
has functionality for importing refs from the Git repo and for exporting to refs
|
|
|
|
in the Git repo. It also has functionality for pushing and pulling to/from Git
|
|
|
|
remotes.
|
|
|
|
|
|
|
|
### Revsets
|
|
|
|
|
|
|
|
A user-provided revset expression string goes through a few different stages to
|
|
|
|
be evaluated:
|
|
|
|
|
|
|
|
1. Parse the expression into a `RevsetExpression`, which is close to an AST
|
|
|
|
2. Resolve symbols and functions like `tags()` into specific commits. After
|
|
|
|
this stage, the expression is still a `RevsetExpression`, but it won't have
|
|
|
|
any `CommitRef` variants in it.
|
|
|
|
3. Resolve visibility. This stage resolves `visible_heads()` and `all()` and
|
|
|
|
produces a `ResolvedExpression`.
|
|
|
|
4. Evaluate the `ResolvedExpression` into a `Revset`.
|
|
|
|
|
|
|
|
This evaluation step is performed by `Index::evaluate_revset()`, allowing
|
|
|
|
the `Revset` implementation to leverage the specifics of a custom index
|
|
|
|
implementation. The first three steps are independent of the index
|
|
|
|
implementation.
|
|
|
|
|
|
|
|
### StackedTable
|
|
|
|
|
|
|
|
`StackedTable` (actually `ReadonlyTable` and `MutableTable`) is a simple disk
|
|
|
|
format for storing key-value pairs sorted by key. The keys have to have the same
|
|
|
|
size but the values can have different sizes. We use our own format because we
|
|
|
|
want [lock-free concurrency](concurrency.md) and there doesn't seem to be an
|
|
|
|
existing key-value store we could use.
|
|
|
|
|
|
|
|
The file format contains a lookup table followed by concatenated values. The
|
|
|
|
lookup table is a sorted list of keys, where each key is followed by the
|
|
|
|
associated value's offset in the concatenated values.
|
|
|
|
|
|
|
|
A table can have a parent table. When looking up a key, if it's not found in the
|
|
|
|
current table, the parent table is searched. We never update a table in place.
|
|
|
|
If the number of new entries to write is less than half the number of entries in
|
|
|
|
the parent table, we create a new table with the new entries and a pointer to
|
|
|
|
the parent. Otherwise, we copy the entries from the parent table and the new
|
|
|
|
entries into a new table with the grandparent as the parent. We do that
|
|
|
|
recursively so parent tables are at least 2 times as large as child tables. This
|
|
|
|
results in O(log N) amortized insertion time and lookup time.
|
|
|
|
|
|
|
|
There's no garbage collection of unreachable tables yet.
|
|
|
|
|
|
|
|
The tables are named by their hash. We keep a separate directory of pointers to
|
|
|
|
the current leaf tables, in the same way as we
|
|
|
|
do [for the operation log](concurrency.md#storage).
|
|
|
|
|
|
|
|
## Design of the CLI crate
|
|
|
|
|
|
|
|
### Templates
|
|
|
|
|
|
|
|
The concept is copied from Mercurial, but the syntax is different. The main
|
|
|
|
difference is that the top-level expression is a template expression, not a
|
|
|
|
string like in Mercurial. There is also no string interpolation (e.g.
|
|
|
|
`"Commit ID: {node}"` in Mercurial).
|
|
|
|
|
|
|
|
### Diff-editing
|
|
|
|
|
|
|
|
Diff-editing works by creating two very sparse working copies, containing only
|
|
|
|
the files we want the user to edit. We then let the user edit the right-hand
|
|
|
|
side of the diff. Then we simply snapshot that working copy to create the new
|
|
|
|
tree.
|