Skip to main content
Mesa versions every document with robust version-control and exposes two sets of APIs for managing versioning:
  1. High-level APIs designed to work for most use cases and simplify common versioning patterns.
  2. Low-level APIs for maximum flexibility and control. These will feel familiar for engineers who are used to working with version control systems like Git and Jujutsu.
Our low-level version control system is based on Jujutsu VCS which allows for full Git-compatibility while enabling more efficient agent-first workflows.
If you just want to get started quickly jump to Higher-level operations.

Core Primitives

Repository

A repository on Mesa is essentially just a folder that has its own version history and permissions. As the documents are modified in a repository the version history is updated and you can easily undo, redo, and modify this history as you please while never losing work.
const repo = await mesa.repo.create({ name: "project-1234" });
When working with Mesa you’ll often have many repositories. For example, an app builder that turns user prompts into apps might treat every user app as a separate repository because two users building separate apps shouldn’t affect each other’s version history or documents.

Changes

The core primitive of Mesa’s version control is the Change. Think of a change as a logical unit of work or logical modification to your repository. Each change has a unique identifier, a message/description, and can contain anywhere from zero edited files to many edited files. An example of the change history for a simple repository that scaffolds an internal dashboard and adds a forecast widget might look like this:
@  qzvqqupx   2026-03-16 10:42:31Z
│  Tune forecast widget behavior
○  puqltutt   2026-03-16 10:37:08Z
│  Add forecast widget
○  ovknlmro   2026-03-16 10:30:02Z
│  Seed dashboard layout
○  nuvyytnq   2026-03-16 10:24:19Z
│  Initialize dashboard repo
◆  root
Changes are organized into a Directed Acyclic Graph (DAG) of changes in the same way commits are in Git. This means that changes can have multiple parents and multiple children which allow for branching and merging. You can modify the change DAG with all the operations you’re familiar with: branching, merging, rebasing, reverting, cherry-picking, etc.

Bookmarks

Git is very focused on branches but JJ uses lightweight bookmarks instead. A bookmark just a lightweight pointer to a specific change in the change DAG which allows you to use reference that change more easily. Almost any of our APIs where you can reference a change you can pass in either the change id or an associated bookmark. Bookmarks are mutable and can be moved from one change to another. Every repository on mesa has a default bookmark, usually called main although this is configurable when creating a repository. If cloned from an upstream like GitHub we will use the default branch as the default bookmark.
@  qzvqqupx   2026-03-16 11:02:11Z  feature/widget
│  Polish widget interactions
│ ○  puqltutt   2026-03-16 10:58:40Z  main
├─╯  Prepare baseline release
○  ovknlmro   2026-03-16 10:30:02Z
│  Seed dashboard layout
○  nuvyytnq   2026-03-16 10:24:19Z
│  Initialize dashboard repo
◆  root
In the above version tree, we can see the main bookmark pointing to the puqltutt change and the feature/widget bookmark pointing to the qzvqqupx change. If we want to merge that new change into main we can do so with the following command:
await repo.branch.merge({ repo: repo.id, from: "feature/widget", to: "main", message: "merge feature/widget into main" });

Conflicts

Conflicts work exactly how they do in JJ. This is mostly the same as Git with the sole exception that conflicts are non-blocking. A change can be in a conflicted state and you can resolve the conflicts later or in some cases ignore them entirely. This prevents your agents from getting stuck and gives you maximum flexibility in how to handle conflicts. A conflict occurs when you’ve modified the same part of the same file in different ways on two separate branches and then try to merge them. When using simple workflows like checkpointing and linear history you’ll rarely need to worry about conflicts but if you do, we have multiple ways to resolve them. See dealing with conflicts for more details.

High-level Operations

We’ve created two higher-level concepts that are built on top of the core primitives to better facilitate the most common workflows. You can at any time switch out the usage of these operations for equivalent lower-level operations. The key limitation of these higher-level operations is that they must be performed on a bookmarked change, while lower level version control operations can be performed on any change.

Checkpoints

Checkpoints are designed for easy linear versioning and capture a point in time for you repository. You can create checkpoints as frequently or infrequently as you’d like and once created it’s easy to undo or revert to a previous checkpoint.
// ... modify some files...
const checkpoint = await repo.checkpoint.new({ repo: repo.id, message: "checkpoint 1" });
// ... more changes ...

// Revert to old checkpoint
await repo.revertToCheckpoint(checkpoint.id);
A common pattern for app builders is to checkpoint every time a user passes in a new message to the agent. You can then allow users to easily undo their latest chat messages and the associated changes.
Under the hood, a checkpoint is just a new change on top of an existing bookmarked change. The bookmark is automatically moved to the new checkpoint. In jj terms this is jj describe && jj new && jj bookmark set

Branches

Branches allow you to perform changes to your repository in isolation and also act as an approval mechanism for changes that are experimental or need user approval. For example, if your app builder allows users to vibe-code dashboards, they might start a new chat designed to add KPI widgets. You can create a branch specifically for this chat:
// Default branch for repos is "main" but you can change this when creating a repo
const repo = await mesa.repo.create({ name: "project-1234", defaultBranch: "main" });

const branch = await repo.branch.create({ repo: repo.id, name: "feature/add-kpi-widgets" });

// ... do some work ...
await repo.checkpoint.new({ repo: repo.id, branch: branch.id, message: "coding plan"})
// ... some more agent work
await repo.checkpoint.new({ repo: repo.id, branch: branch.id, message: "implementation"})
// ... more work ...
await repo.checkpoint.new({ repo: repo.id, branch: branch.id, message: "agent reviewed and approved"})
Now all the work to add widgets has been saved and versioned but it’s still not on the main branch. You might want to show the user the changes in your UI in a way that makes sense for your app and then when they approve the changes you can merge the branch to main.
await repo.branch.merge({ repo: repo.id, from: branch.id, to: "main", message: "merge feature/add-kpi-widgets into main" });
Mesa branches just provide Git-style branch semantics over changes and bookmarks. This gives us an easy concept for different merge strategies and approval gates.

Dealing with conflicts

A conflict happens when two branches start from the same base change and edit the same location differently Example: two separate branches edit overview.json and change the title property: Before any merge Both Chat A and Chat B have edited overview.json in the same place.
@  qzvqqupx   2026-03-16 11:12:04Z  feature/chat-a
│  title = "Revenue + Forecast"
│ ○  puqltutt   2026-03-16 11:10:27Z  feature/chat-b
├─╯  title = "Revenue Q4 Summary"
○  ovknlmro   2026-03-16 10:58:40Z  main
│  title = "Revenue Overview"
◆  root
Chat B merges first Chat B merges first and creates a new change on top of the main bookmark.
@  qzvqqupx   2026-03-16 11:12:04Z  feature/chat-a
│  title = "Revenue + Forecast"
○  puqltutt   2026-03-16 11:10:27Z  main
│  title = "Revenue Q4 Summary"
○  ovknlmro   2026-03-16 10:58:40Z
│  title = "Revenue Overview"
◆  root
Now, if we wanted to merge feature/chat-a into main Mesa will return an error because the new merge commit would be conflicted. Let’s say we want to merge feature/chat-a into main. We can deal with the conflict as follows: 1. Accept the conflict Conflicts are non-blocking in Mesa but we cannot sync conflicts to git upstreams like GitHub so we do block by default when there’s a conflict when merging into main. You can override this behavior by passing blockOnConflict: false to the merge operation.
await repo.branch.merge({ 
  repo: repo.id, 
  from: "feature/chat-a", 
  to: "main", 
  message: "merge feature/chat-a into main", 
  blockOnConflict: false 
});
Which puts the main branch into a conflicted state
x  zxoosnnp   2026-03-16 11:13:52Z  main
│  (conflict) merge feature/chat-a into main
├─╮
│ ○  qzvqqupx   2026-03-16 11:12:04Z  feature/chat-a
│ │  title = "Revenue + Forecast"
○ │  puqltutt   2026-03-16 11:10:27Z
│ │  title = "Revenue Q4 Summary"
├─╯
○  ovknlmro   2026-03-16 10:58:40Z
│  title = "Revenue Overview"
◆  root
feature/chat-a bookmark still points to qzvqqupx (branched from original main change ovknlmro), while the conflicted merge change zxoosnnp combines both parents (qzvqqupx and puqltutt).
When merging two changes that aren’t on the main branch we do not block on conflicts.
2. Refresh and resolve conflict Refreshing a branch simply rebases it on the latest main bookmark which lets you resolve conflicts in the feature branch before merging to main.
await repo.branch.refresh({ repo: repo.id, branch: "feature/chat-a"})
This puts the feature branch into a conflicted state:
  x  qzvqqupx   2026-03-16 11:12:04Z  feature/chat-a
├─╯  (conflict) title = "Revenue + Forecast"
○   puqltutt   2026-03-16 11:10:27Z
│   title = "Revenue Q4 Summary"
○  ovknlmro   2026-03-16 10:58:40Z
│  title = "Revenue Overview"
◆  root
Now you can view the conflict details and resolve them as you see fit.
const conflicts = await repo.change.conflicts.list({ repo: repo.id, change: "qzvqqupx" });
console.log(conflictSet);
/* {
  change: "zxoosnnp",
  status: "conflicted",
  conflicts: [
    {
      path: "overview.json",
      target: { branch: "main",           oid: "qzvqqupx" },
      source: { branch: "feature/chat-a", oid: "puqltutt" },
    },
  ]
} */
// Resolve by file
await repo.change.conflicts.resolve({
  repo: repo.id,
  change: "qzvqqupx",
  message: "Resolve merge conflicts from feature/chat-a",
  resolutions: [
    { path: "overview.json", action: "take_source" },
  ],
});
Now your Chat A changes are clean and you can merge into main.
We will be adding more granular, line and hunk-based conflict resolution as well as powerful APIs for agents to resolve conflicts automatically

Comparing to Git and JJ

Mesa is Jujutsu-based while retaining full Git compatibility. You can essentially treat changes like commits and bookmarks like branches but there are some key differences:
  • There’s no staging area like in Git. You are always editing an existing change and changes can evolve as you edit them. When you create a new change in Mesa it is initially empty and then you can edit and modify files at that change. This is ideal for agents running in sandboxes because any edits they make are automatically persisted to a very specific place in the version tree rather than being saved in some dangling staging area.
  • Bookmarks are like branches in git but lighter weight. They do not automatically move from one change to another but can be moved manually. The consequence of this is that you do not need to explicitly think about branching ahead of time. If you have a main bookmark, you can just create a new change from it and that’s effectively an unnamed branch. You can name it later or leave it unnamed and create new changes on top of it or just move the main bookmark to point at the new change.
Here’s a handy table to explain how Mesa version concepts map to Git and Jujutsu.
Mesa ConceptGitJJ
RepoRepoRepo
ChangeCommitChange
Bookmark~BranchBookmark
We’ve introduced two higher-level concepts that don’t exist in JJ to acommodate common workflows:
  • Branches
  • Checkpoints
These are simply composite operations of the underlying version control system.
Mesa OperationGitJJ
Create Branchgit checkout -b <branch-name>jj new && jj bookmark create <bookmark-name>
Delete Branchgit branch -d <branch-name>jj abandon 'main..<bookmark-name>'
Merge Branchgit merge <branch-name>jj new main <bookmark-name>
Create Checkpointgit add . && git commit -m "<message>"jj describe -m "<message>" && jj new && jj bookmark set <bookmark-name>
Revert to Checkpointgit reset --hard <checkpoint-id>jj abandon '<checkpoint-id>..@'

Syncing to GitHub & GitLab

Mesa can sync to arbitrary git upstreams including GitHub and GitLab. This is useful if you want to create a Mesa repository based on a template repo that lives in GitHub or if you want to sync changes between GitHub and Mesa. We will add more instructions here soon.

Next steps