Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 227 additions & 54 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "scorpiofs"
version = "0.3.0"
version = "0.3.1"
edition = "2021"
# `cargo run` (no --bin) runs the main `scorpio` binary; `antares` is the alias.
default-run = "scorpio"
Expand All @@ -25,7 +25,7 @@ path = "src/main.rs"


[dependencies]
git-internal = "0.4.1"
git-internal = "0.8.5"
reqwest = { version = "0.13.1", features = ["json", "blocking"] }
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.49.0", features = ["full"] }
Expand Down
36 changes: 36 additions & 0 deletions docs/antares.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,42 @@ Antares 路由可通过两种方式暴露,**路径前缀不同**:

---

### 4.3. List changed paths

**Endpoint**: `GET /mounts/{mount_id}/changes` (primary process:
`GET /antares/mounts/{mount_id}/changes`)

**Description**: Returns the paths represented by the optional CL layer and
the private writable upper layer. The service does not walk the remote Dicfuse
base. Upper-layer entries override CL entries for the same path, character
device whiteouts are reported as `deleted`, and other entries are reported as
`modified`. The `.libra` linked-worktree metadata path is excluded.

**Response** (200 OK):

```json
{
"mount_id": "550e8400-e29b-41d4-a716-446655440000",
"generation": 14695981039346656037,
"changes": [
{
"kind": "modified",
"path": "src/lib.rs"
},
{
"kind": "deleted",
"path": "src/obsolete.rs"
}
]
}
```

`generation` is a stable fingerprint of the sorted changed-path set. Clients
must still compare candidate files against their own index and object model;
this endpoint reports overlay candidates, not Git status.

---

### 5. 删除挂载

**端点**: `DELETE /mounts/{mount_id}`
Expand Down
127 changes: 127 additions & 0 deletions docs/worktree-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Antares worktree control API

This API is the ScorpioFS half of a Libra-managed worktree. It exposes mount
lifecycle, writable-upper state, and safe base-switch preflight. It does not
implement Git refs, commits, index updates, merge, rebase, or credentials.

## Capability discovery

Check `/health` before using worktree operations. A compatible daemon reports:

```text
mount.v1
ready.v1
changes.v1
worktree-base.v1
refresh-plan.v1
```

Clients should negotiate capabilities instead of assuming every ScorpioFS
daemon implements the worktree API.

## Ownership contract

Libra owns HEAD, index, refs, object storage, commit construction, remote
transport, and conflict stages. ScorpioFS owns the mount, the Dicfuse lower
projection, and the writable Antares upper layer.

The mount must contain only a reconstructable `.libra` pointer. Do not create
or persist `.libra` refs or index files inside an Antares upper directory.

## Attach a Libra worktree

1. Libra resolves the commit/tree it intends to expose.
2. Create an Antares mount without a `cl` field.
3. Wait for the mount to become ready.
4. Bind the resolved revision before allowing a process to write to the mount.

```bash
curl -X POST http://127.0.0.1:2725/antares/mounts \
-H 'Content-Type: application/json' \
-d '{"job_id":"libra-dev-42","path":"/project/aardvark-dns"}'

curl http://127.0.0.1:2725/antares/mounts/<mount-id>/ready

curl -X POST http://127.0.0.1:2725/antares/mounts/<mount-id>/worktree/base \
-H 'Content-Type: application/json' \
-d '{"base_revision":"<resolved-commit-oid>"}'
```

Binding is rejected for a mount with a CL layer, a dirty upper layer, or an
existing different base binding. Repeating the same binding is idempotent.

## Read worktree state

```bash
curl http://127.0.0.1:2725/antares/mounts/<mount-id>/worktree
```

Example response:

```json
{
"mount_id": "...",
"path": "/project/aardvark-dns",
"base_revision": "abc123",
"mount_state": "ready",
"dirty": true,
"changes": {
"mount_id": "...",
"generation": 123456,
"changes": [
{ "kind": "modified", "path": "src/lib.rs" }
]
}
}
```

`changes` contains private upper-layer edits only. A CL layer is a build
baseline, not an unstaged Git worktree edit. Libra may use `generation` to
avoid reprocessing an unchanged path set, but it remains responsible for blob
hashing when updating its index.

## Plan a refresh

`refresh-plan` is a guard, not a mutation. Libra resolves target refs and calls
it before a later checkout, fast-forward, merge, or rebase operation.

```bash
curl -X POST \
http://127.0.0.1:2725/antares/mounts/<mount-id>/worktree/refresh-plan \
-H 'Content-Type: application/json' \
-d '{
"expected_base_revision":"abc123",
"target_revision":"def456",
"require_clean":true
}'
```

The disposition is one of:

| Value | Meaning | Libra action |
| --- | --- | --- |
| `ready` | Bound base matches and the requested cleanliness rule passes | Proceed to a future transactional base switch |
| `already_at_target` | Target already matches the bound base | No worktree action |
| `unbound` | No Libra base has been registered | Bind a base first |
| `base_mismatch` | Caller has stale worktree metadata | Re-read state and resolve refs again |
| `blocked_dirty` | Upper layer has local edits | Reject, stash, merge, or rebase in Libra |

## Git operation mapping

| Libra operation | ScorpioFS action |
| --- | --- |
| `libra fetch origin` | None; only remote refs change |
| `libra status` | Read `/worktree`, then compare candidate paths with Libra index |
| `libra add` | None; Libra updates its host-local index |
| clean `pull --ff-only` | Call `refresh-plan`, then a future base switch/remount |
| dirty pull or checkout | `blocked_dirty`; Libra owns stash/merge/rebase |
| `libra commit` | Future transaction: switch lower to committed tree, then clear committed upper paths |
| `libra push` | None; pushing does not mutate the active mount |

## Current lower-snapshot boundary

`base_revision` records the revision selected by Libra. An actual Dicfuse lower
switch is deliberately not implemented until Mega exposes revision-addressable
tree, hash, and blob APIs. A successful refresh plan therefore proves only
that the worktree is safe to switch; it does not claim that the FUSE lower tree
has already moved to the target revision.
145 changes: 145 additions & 0 deletions docs/worktree-state-transitions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# ScorpioFS worktree state transitions

## Scope and ownership

ScorpioFS is a projection and FUSE ownership layer. Libra owns Git-compatible
objects, refs, HEAD, index, commits, transport, and credentials. ScorpioFS
must never persist `.libra` state in an Antares upper layer.

The mapping is deliberately narrow:

| Version-control concept | Owner | ScorpioFS representation |
| --- | --- | --- |
| `HEAD` commit/tree | Libra | `base_revision` bound to an Antares mount |
| index | Libra | not materialized by ScorpioFS |
| working tree | both | immutable Dicfuse lower plus Antares upper |
| unstaged changes | ScorpioFS | changed paths in the private upper layer |
| remote-tracking refs | Libra | not a filesystem update |

An optional Antares CL layer is a build-view input. It is not a Libra index and
cannot be used with a Libra worktree-base binding.

## Invariants

1. A mounted worktree has at most one immutable `base_revision`.
2. `libra fetch` only updates remote refs and never changes Dicfuse lower files.
3. ScorpioFS never changes a mounted lower tree merely because a remote branch moved.
4. A dirty upper layer blocks a safe fast-forward base switch by default.
5. A base switch is coordinated by Libra and must be an explicit remount/generation
change; the current `refresh-plan` API is intentionally non-mutating.
6. Open handles continue to observe their existing generation until the caller
quiesces the worktree and performs the switch.

## Mount states

```text
Provisioning -> Mounted -> Ready
-> Quiescing -> Refreshing -> Ready
-> Quiescing -> Conflict
-> Unmounting -> Unmounted
```

`Refreshing` and `Conflict` are target states for the future revision-aware
Dicfuse switch. Existing implementations use `Ready` plus the non-mutating
refresh plan and do not claim to have switched the lower tree.

## Git-compatible transitions

### Attach

1. Libra resolves the exact commit/tree to check out.
2. Libra creates the Antares mount.
3. Before a client writes to the mount, Libra calls
`POST /mounts/{id}/worktree/base` with that immutable revision.
4. ScorpioFS rejects the binding if the mount has a CL layer or an upper delta.

### Fetch

`libra fetch origin` updates `refs/remotes/origin/*` only. It must not invoke a
Dicfuse refresh, invalidate an active worktree, or change `base_revision`.
The status command can report ahead/behind using Libra refs while the visible
filesystem remains pinned.

### Status

Libra computes staged changes from `HEAD -> index`. It obtains unstaged and
untracked candidates from `GET /mounts/{id}/worktree`, then compares only those
paths against its index. This avoids recursively walking a monorepo mount.

### Clean fast-forward

1. Libra resolves target commit `T`.
2. Libra calls `POST /mounts/{id}/worktree/refresh-plan` with the currently
bound revision and `T`.
3. A `ready` plan permits a future transactional base switch.
4. Libra quiesces users, switches Dicfuse to the tree for `T`, remounts, then
updates HEAD and index atomically.

### Dirty worktree

The default plan returns `blocked_dirty`. Libra must preserve the upper layer
and either reject the operation with Git-style overwrite protection, create a
stash, or run a later three-way merge/rebase implementation. ScorpioFS must
not silently overwrite upper files.

### Commit

After Libra creates commit `C`, it must atomically switch the lower base to
`C` and remove only upper entries represented by the committed tree. If that
switch fails, HEAD/index/upper remain unchanged so no local edit is lost.

## Refresh-plan API

```http
GET /mounts/{id}/worktree
POST /mounts/{id}/worktree/base
POST /mounts/{id}/worktree/refresh-plan
```

Base binding:

```json
{ "base_revision": "commit-or-tree-oid" }
```

Refresh preflight:

```json
{
"expected_base_revision": "current-oid",
"target_revision": "target-oid",
"require_clean": true
}
```

The response is one of `ready`, `already_at_target`, `unbound`,
`base_mismatch`, or `blocked_dirty`. It is not an update command.

## Required server capability for actual switching

Mega must expose revision-addressable tree and blob reads, for example:

```text
GET /api/v1/tree?path=<path>&revision=<commit>
GET /api/v1/tree/content-hash?path=<path>&revision=<commit>
GET /api/v1/blob/<oid>
```

Until that exists, a `base_revision` is control-plane provenance only. It must
not be advertised as proof that Dicfuse is reading an immutable historical tree.

## Future conflict model

For merge/rebase, Libra owns the three-way comparison and index stages:

```text
BASE = merge base
OURS = HEAD/index/upper
THEIRS = target revision
```

ScorpioFS should later record the base OID at first copy-up/delete, expose it
with each changed path, and preserve upper files. Libra then creates normal
Git conflict stages and decides whether to materialize conflict files. The FUSE
layer must not write conflict markers by default because build tools may read
them as ordinary source.
Loading