Skip to content
Merged
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
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# gitsift

Git hunk sifter for code agents. A selective staging tool (`git add -p` replacement) designed for CLI agents like Claude Code and Codex.
Git hunk sifter for code agents. A selective staging and checkout tool (`git add -p` / `git checkout -p` replacement) designed for CLI agents like Claude Code and Codex.

## Features

- **Hunk-level staging** — stage entire hunks by ID
- **Line-level staging** — stage individual lines within a hunk via patch reconstruction
- **Hunk-level checkout** — discard unstaged or staged changes per hunk
- **Staged diff** — view staged changes (HEAD vs index) with `diff --staged`
- **Compact output** — token-efficient default format (~40% smaller than JSON), inspired by [TOON](https://toonformat.dev/)
- **JSON output** — full structured diff output with file/hunk/line metadata
- **JSON-lines protocol** — persistent stdin/stdout mode for agent sessions
Expand Down Expand Up @@ -51,12 +53,24 @@ gitsift diff --format json
# Filter by file
gitsift diff --file src/main.rs

# List staged changes (HEAD vs index)
gitsift diff --staged

# Stage hunks by ID
gitsift stage --hunk-ids abc123,def456

# Stage via JSON on stdin (supports line-level selections)
echo '{"hunk_ids": ["abc123"]}' | gitsift stage --from-stdin

# Discard unstaged hunks (revert workdir → index)
gitsift checkout --hunk-ids abc123,def456

# Discard staged hunks (revert index → HEAD)
gitsift checkout --staged --hunk-ids abc123

# Discard via JSON on stdin
echo '{"hunk_ids": ["abc123"]}' | gitsift checkout --from-stdin

# Show staging status
gitsift status
```
Expand All @@ -73,7 +87,10 @@ Send JSON requests on stdin, receive JSON responses on stdout:

```json
{"method": "diff", "params": {"file": "src/main.rs"}}
{"method": "diff", "params": {"staged": true}}
{"method": "stage", "params": {"hunk_ids": ["abc123"]}}
{"method": "checkout", "params": {"hunk_ids": ["abc123"]}}
{"method": "checkout", "params": {"hunk_ids": ["abc123"], "staged": true}}
{"method": "status"}
```

Expand Down Expand Up @@ -111,13 +128,20 @@ files[1]:

## Agent integration

gitsift is designed for the following workflow:
gitsift is designed for the following workflows:

1. Agent calls `gitsift diff` to inspect available changes
**Selective staging:**
1. Agent calls `gitsift diff` to inspect unstaged changes
2. Agent selects hunks/lines to stage based on the structured output
3. Agent calls `gitsift stage --hunk-ids <ids>` or pipes a `StageRequest` via `--from-stdin`
4. Agent calls `gitsift status` to verify staging result

**Selective discard:**
1. Agent calls `gitsift diff` (or `gitsift diff --staged`) to inspect changes
2. Agent selects hunks to discard
3. Agent calls `gitsift checkout --hunk-ids <ids>` (or `--staged` for staged changes)
4. Agent calls `gitsift diff` to verify the changes were discarded

For persistent sessions, use `gitsift protocol` to avoid process startup overhead.

## Architecture
Expand All @@ -129,8 +153,9 @@ src/
├── models.rs # Serde types: Hunk, HunkLine, DiffOutput, StageRequest, Response<T>
├── git/
│ ├── mod.rs # shared git2 helpers (diff_opts, delta_path, hunk_header, etc.)
│ ├── diff.rs # diff engine: git2 diff_index_to_workdir → Vec<Hunk>
│ ├── diff.rs # diff engine: diff_unstaged (index→workdir), diff_staged (HEAD→index)
│ ├── stage.rs # staging: hunk-level via ApplyOptions, line-level via patch reconstruction
│ ├── checkout.rs # checkout (discard): hunk-level reverse-patch for unstaged and staged changes
│ └── status.rs # staging status summary
├── protocol.rs # stdin/stdout JSON-lines request/response loop
├── toon.rs # compact output format (TOON-inspired, token-efficient)
Expand Down
62 changes: 52 additions & 10 deletions skills/gitsift-staging/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
name: gitsift-staging
description: "Selective git staging with gitsift — stage specific hunks or individual lines instead of entire files. Use this skill whenever the user needs to split changes into multiple commits, stage only part of their work, create atomic commits from a large diff, cherry-pick specific changes to stage, or do anything that resembles `git add -p` but with structured control. Trigger phrases include: 'stage only the bug fix', 'commit these separately', 'only stage lines X-Y', 'split this into two commits', 'don't stage everything', 'only commit the tests', 'separate the formatting from the logic', 'partial staging', 'I want to pick which changes to commit', 'selective commit'."
description: "Selective git staging and checkout with gitsift — stage or discard specific hunks or individual lines instead of entire files. Use this skill whenever the user needs to split changes into multiple commits, stage only part of their work, create atomic commits from a large diff, cherry-pick specific changes to stage, discard specific hunks, undo certain changes selectively, revert part of their work, or do anything that resembles `git add -p` or `git checkout -p` but with structured control. Trigger phrases include: 'stage only the bug fix', 'commit these separately', 'only stage lines X-Y', 'split this into two commits', 'don't stage everything', 'only commit the tests', 'separate the formatting from the logic', 'partial staging', 'I want to pick which changes to commit', 'selective commit', 'discard this hunk', 'revert only that change', 'undo the formatting changes', 'unstage this hunk', 'throw away this change'."
user_invocable: true
---

# Selective Staging with gitsift
# Selective Staging and Checkout with gitsift

gitsift is a CLI tool that replaces `git add -p` with structured output. It lets you stage individual hunks or even specific lines from unstaged changes — perfect for creating clean, atomic commits.
gitsift is a CLI tool that replaces `git add -p` and `git checkout -p` with structured output. It lets you stage or discard individual hunks or even specific lines — perfect for creating clean, atomic commits and selectively reverting changes.

**Output formats**: gitsift supports two formats via `--format`:
- `toon` — compact, token-efficient format (default). ~40% fewer tokens than JSON.
Expand All @@ -18,21 +18,28 @@ Run `gitsift --version` to confirm it's installed. If not, see https://github.co

## Core Workflow

The workflow is always: **diff → decide → stage → verify → commit**.
### Staging workflow: **diff → decide → stage → verify → commit**

### Checkout (discard) workflow: **diff → decide → checkout → verify**

### 1. See what changed

```bash
gitsift diff
```

This returns all unstaged changes in compact (toon) format. Each change is organized as files → hunks → lines, and every hunk has a unique ID you'll use for staging.
This returns all unstaged changes in compact (toon) format. Each change is organized as files → hunks → lines, and every hunk has a unique ID you'll use for staging or checkout.

To focus on a specific file:
```bash
gitsift diff --file src/main.rs
```

To see staged changes (what's in the index vs HEAD):
```bash
gitsift diff --staged
```

### 2. Decide what to stage

Look at the diff output and identify which hunks or lines belong together logically. Think about what makes a clean, atomic commit — group related changes together.
Expand Down Expand Up @@ -107,7 +114,26 @@ echo '{"line_selections": [{"hunk_id": "59a9050fd4195c94", "line_indices": [1, 2

**Important**: you cannot mix `hunk_ids` and `line_selections` in one request. If you need both, make two separate calls.

### 5. Verify and commit
### 5. Discard unwanted hunks (checkout)

To discard specific unstaged hunks (revert working tree → index):
```bash
gitsift checkout --hunk-ids abc123,def456
```

To discard specific staged hunks (revert index → HEAD):
```bash
gitsift checkout --staged --hunk-ids abc123
```

To discard via JSON stdin:
```bash
echo '{"hunk_ids": ["abc123"]}' | gitsift checkout --from-stdin
```

**Note**: Discarding an untracked file deletes it from the working tree. Discarding a staged new file removes it from the index (file stays on disk as untracked).

### 6. Verify and commit

Check what's staged vs unstaged:
```bash
Expand All @@ -119,7 +145,7 @@ Then commit as usual:
git commit -m "your message"
```

If there are more changes to stage for a second commit, go back to step 1 — you need to re-diff because hunk IDs change after staging.
If there are more changes to stage for a second commit, go back to step 1 — you need to re-diff because hunk IDs change after staging or checkout.

## Protocol Mode (persistent sessions)

Expand All @@ -131,7 +157,10 @@ gitsift protocol --repo .

```json
{"method": "diff", "params": {"file": "src/main.rs"}}
{"method": "diff", "params": {"staged": true}}
{"method": "stage", "params": {"hunk_ids": ["abc123"]}}
{"method": "checkout", "params": {"hunk_ids": ["abc123"]}}
{"method": "checkout", "params": {"hunk_ids": ["abc123"], "staged": true}}
{"method": "status"}
```

Expand All @@ -151,7 +180,7 @@ gitsift stage --hunk-ids <id>

**Changes close together merge into one hunk.** If your modifications are within 3 lines of each other, git combines them into a single hunk. You can't split them with hunk-level staging — use line-level staging (`--from-stdin` with `line_selections`) instead. Check the hunk's `lines` array to pick exactly which delete/insert pairs to include.

**Re-diff after every stage.** Once you stage something, the remaining hunks shift and their IDs change. Always run `gitsift diff` again before the next `gitsift stage`. Using stale IDs will fail with "hunk ID not found".
**Re-diff after every stage or checkout.** Once you stage or checkout something, the remaining hunks shift and their IDs change. Always run `gitsift diff` again before the next operation. Using stale IDs will fail with "hunk ID not found".

**One mode per request.** Either `hunk_ids` or `line_selections`, never both at once. The API rejects mixed requests — use separate calls if you need both.

Expand Down Expand Up @@ -181,19 +210,32 @@ git commit -m "feat: add retry logic for API calls"

## Response Format

**Compact (toon) format** default:
**Stage result — compact (toon) format** (default):
```
version: 1
ok: true
staged: 2
failed: 0
```

**JSON format** — use `--format json`:
**Stage result — JSON format** (`--format json`):
```json
{"version": 1, "ok": true, "data": {"staged": 2, "failed": 0}}
```

**Checkout result — compact (toon) format** (default):
```
version: 1
ok: true
discarded: 1
failed: 0
```

**Checkout result — JSON format** (`--format json`):
```json
{"version": 1, "ok": true, "data": {"discarded": 1, "failed": 0}}
```

On error (JSON):
```json
{"version": 1, "ok": false, "error": "description of what went wrong"}
Expand Down
20 changes: 19 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ pub struct Cli {

#[derive(Subcommand)]
pub enum Commands {
/// List all unstaged hunks
/// List unstaged hunks (or staged hunks with --staged)
Diff {
/// Filter by file path
#[arg(short, long)]
file: Option<PathBuf>,

/// Show staged changes (HEAD vs index) instead of unstaged
#[arg(long)]
staged: bool,
},
/// Stage selected hunks or lines
Stage {
Expand All @@ -35,6 +39,20 @@ pub enum Commands {
#[arg(long)]
from_stdin: bool,
},
/// Discard selected hunks (unstaged by default, or staged with --staged)
Checkout {
/// Hunk IDs to discard (comma-separated)
#[arg(long, value_delimiter = ',')]
hunk_ids: Option<Vec<String>>,

/// Read JSON selection from stdin
#[arg(long)]
from_stdin: bool,

/// Discard staged changes (index → HEAD) instead of unstaged (workdir → index)
#[arg(long)]
staged: bool,
},
/// Show staging status summary
Status,
/// Enter stdin/stdout JSON-lines protocol mode
Expand Down
Loading
Loading