Skip to content

Commit 22c331c

Browse files
authored
fix(reset): support git-style bare path reset (#431)
Signed-off-by: Quanyi Ma <eli@patch.sh>
1 parent 3dac82a commit 22c331c

11 files changed

Lines changed: 336 additions & 48 deletions

File tree

COMPATIBILITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ batch document.
7171
| merge | partial | fast-forward and single-head three-way merge supported; `-m <msg>`, `--ff-only`, `--no-ff`, `--squash`, `--no-commit`, `--no-edit` (accepted no-op; Libra never opens an editor for merge), `--stat`/`-n`/`--no-stat` (last-wins toggle; `--stat` prints a post-merge diffstat of the merge's changes, the default is no diffstat), `--no-progress` (accepted no-op; Libra's merge renders no progress meter), `--verify-signatures` (verify the merged tip's PGP signature against the local vault key — like `tag -v`, only signatures made by this repository's vault key can be validated; no external keyring) / `--no-verify-signatures` (the default; toggle pair, last wins), `--no-rerere-autoupdate` (accepted no-op — never auto-stages replayed resolutions; rerere IS auto-integrated when `rerere.enabled` is set, with staging following `rerere.autoUpdate` since merge does not expose the positive `--rerere-autoupdate`), and `--no-gpg-sign` (accepted no-op; Libra's merge never signs the merge commit) supported; the `merge.conflictStyle` config (`merge` default / `diff3` adds the `||||||| base` ancestor block to line-level text conflicts; honored by merge and cherry-pick; an unsupported value such as `zdiff3` is a hard error when a conflict must be rendered, exit 128) supported; Libra extensions: `--dry-run` (preview the outcome writing nothing — no HEAD/index/worktree/state/object write; exit 0 clean, exit 1 would-conflict with `would_conflict`+`conflicted_paths` in `--json`) and `--restart` (abort the in-progress conflicted merge — discarding resolution work like `--abort` — then re-run the same merge against the recorded target commit; original merge options not replayed); octopus/custom strategies, `--rerere-autoupdate`, and `-S`/`--gpg-sign` (signing the merge commit) deferred; `--autostash`/`--no-autostash` + `merge.autostash` config (lore.md 1.8, Git-faithful): a dirty tree (tracked changes incl. staged) is stashed into a HELD commit before the merge (not in `stash list` — the MERGE_AUTOSTASH model, sidecar `merge-autostash.json`), re-applied when the merge concludes (clean success, up-to-date, squash, start failure, `--continue`, `--abort`); a conflicted merge HOLDS it (also across `--restart` cycles); a conflicting re-apply promotes it into the stash list with a notice — changes are never lost; invalid `merge.autostash` is a hard error; JSON gains an additive `autostash: applied|stashed|kept` field lore.md 2.6: participates in the symmetric cross-op sequencer mutex — starting a `merge` is refused (`LBR-CONFLICT-002`) while any OTHER sequence is unresolved (and vice-versa). |
7272
| merge-base | partial | Prints the best common ancestor of two commits — a true LCA (a common ancestor that is not a strict ancestor of another), shared with `diff A...B` via `internal/merge_base.rs`; `--all` prints every lowest common ancestor, `--is-ancestor` tests ancestry (exit 0/1), `--json`/`--machine` supported. Exit 0 (base found / ancestry holds) / 1 (no common ancestor / not an ancestor; no output, matching Git) / 128 (unresolvable commit or wrong arg count). More than two commits, `--octopus`/`--independent`/`--fork-point` not exposed. `rebase` now computes its merge base through this shared LCA (no longer a first-found walk), and `log A...B` excludes the reachable-set intersection (correct for multiple merge bases) — the consolidation is complete |
7373
| merge-file | partial | File-level three-way merge of `<current> <base> <other>` reusing the same `diffy` merge as `merge` (markers labelled `ours`/`theirs`, `||||||| original` with `--diff3`); `-p`/`--stdout`, `--diff3`, `-q`/`--quiet`, `--json`/`--machine` supported; works outside a repository. Exit 0 (clean) / 1 (conflict, fixed at 1) / 128 (missing/unreadable/binary input). In-place writes back up the original under `.libra/merge-file-backup/` (kept on conflict). `-L <label>`, `--ours`/`--theirs`/`--union`, and `--marker-size` not exposed (deferred) |
74-
| reset | partial | `--soft`/`--mixed`/`--hard` and pathspec un-staging supported, with index-rollback on failure. `--pathspec-from-file`/`--pathspec-file-nul` supported for bulk/stdin pathspec input, but paths are taken literally — Git's default-mode C-style quoted-path decoding is intentionally not performed (use `--pathspec-file-nul` for special characters). `--no-refresh` is accepted as a no-op (Libra's reset never refreshes the index, so there is no refresh to skip; no `--refresh`). `--merge`/`--keep` remain unsupported (see [docs/commands/reset.md](docs/commands/reset.md) "Why no --merge/--keep?") |
74+
| reset | partial | `--soft`/`--mixed`/`--hard` and pathspec un-staging supported, with index-rollback on failure. Bare pathspecs follow Git disambiguation: `libra reset <path>` unstages from `HEAD`, `libra reset -- <path>` forces path interpretation for revision-like filenames, and a token that is both revision and filename is rejected as ambiguous (`LBR-CLI-002`). `--pathspec-from-file`/`--pathspec-file-nul` supported for bulk/stdin pathspec input, but paths are taken literally — Git's default-mode C-style quoted-path decoding is intentionally not performed (use `--pathspec-file-nul` for special characters). `--no-refresh` is accepted as a no-op (Libra's reset never refreshes the index, so there is no refresh to skip; no `--refresh`). `--merge`/`--keep` remain unsupported (see [docs/commands/reset.md](docs/commands/reset.md) "Why no --merge/--keep?") |
7575
| rev-parse | partial | basic revision parsing, `--verify`, `--short[=<n>]`, `--abbrev-ref`, `--symbolic-full-name` (resolve a spec to its full ref name — `refs/heads/…`/`refs/tags/…`/`refs/remotes/…`, or `HEAD` when detached; a valid non-ref object prints nothing, an unresolvable name exits 128 — Libra reports it on stderr rather than echoing the spec to stdout), `--symbolic` (echo a resolvable ref/revision/object-id spec verbatim — `main` stays `main`, not `refs/heads/main`; unresolvable names exit 128 like `--symbolic-full-name`), `--show-toplevel`, `--show-prefix`, `--show-cdup`, `--is-inside-work-tree`, `--is-inside-git-dir`, `--is-bare-repository`, `--git-dir`, `--absolute-git-dir`, `--sq` (shell-quote the resolved object name), the output-filter modes `--flags`/`--no-flags`/`--revs-only`/`--no-revs` (classify each arg as flag/revision/path and print a filtered subset), and multiple `<SPEC>` arguments (each resolved on its own line) supported; a `--` separator splits revisions from paths in all of these. **Intentional divergence:** combining `--verify`/`--short` (single-revision modes) with any output-filter flag is rejected with a usage error (LBR-CLI-002 / exit 129) — Git's behavior in that corner is ill-defined. Remaining output-filter (`--abbrev=<n>`) and parseopt modes are incomplete |
7676
| rev-list | partial | multi-revision reachability, `^` exclusions, `A..B`/`A...B` ranges, `--count`, `-n`/`--max-count`, `--skip`, `--since`/`--after`, `--until`/`--before`, parent-count filters and reset aliases, `--first-parent`, `--author`, `--committer`, `--grep`, path limitation after `--`, symmetric side filters (`--left-right`, `--left-only`, `--right-only`), cherry filters (`--cherry`, `--cherry-pick`, `--cherry-mark`), `--parents`, `--children`, `--timestamp`, and `--reverse` (limit-then-reverse output ordering), `--all` (seed the walk from every branch/remote/tag ref and HEAD), `--date-order` (no-op for Libra's default committer-date ordering; unlike Git, no topo constraint under date skew), and `--boundary` (append the frontier commits — parents of listed commits that are not themselves listed, whether excluded by a range or beyond a `--max-count`/`--skip` cut — each prefixed with `-` and carrying `--parents`/`--children`/`--timestamp` metadata, with Git-faithful merge handling under `--first-parent --parents` and `--children`, leading under `--reverse`, and counted by `--count`), and object-enumeration output (`--objects`, plus `--objects-edge`/`--objects-edge-aggressive` which also emit `-`-prefixed edge commits — the latter an alias, a documented narrowing) supported — objects are the deduplicated reachable trees/blobs printed after the commits as `<oid> <path>` (root tree empty path), byte-for-byte matching `git rev-list --objects`, with excluded-side objects treated as uninteresting (range/`^` closure), `-- <pathspec>` walk pruning, gitlinks skipped, and a hard error on a corrupt included tree |
7777
| describe | partial | basic describe, `--tags`, `--always`, `--abbrev`, `--exact-match`, `--long`, `--dirty[=<mark>]`, `--first-parent`, `--match`/`--exclude` (wax globs, ≤256 chars; exclude wins over match), `--candidates <n>` (n=0 ⇒ exact-match), `--all` (use any ref — branches/remotes/tags — with `heads/`/`remotes/`/`tags/` prefixes), and `--contains` (git name-rev: name a commit relative to the nearest descendant tag — `<tag>`, `<tag>~<n>`, or `<tag>~<n>^<m>~<k>`) supported |

docs/commands/reset.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Move `HEAD` and reset the index or working tree depending on the selected mode.
66

77
```
88
libra reset [<target>] [--soft | --mixed | --hard]
9+
libra reset <pathspec>...
910
libra reset [<target>] [--] <pathspec>...
1011
libra reset [<target>] --pathspec-from-file=<file> [--pathspec-file-nul]
1112
```
@@ -18,7 +19,7 @@ libra reset [<target>] --pathspec-from-file=<file> [--pathspec-file-nul]
1819
- **`--mixed`** (default): moves HEAD and resets the index. The working tree is untouched, so changes appear as unstaged modifications. Useful for un-staging files.
1920
- **`--hard`**: moves HEAD, resets the index, and restores the working tree. All uncommitted changes are discarded. Useful for fully reverting to a known state.
2021

21-
When pathspecs are provided, the command performs a targeted mixed reset: only the named files are reset in the index to match the target commit, without moving HEAD. This is the primary way to un-stage specific files. Pathspecs are incompatible with `--soft` and `--hard`.
22+
When pathspecs are provided, the command performs a targeted mixed reset: only the named files are reset in the index to match the target commit, without moving HEAD. This is the primary way to un-stage specific files. Like Git, a bare first positional that is a known path and not a revision is treated as a pathspec with target `HEAD`, so `libra reset src/lib.rs` is equivalent to `libra reset HEAD -- src/lib.rs`. If a token is both a revision and a filename, reset refuses it as ambiguous; use `libra reset <revision> -- <file>` for a target revision or `libra reset -- <file>` for a path. Pathspecs are incompatible with `--soft` and `--hard`.
2223

2324
The default target is `HEAD`, making `libra reset` (with no arguments) equivalent to un-staging everything.
2425

@@ -30,7 +31,7 @@ The default target is `HEAD`, making `libra reset` (with no arguments) equivalen
3031
| | `--soft` | | Move HEAD only; keep index and working tree |
3132
| | `--mixed` | | Move HEAD and reset index; keep working tree (default) |
3233
| | `--hard` | | Move HEAD, reset index, and restore working tree |
33-
| | `<pathspec>...` | positional (after `--`) | Specific files to reset in the index |
34+
| | `<pathspec>...` | positional, optionally after `--` | Specific files to reset in the index |
3435
| | `--pathspec-from-file` | `<file>` | Read pathspecs from a file (`-` for stdin) instead of the command line. Mutually exclusive with command-line pathspecs |
3536
| | `--pathspec-file-nul` | | Treat `--pathspec-from-file` input as NUL-separated rather than line-separated. No-op without `--pathspec-from-file` |
3637
| | `--no-refresh` | | Accepted for Git compatibility; a no-op in Libra (see below) |
@@ -63,10 +64,16 @@ libra reset HEAD~2
6364
libra reset --hard main
6465

6566
# Un-stage a specific file
67+
libra reset src/lib.rs
68+
69+
# Un-stage a revision-like filename
70+
libra reset -- HEAD
71+
72+
# Un-stage a specific file from an explicit target
6673
libra reset HEAD -- src/lib.rs
6774

6875
# Un-stage multiple files
69-
libra reset HEAD -- src/main.rs src/cli.rs
76+
libra reset src/main.rs src/cli.rs
7077

7178
# Reset specific files to a prior commit
7279
libra reset abc1234 -- path/to/file.rs
@@ -87,6 +94,7 @@ libra reset --json --hard HEAD~1
8794
libra reset HEAD~1 # Move HEAD and reset index to the previous commit
8895
libra reset --soft HEAD~2 # Move HEAD only, keep index and worktree
8996
libra reset --hard main # Reset HEAD, index, and worktree to branch 'main'
97+
libra reset src/lib.rs # Unstage a path back to HEAD
9098
libra reset HEAD -- src/lib.rs # Unstage a path back to HEAD
9199
libra reset --pathspec-from-file=paths.txt # Unstage paths read from a file ('-' for stdin)
92100
libra reset --json --hard HEAD~1 # Structured JSON output for agents
@@ -159,7 +167,7 @@ Pathspec reset:
159167

160168
### Why reject pathspecs with --soft/--hard?
161169

162-
- **`--soft` + pathspecs**: `--soft` by definition only moves HEAD and touches nothing else. Resetting individual file index entries contradicts the "HEAD only" contract. If you want to un-stage specific files, use the default mixed mode: `libra reset HEAD -- file`.
170+
- **`--soft` + pathspecs**: `--soft` by definition only moves HEAD and touches nothing else. Resetting individual file index entries contradicts the "HEAD only" contract. If you want to un-stage specific files, use the default mixed mode: `libra reset file` or `libra reset HEAD -- file`.
163171
- **`--hard` + pathspecs**: `--hard` restores the entire working tree to match the target commit. Selectively restoring only some files while leaving others in a different state would create a confusing hybrid that is neither "fully reset" nor "index-only reset." For selective file restoration, use `libra restore --source <commit> -- file`.
164172

165173
This restriction makes the three modes unambiguous: soft touches HEAD, mixed touches HEAD + index, hard touches HEAD + index + worktree. Pathspecs operate orthogonally at the index level only.
@@ -185,15 +193,15 @@ Libra favors explicit, composable commands over implicit multi-step operations h
185193
| Mixed reset (default) | `git reset <target>` | `libra reset <target>` | N/A (jj has no staging area) |
186194
| Soft reset | `git reset --soft <target>` | `libra reset --soft <target>` | N/A |
187195
| Hard reset | `git reset --hard <target>` | `libra reset --hard <target>` | `jj restore --from <rev>` |
188-
| Un-stage files | `git reset HEAD -- <file>` | `libra reset HEAD -- <file>` | N/A (no staging area) |
196+
| Un-stage files | `git reset <file>` / `git reset HEAD -- <file>` | `libra reset <file>` / `libra reset HEAD -- <file>` | N/A (no staging area) |
189197
| Merge reset | `git reset --merge <target>` | Not supported | N/A |
190198
| Keep reset | `git reset --keep <target>` | Not supported | N/A |
191199
| Pathspec from file | `git reset --pathspec-from-file=<f>` | `libra reset --pathspec-from-file=<f>` (literal paths; no C-style quote decoding) | N/A |
192200
| Pathspec file NUL | `git reset --pathspec-file-nul` | `libra reset --pathspec-file-nul` | N/A |
193201
| Index refresh control | `git reset --[no-]refresh` | `--no-refresh` accepted as a no-op; no `--refresh` | N/A |
194202
| Default target | HEAD | HEAD | N/A |
195203
| Structured output | No | `--json` / `--machine` | `--template` |
196-
| Pathspec + soft | Allowed (un-stages) | Rejected (`LBR-CLI-002`) | N/A |
204+
| Pathspec + soft | Rejected | Rejected (`LBR-CLI-002`) | N/A |
197205
| Pathspec + hard | Rejected | Rejected (`LBR-CLI-002`) | N/A |
198206
| Pathspec from file + CLI pathspec | Rejected | Rejected (`LBR-CLI-002`) | N/A |
199207
| Rollback on failure | No | Attempts index rollback | N/A (operation log undo) |
@@ -204,6 +212,7 @@ Libra favors explicit, composable commands over implicit multi-step operations h
204212
|----------|-----------|------|
205213
| Not a libra repository | `LBR-REPO-001` | "run 'libra init' to create a repository in the current directory." |
206214
| Invalid revision | `LBR-CLI-003` | "check the revision name and try again." |
215+
| Ambiguous revision/path token | `LBR-CLI-002` | "use '--' to separate paths from revisions, like 'libra reset <revision> -- <file>' or 'libra reset -- <file>'." |
207216
| HEAD is unborn | `LBR-REPO-003` | "create a commit first before resetting HEAD." |
208217
| Failed to resolve HEAD | `LBR-IO-001` | "check whether the repository database is readable." |
209218
| HEAD reference corrupt | `LBR-REPO-002` | "the HEAD reference or branch metadata may be corrupted." |

0 commit comments

Comments
 (0)