git-jev-stage 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +5 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +81 -37
- package/dist/git-jev-stage.js +22 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/skills/git-jev-stage/SKILL.md +7 -6
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-jev-stage",
|
|
3
|
-
"owner": {
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Ibrahem"
|
|
5
|
+
},
|
|
6
|
+
"description": "Select Git changes for staging with a plain-language description.",
|
|
4
7
|
"plugins": [
|
|
5
8
|
{
|
|
6
9
|
"name": "git-jev-stage",
|
|
7
10
|
"source": "./",
|
|
8
|
-
"description": "
|
|
11
|
+
"description": "Select Git changes for staging with a plain-language description."
|
|
9
12
|
}
|
|
10
13
|
]
|
|
11
14
|
}
|
package/README.md
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
# git-jev-stage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Describe the change to stage in one sentence. git-jev-stage classifies each block of changed lines (a Git hunk), shows the plan, and stages the selected blocks after confirmation. It only stages. Working files stay as they are, and the commit and its message are yours to write.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
git jev-stage "only the auth fix and its tests"
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install -g git-jev-stage
|
|
13
|
+
export TYPESAFE_API_KEY=...
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Git 2.30 or newer, Node 22 or newer. Run it inside a Git repository with at least one commit.
|
|
17
|
+
|
|
18
|
+
Automatic selection uses [Jev](https://typesafe.ai), TypeSafe's decision model, and needs an API key from the early-access waitlist at [typesafe.ai](https://typesafe.ai). The key is also read from the nearest `.env` file between the current directory and the repository root. Without a key, an interactive run asks about every hunk, `--yes` and `--json` exit with `missing-api-key`, and `--dry-run` marks every hunk as `mixed` and prints no patch.
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
## The command
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
A working tree contains an auth fix, a CSS tweak, and a leftover `console.log`.
|
|
12
25
|
|
|
13
26
|
```
|
|
14
27
|
$ git jev-stage "only the auth fix and its tests"
|
|
@@ -28,32 +41,15 @@ MM src/auth/login.ts
|
|
|
28
41
|
M test/auth.test.ts
|
|
29
42
|
```
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
## Install
|
|
34
|
-
|
|
35
|
-
```sh
|
|
36
|
-
npm install -g git-jev-stage
|
|
37
|
-
export TYPESAFE_API_KEY=... # early-access key from typesafe.ai
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Git 2.30 or newer, Node 22 or newer. The key is also read from the nearest `.env` file between the current directory and the repository root.
|
|
44
|
+
In the plan, `+` means selected and `-` means left unstaged. The three scores are Jev's probabilities for `include`, `exclude` and `mixed`. A `mixed` or low-confidence decision needs review: that hunk is printed and asked about before the final confirmation. The plan lists the selection; the composed patch is printed by `--dry-run`.
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
1. **Snapshot.** `git diff` from the index to the working tree, with fixed flags (`--binary --full-index --no-renames --unified=6`), parsed byte for byte into hunks. Each hunk gets an id from its path and bytes. HEAD, the index bytes and the diff are hashed.
|
|
45
|
-
2. **One question per hunk.** Every hunk goes to [Jev](https://typesafe.ai) as a `choice` question with three options: `include` (every changed line belongs to the sentence), `exclude` (none does), `mixed` (some do). The sentence and the neighboring hunks of the same file travel along as context. Large diffs are split into windows under a token budget and sent concurrently.
|
|
46
|
-
3. **Policy.** `include` or `exclude` with confidence at or above `--threshold` (default 0.6) is taken as is. Anything else, including a missing or malformed answer, is `mixed`.
|
|
47
|
-
4. **Plan, then confirm.** The plan prints before anything changes. Each `mixed` hunk is shown and asked about: the whole hunk goes in or stays out. Lines are never split.
|
|
48
|
-
5. **Atomic staging.** The selected hunks become one patch. It is applied to a private copy of the index with `git apply --cached --check` and then `git apply --cached`, the copy is verified, `index.lock` is taken, HEAD, the index bytes and the full diff are checked against the snapshot, and the copy is renamed into place. If anything moved in between, nothing is staged and the command exits 3.
|
|
49
|
-
|
|
50
|
-
The working tree is never written. No commit is made, no message is generated.
|
|
46
|
+
Then `git commit`, and `git jev-stage "the css change"` for the next one.
|
|
51
47
|
|
|
52
48
|
## Flags
|
|
53
49
|
|
|
54
50
|
| Flag | Effect |
|
|
55
51
|
|---|---|
|
|
56
|
-
| `--exclude "<sentence>"` |
|
|
52
|
+
| `--exclude "<sentence>"` | Adds a sentence describing changes Jev should classify as out of scope. |
|
|
57
53
|
| `--dry-run` | Print the plan and the patch that would be staged. No prompts, no changes. |
|
|
58
54
|
| `--yes` | Skip the confirmation. `mixed` hunks stay unstaged and are listed. |
|
|
59
55
|
| `--json` | One JSON document on stdout, plan on stderr. Stages only with `--yes`. |
|
|
@@ -62,13 +58,35 @@ The working tree is never written. No commit is made, no message is generated.
|
|
|
62
58
|
|
|
63
59
|
Exit codes: 0 done or nothing to stage, 1 error, 2 usage, 3 the snapshot went stale or the index is locked. Git routes `git jev-stage --help` to a man page; use `git-jev-stage --help`.
|
|
64
60
|
|
|
61
|
+
## Limits
|
|
62
|
+
|
|
63
|
+
- Hunks are git's, cut with 6 lines of context. A hunk with wanted and unwanted lines is `mixed`, and is staged whole or not at all.
|
|
64
|
+
- Untracked files are not included. Run `git add -N <path>` to make a new text file available for hunk classification.
|
|
65
|
+
- Renames appear as a deletion and an addition. Either side can be staged separately.
|
|
66
|
+
- Binary, symlink and submodule changes stop the run with an error naming the paths. Stage or stash those first. A submodule that is only dirty is ignored.
|
|
67
|
+
- Empty new files and mode-only changes are listed as skipped. They cannot be staged by hunk; stage them with `git add`. Staging any hunk of a file also stages that file's mode change.
|
|
68
|
+
- Hunks in different windows of a large diff do not see each other.
|
|
69
|
+
- If HEAD, the index or the working tree changes between the plan and the confirmation, nothing is staged and the command exits 3. Run it again.
|
|
70
|
+
|
|
71
|
+
## What leaves the machine
|
|
72
|
+
|
|
73
|
+
The command sends the staging sentence, the optional exclusion sentence, changed file paths, hunk headers and hunk text to api.typesafe.ai. During staging it writes a temporary private index and updates the repository index. It does not write working-tree files.
|
|
74
|
+
|
|
75
|
+
## Implementation details
|
|
76
|
+
|
|
77
|
+
1. **Snapshot.** `git diff` from the index to the working tree, with fixed flags (`--binary --full-index --no-renames --unified=6`), parsed byte for byte into hunks. Each hunk gets an id from its path and bytes. HEAD, the index bytes and the diff are hashed.
|
|
78
|
+
2. **One question per hunk.** Every hunk goes to Jev as a `choice` question with three options: `include` (every changed line belongs to the sentence), `exclude` (none does), `mixed` (some do). The sentence and the neighboring hunks of the same file travel along as context. Large diffs are split into windows under a token budget and sent concurrently.
|
|
79
|
+
3. **Policy.** `include` or `exclude` with confidence at or above `--threshold` (default 0.6) is taken as is. Anything else, including a missing or malformed answer, is `mixed`.
|
|
80
|
+
4. **Plan, then confirm.** The plan prints before anything changes. Each `mixed` hunk is shown and asked about: the whole hunk goes in or stays out. Lines are never split.
|
|
81
|
+
5. **Atomic staging.** The selected hunks become one patch. It is applied to a private copy of the index with `git apply --cached --check` and then `git apply --cached`, the copy is verified, `index.lock` is taken, HEAD, the index bytes and the full diff are checked against the snapshot, and the copy is renamed into place. If anything moved in between, nothing is staged and the command exits 3.
|
|
82
|
+
|
|
65
83
|
## For coding agents
|
|
66
84
|
|
|
67
85
|
```sh
|
|
68
86
|
git jev-stage "the auth fix" --json --yes
|
|
69
87
|
```
|
|
70
88
|
|
|
71
|
-
The document lists every hunk with its `id`, `header`, `text`, `decision`, `source`, `confidence` and `probabilities`, plus `applied`, `stagedHunkIds` and `mixedHunkIds`. An agent stages the mixed ones itself or leaves them.
|
|
89
|
+
The document lists every hunk with its `id`, `header`, `text`, `decision`, `source`, `confidence` and `probabilities`, plus `applied`, `stagedHunkIds` and `mixedHunkIds`. An agent stages the mixed ones itself or leaves them. `source` explains how the decision was produced: `model`, `manual`, `low-confidence`, `missing`, `invalid`, `too-large` or `no-provider`.
|
|
72
90
|
|
|
73
91
|
Claude Code plugin:
|
|
74
92
|
|
|
@@ -81,31 +99,57 @@ Other agents: `npx skills add ibrahemid/git-jev-stage`. The skill is `skills/git
|
|
|
81
99
|
|
|
82
100
|
## Library
|
|
83
101
|
|
|
84
|
-
```
|
|
85
|
-
|
|
102
|
+
```sh
|
|
103
|
+
npm install git-jev-stage
|
|
104
|
+
```
|
|
86
105
|
|
|
87
|
-
|
|
106
|
+
```ts
|
|
107
|
+
import { applySelection, planSelection, TypeSafeJevProvider } from "git-jev-stage";
|
|
108
|
+
|
|
109
|
+
const provider = new TypeSafeJevProvider({ apiKey: process.env.TYPESAFE_API_KEY });
|
|
110
|
+
const plan = await planSelection({
|
|
111
|
+
cwd: process.cwd(),
|
|
112
|
+
intent: "the auth fix",
|
|
113
|
+
provider,
|
|
114
|
+
});
|
|
88
115
|
const includeIds = [...plan.decisions.values()]
|
|
89
116
|
.filter((decision) => decision.decision === "include")
|
|
90
117
|
.map((decision) => decision.hunkId);
|
|
91
118
|
await applySelection(plan, { includeIds });
|
|
92
119
|
```
|
|
93
120
|
|
|
94
|
-
`
|
|
121
|
+
Read `plan.decisions` and the text of the selected hunks before applying.
|
|
95
122
|
|
|
96
|
-
|
|
123
|
+
`planSelection({ cwd, intent, exclude?, threshold?, provider?, git? })` reads the unstaged diff and returns a `Plan`. It does not read `TYPESAFE_API_KEY` and does not mutate the index. Without a provider every decision is `mixed`, so the selected-id list above comes out empty.
|
|
97
124
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
`applySelection(plan, { includeIds, git? })` preserves existing staged changes and returns `{ stagedHunkIds, skippedMixedIds, patchBytes }`. It rejects unknown ids and refuses to apply if HEAD, the index or the working-tree diff changed. Selecting a hunk also stages that file's mode change.
|
|
126
|
+
|
|
127
|
+
Failures arrive as `UnknownHunkError`, `StaleSnapshotError`, `IndexLockedError` and `PatchApplyError`. The provider throws `ProviderConfigError` for an empty key and `ProviderError` for a failed request.
|
|
128
|
+
|
|
129
|
+
In tests, pass a `FakeProvider`:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { FakeProvider, type FakeScript, planSelection } from "git-jev-stage";
|
|
133
|
+
|
|
134
|
+
const script: FakeScript = {
|
|
135
|
+
defaultAnswer: {
|
|
136
|
+
choice: "include",
|
|
137
|
+
confidence: 1,
|
|
138
|
+
probabilities: { include: 1, exclude: 0, mixed: 0 },
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const plan = await planSelection({
|
|
142
|
+
cwd: process.cwd(),
|
|
143
|
+
intent: "the auth fix",
|
|
144
|
+
provider: new FakeProvider(script),
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The same validator runs on scripted and real answers.
|
|
105
149
|
|
|
106
150
|
## Neighbors
|
|
107
151
|
|
|
108
|
-
- `git add -p`:
|
|
152
|
+
- `git add -p`: interactive staging with options to split or edit hunks, without sentence-based classification.
|
|
109
153
|
- [git-surgeon](https://github.com/raine/git-surgeon): stages explicit hunk ids and line ranges. Built for agents that already know which lines they want.
|
|
110
154
|
- [VibeGit](https://github.com/kklemon/vibegit): groups a whole working tree into commits with an LLM. git-jev-stage answers one narrower question and never commits.
|
|
111
155
|
|
package/dist/git-jev-stage.js
CHANGED
|
@@ -1224,7 +1224,7 @@ var IndexLockedError = class extends JevStageError {
|
|
|
1224
1224
|
};
|
|
1225
1225
|
var MissingApiKeyError = class extends JevStageError {
|
|
1226
1226
|
code = "missing-api-key";
|
|
1227
|
-
constructor(message = "TYPESAFE_API_KEY is not set", options) {
|
|
1227
|
+
constructor(message = "TYPESAFE_API_KEY is not set. Set an early-access key from typesafe.ai, or rerun in an interactive terminal without --json or --yes to select hunks by hand.", options) {
|
|
1228
1228
|
super(message, options);
|
|
1229
1229
|
this.name = "MissingApiKeyError";
|
|
1230
1230
|
}
|
|
@@ -2387,7 +2387,7 @@ function withoutProvider(snapshot) {
|
|
|
2387
2387
|
}
|
|
2388
2388
|
|
|
2389
2389
|
// src/version.ts
|
|
2390
|
-
var VERSION2 = "0.1.
|
|
2390
|
+
var VERSION2 = "0.1.1";
|
|
2391
2391
|
|
|
2392
2392
|
// src/cli/args.ts
|
|
2393
2393
|
import { parseArgs } from "node:util";
|
|
@@ -2409,10 +2409,11 @@ function usageText() {
|
|
|
2409
2409
|
return [
|
|
2410
2410
|
'usage: git jev-stage "<sentence>" [options]',
|
|
2411
2411
|
"",
|
|
2412
|
-
"
|
|
2412
|
+
"Classifies every unstaged hunk against the sentence and stages the selected",
|
|
2413
|
+
"hunks after confirmation.",
|
|
2413
2414
|
"",
|
|
2414
2415
|
"options:",
|
|
2415
|
-
' --exclude "<sentence>"
|
|
2416
|
+
' --exclude "<sentence>" describes changes to classify as out of scope',
|
|
2416
2417
|
" --threshold <n> confidence needed to stage or skip a hunk, 0-1 (default 0.6)",
|
|
2417
2418
|
" --dry-run print the plan and the composed patch, stage nothing",
|
|
2418
2419
|
" --yes skip prompts and leave mixed hunks unstaged",
|
|
@@ -2640,6 +2641,7 @@ var FILE_MARKS = Object.freeze({
|
|
|
2640
2641
|
"mode-only": "M"
|
|
2641
2642
|
});
|
|
2642
2643
|
var PROBABILITY_ORDER = Object.freeze(["include", "exclude", "mixed"]);
|
|
2644
|
+
var SKIPPED_INSTRUCTION = "these changes cannot be staged by hunk; stage them with git add";
|
|
2643
2645
|
function shouldColor({ requested, isTty, noColor }) {
|
|
2644
2646
|
return requested && isTty && (noColor === void 0 || noColor.length === 0);
|
|
2645
2647
|
}
|
|
@@ -2651,9 +2653,7 @@ function renderPlan({ plan, includeIds, color }) {
|
|
|
2651
2653
|
lines.push(renderHunk(hunk.id, hunk.header, plan.decisions.get(hunk.id), color));
|
|
2652
2654
|
}
|
|
2653
2655
|
}
|
|
2654
|
-
|
|
2655
|
-
lines.push(`skipped: ${skipped.path} (${skipped.reason})`);
|
|
2656
|
-
}
|
|
2656
|
+
lines.push(...skippedLines(plan.snapshot.skipped));
|
|
2657
2657
|
const summary = summarizeSelection(plan.snapshot.files, includeIds);
|
|
2658
2658
|
lines.push(
|
|
2659
2659
|
`will stage: ${count(summary.hunks, "hunk")}, ${count(summary.files, "file")} (+${summary.added} -${summary.removed})`
|
|
@@ -2661,6 +2661,20 @@ function renderPlan({ plan, includeIds, color }) {
|
|
|
2661
2661
|
return `${lines.join("\n")}
|
|
2662
2662
|
`;
|
|
2663
2663
|
}
|
|
2664
|
+
function renderSkipped(skipped) {
|
|
2665
|
+
const lines = skippedLines(skipped);
|
|
2666
|
+
return lines.length === 0 ? "" : `${lines.join("\n")}
|
|
2667
|
+
`;
|
|
2668
|
+
}
|
|
2669
|
+
function skippedLines(skipped) {
|
|
2670
|
+
if (skipped.length === 0) {
|
|
2671
|
+
return [];
|
|
2672
|
+
}
|
|
2673
|
+
return [
|
|
2674
|
+
...skipped.map((entry) => `skipped: ${entry.path} (${entry.reason})`),
|
|
2675
|
+
SKIPPED_INSTRUCTION
|
|
2676
|
+
];
|
|
2677
|
+
}
|
|
2664
2678
|
function renderPatch(patch, write) {
|
|
2665
2679
|
if (patch.length === 0) {
|
|
2666
2680
|
return;
|
|
@@ -2776,6 +2790,7 @@ async function stage(options, io) {
|
|
|
2776
2790
|
...provider === void 0 ? {} : { provider }
|
|
2777
2791
|
});
|
|
2778
2792
|
if (plan.snapshot.files.length === 0) {
|
|
2793
|
+
report(renderSkipped(plan.snapshot.skipped));
|
|
2779
2794
|
if (options.json) {
|
|
2780
2795
|
io.stdout(renderJsonDocument({ plan, applied: false, stagedHunkIds: [], mixedHunkIds: [] }));
|
|
2781
2796
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -211,7 +211,7 @@ declare class GitCommandError extends JevStageError {
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
type Decision = "include" | "exclude" | "mixed";
|
|
214
|
-
type DecisionSource = "model" | "low-confidence" | "missing" | "invalid" | "too-large" | "
|
|
214
|
+
type DecisionSource = "model" | "low-confidence" | "missing" | "invalid" | "too-large" | "manual" | "no-provider";
|
|
215
215
|
type FileKind = "modified" | "added" | "deleted" | "mode-only";
|
|
216
216
|
interface Hunk {
|
|
217
217
|
id: string;
|
package/dist/index.js
CHANGED
|
@@ -634,7 +634,7 @@ var IndexLockedError = class extends JevStageError {
|
|
|
634
634
|
};
|
|
635
635
|
var MissingApiKeyError = class extends JevStageError {
|
|
636
636
|
code = "missing-api-key";
|
|
637
|
-
constructor(message = "TYPESAFE_API_KEY is not set", options) {
|
|
637
|
+
constructor(message = "TYPESAFE_API_KEY is not set. Set an early-access key from typesafe.ai, or rerun in an interactive terminal without --json or --yes to select hunks by hand.", options) {
|
|
638
638
|
super(message, options);
|
|
639
639
|
this.name = "MissingApiKeyError";
|
|
640
640
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: git-jev-stage
|
|
3
|
-
description: Stage
|
|
3
|
+
description: Stage Git changes selected by a plain-language description, with git jev-stage. Use when asked to commit or stage part of the working tree ("commit just the auth fix", "stage only the tests", "leave the debug logging out").
|
|
4
4
|
allowed-tools: Bash(git jev-stage *) Bash(git-jev-stage *) Bash(git status *) Bash(git diff *) Bash(git commit *) Bash(git add *)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
`git jev-stage "<sentence>"`
|
|
7
|
+
`git jev-stage "<sentence>"` asks Jev to classify every unstaged hunk. In interactive mode, it stages selected hunks after confirmation, with separate questions for `mixed` or low-confidence decisions. With `--yes`, it stages accepted `include` decisions and leaves `mixed` hunks unstaged. It only stages: working files stay untouched, and the commit and its message are left to the caller.
|
|
8
8
|
|
|
9
9
|
## Steps
|
|
10
10
|
|
|
@@ -14,21 +14,22 @@ allowed-tools: Bash(git jev-stage *) Bash(git-jev-stage *) Bash(git status *) Ba
|
|
|
14
14
|
git jev-stage "<what to stage>" --json --yes
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Add `--exclude "<what to leave out>"`
|
|
17
|
+
Add `--exclude "<what to leave out>"` to give Jev explicit out-of-scope context. Verify those changes remain unstaged.
|
|
18
18
|
|
|
19
19
|
2. Read the JSON. `applied: true` means the `include` hunks are now in the index. `mixedHunkIds` lists hunks the tool did not stage because they hold both wanted and unwanted lines, or because it was not confident.
|
|
20
20
|
|
|
21
|
-
3. If `mixedHunkIds` is non-empty, decide each one
|
|
21
|
+
3. If `mixedHunkIds` is non-empty, decide each one: read the hunk under `files[].hunks[]` (its `header` and `text`), then stage the whole hunk with `git add -p` if it belongs, or leave it. Never edit the working tree to split a hunk.
|
|
22
22
|
|
|
23
23
|
4. Confirm with `git diff --cached --stat`, then commit.
|
|
24
24
|
|
|
25
25
|
## Failure modes
|
|
26
26
|
|
|
27
|
-
- Exit 1 with `missing-api-key`: `TYPESAFE_API_KEY` is not set.
|
|
27
|
+
- Exit 1 with `missing-api-key`: `TYPESAFE_API_KEY` is not set. `--yes` and `--json` need a key. An interactive run without one asks about every hunk, and `--dry-run` marks every hunk as `mixed` and prints no patch. In an agent session, fall back to `git add -p`.
|
|
28
28
|
- Exit 3 with `stale-snapshot` or `index-locked`: the working tree, index, or HEAD changed while planning, or another git process holds the index. Nothing was staged. Run the command again.
|
|
29
29
|
- Exit 1 with `unsupported-entry`: binary, symlink, or submodule changes are present. Stage those by path with `git add` first, or stash them, then rerun.
|
|
30
|
+
- Paths printed as `skipped` are mode-only or empty-file changes. They cannot be staged by hunk; stage them with `git add`.
|
|
30
31
|
|
|
31
32
|
## Do not
|
|
32
33
|
|
|
33
34
|
- Do not pass `--yes` without `--json` in an agent session; the interactive prompt cannot be answered.
|
|
34
|
-
- Do not describe the whole diff.
|
|
35
|
+
- Do not describe the whole diff. Describe the wanted change in one sentence, then verify `git diff --cached` before committing.
|