@arthony/keybook 0.3.1 → 0.4.0
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/README.md +3 -2
- package/dist/cli.js +185 -33
- package/package.json +1 -1
- package/seed/git.yaml +272 -0
- package/seed/vim.yaml +112 -0
package/README.md
CHANGED
|
@@ -42,8 +42,9 @@ keybook check # validate your data files
|
|
|
42
42
|
In the TUI: type to fuzzy-search, ↑/↓ to move, ⏎ to copy the shortcut (or a
|
|
43
43
|
recipe's command) to the clipboard, ⎋ to quit.
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
Manage entries without leaving the search screen: `⌃O` to add, `⌃E` to edit the
|
|
46
|
+
selected entry (pre-filled form), `⌃X` to delete it (with a `y/n` confirm). Or
|
|
47
|
+
script an add: `keybook add --app Fork --action 'Push' --keys 'shift cmd p' --tags push`
|
|
47
48
|
(`--keys` accepts glyphs or words; recipes use repeatable `--step`).
|
|
48
49
|
|
|
49
50
|
## Your data
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Command } from "commander";
|
|
|
7
7
|
import { Box, Text, render, useApp, useInput, useStdout } from "ink";
|
|
8
8
|
import { createElement, useCallback, useMemo, useState } from "react";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
-
import { Document, parse, parseDocument } from "yaml";
|
|
10
|
+
import { Document, isMap, isSeq, parse, parseDocument } from "yaml";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
13
13
|
import { Fzf, extendedMatch } from "fzf";
|
|
@@ -177,6 +177,8 @@ function loadEntries(dataDir) {
|
|
|
177
177
|
}
|
|
178
178
|
entries.push({
|
|
179
179
|
app,
|
|
180
|
+
file,
|
|
181
|
+
index: i,
|
|
180
182
|
...parsed.data
|
|
181
183
|
});
|
|
182
184
|
});
|
|
@@ -310,6 +312,73 @@ function addEntry(dir, app, entry) {
|
|
|
310
312
|
lines: [`✓ ${created ? "created" : "added to"} ${basename(file)}`]
|
|
311
313
|
};
|
|
312
314
|
}
|
|
315
|
+
function deleteEntry(dir, file, index, expectedAction) {
|
|
316
|
+
const path = join(dir, file);
|
|
317
|
+
let original;
|
|
318
|
+
try {
|
|
319
|
+
original = readFileSync(path, "utf8");
|
|
320
|
+
} catch (e) {
|
|
321
|
+
return err(path, [e.message]);
|
|
322
|
+
}
|
|
323
|
+
const doc = parseDocument(original);
|
|
324
|
+
const node = doc.getIn(["entries", index]);
|
|
325
|
+
if ((isMap(node) ? String(node.get("action") ?? "").trim() : void 0) !== expectedAction.trim()) return err(path, ["✗ entry changed on disk — reload and retry"]);
|
|
326
|
+
doc.deleteIn(["entries", index]);
|
|
327
|
+
const seq = doc.getIn(["entries"]);
|
|
328
|
+
const emptied = isSeq(seq) ? seq.items.length === 0 : true;
|
|
329
|
+
try {
|
|
330
|
+
if (emptied) unlinkSync(path);
|
|
331
|
+
else writeFileSync(path, doc.toString());
|
|
332
|
+
} catch (e) {
|
|
333
|
+
return err(path, [e.message]);
|
|
334
|
+
}
|
|
335
|
+
if (!emptied) {
|
|
336
|
+
const fileErr = loadEntries(dir).errors.find((e) => e.file === basename(file));
|
|
337
|
+
if (fileErr) {
|
|
338
|
+
writeFileSync(path, original);
|
|
339
|
+
return err(path, [`✗ ${fileErr.message}`]);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
ok: true,
|
|
344
|
+
file: path,
|
|
345
|
+
created: false,
|
|
346
|
+
lines: [emptied ? `✗ deleted '${expectedAction}' (removed empty ${basename(file)})` : `✗ deleted '${expectedAction}'`]
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function editEntry(dir, file, index, entry, expectedAction) {
|
|
350
|
+
if ("app" in entry) return err("", ["Error: entry must not have an app field; use --app instead"]);
|
|
351
|
+
const parsed = entrySchema.safeParse(entry);
|
|
352
|
+
if (!parsed.success) return err("", parsed.error.issues.map((i) => i.message));
|
|
353
|
+
const clean = buildClean(parsed.data);
|
|
354
|
+
const path = join(dir, file);
|
|
355
|
+
let original;
|
|
356
|
+
try {
|
|
357
|
+
original = readFileSync(path, "utf8");
|
|
358
|
+
} catch (e) {
|
|
359
|
+
return err(path, [e.message]);
|
|
360
|
+
}
|
|
361
|
+
const doc = parseDocument(original);
|
|
362
|
+
const node = doc.getIn(["entries", index]);
|
|
363
|
+
if ((isMap(node) ? String(node.get("action") ?? "").trim() : void 0) !== expectedAction.trim()) return err(path, ["✗ entry changed on disk — reload and retry"]);
|
|
364
|
+
doc.setIn(["entries", index], doc.createNode(clean));
|
|
365
|
+
try {
|
|
366
|
+
writeFileSync(path, doc.toString());
|
|
367
|
+
} catch (e) {
|
|
368
|
+
return err(path, [e.message]);
|
|
369
|
+
}
|
|
370
|
+
const fileErr = loadEntries(dir).errors.find((e) => e.file === basename(file));
|
|
371
|
+
if (fileErr) {
|
|
372
|
+
writeFileSync(path, original);
|
|
373
|
+
return err(path, [`✗ ${fileErr.message}`]);
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
ok: true,
|
|
377
|
+
file: path,
|
|
378
|
+
created: false,
|
|
379
|
+
lines: [`✓ updated '${clean.action}'`]
|
|
380
|
+
};
|
|
381
|
+
}
|
|
313
382
|
//#endregion
|
|
314
383
|
//#region src/commands.ts
|
|
315
384
|
function runPath(env = process.env) {
|
|
@@ -390,7 +459,7 @@ function Field({ label, value, focused }) {
|
|
|
390
459
|
}) : null
|
|
391
460
|
] });
|
|
392
461
|
}
|
|
393
|
-
function FormFields({ draft, apps, appIndex, focused, existingTags }) {
|
|
462
|
+
function FormFields({ draft, apps, appIndex, focused, existingTags, lockedApp }) {
|
|
394
463
|
const appChoices = [...apps, "Create new app…"];
|
|
395
464
|
return /* @__PURE__ */ jsxs(Box, {
|
|
396
465
|
flexDirection: "column",
|
|
@@ -398,7 +467,10 @@ function FormFields({ draft, apps, appIndex, focused, existingTags }) {
|
|
|
398
467
|
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
|
|
399
468
|
color: focused === 0 ? "cyan" : "gray",
|
|
400
469
|
children: "App".padEnd(8)
|
|
401
|
-
}),
|
|
470
|
+
}), lockedApp ? /* @__PURE__ */ jsx(Text, {
|
|
471
|
+
color: "gray",
|
|
472
|
+
children: `${lockedApp} (locked)`
|
|
473
|
+
}) : draft.creatingApp ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, { children: draft.newApp }), focused === 0 ? /* @__PURE__ */ jsx(Text, {
|
|
402
474
|
inverse: true,
|
|
403
475
|
children: " "
|
|
404
476
|
}) : null] }) : /* @__PURE__ */ jsxs(Text, { children: [appChoices[appIndex] ?? "—", focused === 0 ? " (↑/↓)" : ""] })] }),
|
|
@@ -617,10 +689,25 @@ function draftToEntryInput(d) {
|
|
|
617
689
|
if (d.notes.trim()) e.notes = d.notes.trim();
|
|
618
690
|
return e;
|
|
619
691
|
}
|
|
620
|
-
|
|
692
|
+
/** Inverse of draftToEntryInput: seed a Draft from an existing entry for editing. */
|
|
693
|
+
function entryToDraft(app, e) {
|
|
694
|
+
const type = e.keys ? "shortcut" : e.command ? "command" : "recipe";
|
|
695
|
+
return {
|
|
696
|
+
...emptyDraft,
|
|
697
|
+
app,
|
|
698
|
+
type,
|
|
699
|
+
action: e.action,
|
|
700
|
+
keys: e.keys ?? "",
|
|
701
|
+
command: e.command ?? "",
|
|
702
|
+
steps: e.steps ? [...e.steps] : [],
|
|
703
|
+
tags: (e.tags ?? []).join(", "),
|
|
704
|
+
notes: e.notes ?? ""
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function useAddForm(initial = {}) {
|
|
621
708
|
const [draft, setDraft] = useState(() => ({
|
|
622
709
|
...emptyDraft,
|
|
623
|
-
|
|
710
|
+
...initial
|
|
624
711
|
}));
|
|
625
712
|
const update = (patch) => setDraft((d) => ({
|
|
626
713
|
...d,
|
|
@@ -640,9 +727,9 @@ const TYPES = [
|
|
|
640
727
|
"recipe"
|
|
641
728
|
];
|
|
642
729
|
const LAST_FIELD = 5;
|
|
643
|
-
function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, resolveTarget }) {
|
|
644
|
-
const { draft, update, setDraft } = useAddForm(apps[0] ?? "");
|
|
645
|
-
const [focused, setFocused] = useState(0);
|
|
730
|
+
function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, resolveTarget, initial, lockedApp, title }) {
|
|
731
|
+
const { draft, update, setDraft } = useAddForm(initial ?? { app: apps[0] ?? "" });
|
|
732
|
+
const [focused, setFocused] = useState(lockedApp ? 1 : 0);
|
|
646
733
|
const [appIndex, setAppIndex] = useState(0);
|
|
647
734
|
const [screen, setScreen] = useState("form");
|
|
648
735
|
const [hint, setHint] = useState("");
|
|
@@ -682,7 +769,7 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
682
769
|
}
|
|
683
770
|
if (key.escape) return onCancel();
|
|
684
771
|
if (key.ctrl && input === "n") return setFocused((f) => Math.min(f + 1, LAST_FIELD));
|
|
685
|
-
if (key.ctrl && input === "p") return setFocused((f) => Math.max(f - 1, 0));
|
|
772
|
+
if (key.ctrl && input === "p") return setFocused((f) => Math.max(f - 1, lockedApp ? 1 : 0));
|
|
686
773
|
if (focused === 0) {
|
|
687
774
|
if (key.upArrow) {
|
|
688
775
|
const next = Math.max(appIndex - 1, 0);
|
|
@@ -742,7 +829,7 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
742
829
|
children: [
|
|
743
830
|
/* @__PURE__ */ jsx(Text, {
|
|
744
831
|
color: "cyan",
|
|
745
|
-
children: "keybook add"
|
|
832
|
+
children: title ?? "keybook add"
|
|
746
833
|
}),
|
|
747
834
|
/* @__PURE__ */ jsx(Box, {
|
|
748
835
|
marginTop: 1,
|
|
@@ -751,7 +838,8 @@ function AddEntryForm({ apps, existingTags, onSubmit, onComplete, onCancel, reso
|
|
|
751
838
|
apps,
|
|
752
839
|
appIndex,
|
|
753
840
|
focused,
|
|
754
|
-
existingTags
|
|
841
|
+
existingTags,
|
|
842
|
+
lockedApp
|
|
755
843
|
})
|
|
756
844
|
}),
|
|
757
845
|
/* @__PURE__ */ jsx(Box, {
|
|
@@ -796,7 +884,20 @@ function search(entries, query) {
|
|
|
796
884
|
}
|
|
797
885
|
//#endregion
|
|
798
886
|
//#region src/tui/Footer.tsx
|
|
799
|
-
function Footer({ flash, errorCount, resultCount }) {
|
|
887
|
+
function Footer({ flash, errorCount, resultCount, confirm }) {
|
|
888
|
+
if (confirm) return /* @__PURE__ */ jsxs(Box, {
|
|
889
|
+
marginTop: 1,
|
|
890
|
+
justifyContent: "space-between",
|
|
891
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
892
|
+
color: "yellow",
|
|
893
|
+
wrap: "truncate-end",
|
|
894
|
+
children: confirm
|
|
895
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
896
|
+
color: "yellow",
|
|
897
|
+
bold: true,
|
|
898
|
+
children: " y / n"
|
|
899
|
+
})]
|
|
900
|
+
});
|
|
800
901
|
return /* @__PURE__ */ jsxs(Box, {
|
|
801
902
|
marginTop: 1,
|
|
802
903
|
justifyContent: "space-between",
|
|
@@ -804,7 +905,7 @@ function Footer({ flash, errorCount, resultCount }) {
|
|
|
804
905
|
color: "gray",
|
|
805
906
|
wrap: "truncate-end",
|
|
806
907
|
children: [
|
|
807
|
-
"↑↓ move ⏎ copy ⌃O add
|
|
908
|
+
"↑↓ move ⏎ copy ⌃O add ⌃E edit ⌃X del ⎋ quit ⌃U clear (",
|
|
808
909
|
resultCount,
|
|
809
910
|
")"
|
|
810
911
|
]
|
|
@@ -972,6 +1073,8 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
972
1073
|
const { stdout } = useStdout();
|
|
973
1074
|
const [entries, setEntries] = useState(initial);
|
|
974
1075
|
const [mode, setMode] = useState("search");
|
|
1076
|
+
const [editTarget, setEditTarget] = useState(null);
|
|
1077
|
+
const [pendingDelete, setPendingDelete] = useState(null);
|
|
975
1078
|
const [query, setQuery] = useState("");
|
|
976
1079
|
const [selected, setSelected] = useState(0);
|
|
977
1080
|
const [flash, setFlash] = useState("");
|
|
@@ -984,8 +1087,24 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
984
1087
|
const listHeight = visibleListHeight(stdout?.rows);
|
|
985
1088
|
const { left, right } = columnWidths(stdout?.columns);
|
|
986
1089
|
useInput((input, key) => {
|
|
1090
|
+
if (key.ctrl && input === "c") {
|
|
1091
|
+
exit();
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (pendingDelete) {
|
|
1095
|
+
if ((key.return || input === "y" || input === "Y") && dataDir) {
|
|
1096
|
+
const r = deleteEntry(dataDir, pendingDelete.file, pendingDelete.index, pendingDelete.action.trim());
|
|
1097
|
+
if (r.ok) {
|
|
1098
|
+
reload();
|
|
1099
|
+
setSelected(0);
|
|
1100
|
+
}
|
|
1101
|
+
setFlash(r.lines[0] ?? (r.ok ? "✗ deleted" : "✗ delete failed"));
|
|
1102
|
+
} else setFlash("");
|
|
1103
|
+
setPendingDelete(null);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
987
1106
|
if (!key.return && flash) setFlash("");
|
|
988
|
-
if (key.escape
|
|
1107
|
+
if (key.escape) {
|
|
989
1108
|
exit();
|
|
990
1109
|
return;
|
|
991
1110
|
}
|
|
@@ -993,6 +1112,15 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
993
1112
|
setMode("add");
|
|
994
1113
|
return;
|
|
995
1114
|
}
|
|
1115
|
+
if (dataDir && current && key.ctrl && input === "e") {
|
|
1116
|
+
setEditTarget(current);
|
|
1117
|
+
setMode("edit");
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
if (dataDir && current && key.ctrl && input === "x") {
|
|
1121
|
+
setPendingDelete(current);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
996
1124
|
if (key.downArrow || key.ctrl && input === "n") {
|
|
997
1125
|
setSelected((s) => Math.min(s + 1, results.length - 1));
|
|
998
1126
|
return;
|
|
@@ -1025,24 +1153,47 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1025
1153
|
setSelected(0);
|
|
1026
1154
|
}
|
|
1027
1155
|
}, { isActive: mode === "search" });
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1156
|
+
const existingTags = [...new Set(entries.flatMap((e) => e.tags ?? []))].sort();
|
|
1157
|
+
if (mode === "add" && dataDir) return /* @__PURE__ */ jsx(AddEntryForm, {
|
|
1158
|
+
apps: listApps(dataDir),
|
|
1159
|
+
existingTags,
|
|
1160
|
+
resolveTarget: (a) => resolveTargetFile(dataDir, a),
|
|
1161
|
+
onSubmit: (app, entry) => addEntry(dataDir, app, entry),
|
|
1162
|
+
onComplete: (result) => {
|
|
1163
|
+
if (result.ok) {
|
|
1164
|
+
reload();
|
|
1165
|
+
setSelected(0);
|
|
1166
|
+
setFlash(result.lines[0] ?? "✓ added");
|
|
1167
|
+
}
|
|
1168
|
+
setMode("search");
|
|
1169
|
+
},
|
|
1170
|
+
onCancel: () => setMode("search")
|
|
1171
|
+
});
|
|
1172
|
+
if (mode === "edit" && dataDir && editTarget) return /* @__PURE__ */ jsx(AddEntryForm, {
|
|
1173
|
+
apps: listApps(dataDir),
|
|
1174
|
+
existingTags,
|
|
1175
|
+
lockedApp: editTarget.app,
|
|
1176
|
+
initial: entryToDraft(editTarget.app, editTarget),
|
|
1177
|
+
title: `Edit entry — ${editTarget.app}`,
|
|
1178
|
+
resolveTarget: () => ({
|
|
1179
|
+
file: editTarget.file,
|
|
1180
|
+
created: false
|
|
1181
|
+
}),
|
|
1182
|
+
onSubmit: (_app, entry) => editEntry(dataDir, editTarget.file, editTarget.index, entry, editTarget.action),
|
|
1183
|
+
onComplete: (result) => {
|
|
1184
|
+
if (result.ok) {
|
|
1185
|
+
reload();
|
|
1186
|
+
setSelected(0);
|
|
1187
|
+
setFlash(result.lines[0] ?? "✓ updated");
|
|
1188
|
+
}
|
|
1189
|
+
setMode("search");
|
|
1190
|
+
setEditTarget(null);
|
|
1191
|
+
},
|
|
1192
|
+
onCancel: () => {
|
|
1193
|
+
setMode("search");
|
|
1194
|
+
setEditTarget(null);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1046
1197
|
return /* @__PURE__ */ jsxs(Box, {
|
|
1047
1198
|
flexDirection: "column",
|
|
1048
1199
|
children: [
|
|
@@ -1060,7 +1211,8 @@ function App({ entries: initial, errorCount = 0, dataDir, onCopy = copyToClipboa
|
|
|
1060
1211
|
/* @__PURE__ */ jsx(Footer, {
|
|
1061
1212
|
flash,
|
|
1062
1213
|
errorCount,
|
|
1063
|
-
resultCount: results.length
|
|
1214
|
+
resultCount: results.length,
|
|
1215
|
+
confirm: pendingDelete ? `Delete '${pendingDelete.app}: ${pendingDelete.action}'?` : void 0
|
|
1064
1216
|
})
|
|
1065
1217
|
]
|
|
1066
1218
|
});
|
package/package.json
CHANGED
package/seed/git.yaml
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Curated git CLI essentials — branches, commits, history rewriting, recovery.
|
|
2
|
+
# Almost every entry has a `command` (Enter copies it to the clipboard); recipes
|
|
3
|
+
# use `steps` and place the canonical command in `command` so paste-and-tweak
|
|
4
|
+
# still works.
|
|
5
|
+
app: Git
|
|
6
|
+
entries:
|
|
7
|
+
# ---- Branches ----
|
|
8
|
+
- action: Create and switch to a new branch
|
|
9
|
+
command: git switch -c <name>
|
|
10
|
+
notes: Legacy synonym is `git checkout -b <name>`.
|
|
11
|
+
tags: [branch, create, checkout, new]
|
|
12
|
+
|
|
13
|
+
- action: Switch to an existing branch
|
|
14
|
+
command: git switch <name>
|
|
15
|
+
notes: Legacy synonym is `git checkout <name>`.
|
|
16
|
+
tags: [branch, checkout, switch]
|
|
17
|
+
|
|
18
|
+
- action: Delete a local branch
|
|
19
|
+
command: git branch -d <name>
|
|
20
|
+
notes: Use -D (capital) to force-delete a branch with unmerged commits.
|
|
21
|
+
tags: [branch, delete, remove]
|
|
22
|
+
|
|
23
|
+
- action: Delete a remote branch
|
|
24
|
+
command: git push origin --delete <name>
|
|
25
|
+
tags: [branch, delete, remote, push]
|
|
26
|
+
|
|
27
|
+
- action: Rename the current branch
|
|
28
|
+
command: git branch -m <new-name>
|
|
29
|
+
tags: [branch, rename, move]
|
|
30
|
+
|
|
31
|
+
# ---- Commits & undo ----
|
|
32
|
+
- action: Amend the last commit (edit message and/or fold in staged changes)
|
|
33
|
+
command: git commit --amend
|
|
34
|
+
notes: Rewrites the most recent commit. Force-push needed if already pushed.
|
|
35
|
+
tags: [amend, commit, edit, message]
|
|
36
|
+
|
|
37
|
+
- action: Amend the last commit without changing its message
|
|
38
|
+
command: git commit --amend --no-edit
|
|
39
|
+
notes: Quietly folds new staged changes into the most recent commit.
|
|
40
|
+
tags: [amend, fixup, no-edit, commit]
|
|
41
|
+
|
|
42
|
+
- action: Unstage a file but keep the working-copy change
|
|
43
|
+
command: git restore --staged <file>
|
|
44
|
+
notes: Legacy synonym is `git reset HEAD <file>`.
|
|
45
|
+
tags: [unstage, reset, restore]
|
|
46
|
+
|
|
47
|
+
- action: Discard local changes to a file (destructive)
|
|
48
|
+
command: git restore <file>
|
|
49
|
+
notes: The file's uncommitted edits are gone. Legacy `git checkout -- <file>`.
|
|
50
|
+
tags: [discard, undo, restore, dangerous]
|
|
51
|
+
|
|
52
|
+
- action: Undo the last commit but keep its changes staged
|
|
53
|
+
command: git reset --soft HEAD~1
|
|
54
|
+
tags: [undo, reset, soft, uncommit]
|
|
55
|
+
|
|
56
|
+
- action: Hard-reset to a specific commit (destructive)
|
|
57
|
+
command: git reset --hard <ref>
|
|
58
|
+
notes: >-
|
|
59
|
+
Wipes working tree AND index. If you regret it, find the pre-reset HEAD in
|
|
60
|
+
`git reflog` and `git reset --hard <sha>` to recover (within 90 days).
|
|
61
|
+
tags: [reset, hard, dangerous, undo]
|
|
62
|
+
|
|
63
|
+
- action: Set the per-repo author email (useful for work/personal split)
|
|
64
|
+
command: git config user.email "<address>"
|
|
65
|
+
notes: Drop --global to scope to the current repo only. Pair with `git config user.name`.
|
|
66
|
+
tags: [config, email, identity, author]
|
|
67
|
+
|
|
68
|
+
# ---- Stash ----
|
|
69
|
+
- action: Stash all changes including untracked files
|
|
70
|
+
command: git stash -u
|
|
71
|
+
notes: -u (--include-untracked) saves new files too; without it, untracked stays put.
|
|
72
|
+
tags: [stash, save, wip, untracked]
|
|
73
|
+
|
|
74
|
+
- action: Stash with a message
|
|
75
|
+
command: git stash push -m "<message>"
|
|
76
|
+
tags: [stash, message, save]
|
|
77
|
+
|
|
78
|
+
- action: List all stashes
|
|
79
|
+
command: git stash list
|
|
80
|
+
tags: [stash, list]
|
|
81
|
+
|
|
82
|
+
- action: Apply and drop the most recent stash
|
|
83
|
+
command: git stash pop
|
|
84
|
+
notes: Use `git stash apply` to keep the stash on the list after applying.
|
|
85
|
+
tags: [stash, pop, apply, restore]
|
|
86
|
+
|
|
87
|
+
# ---- Log & inspect ----
|
|
88
|
+
- action: Pretty one-line log with a branch graph
|
|
89
|
+
command: git log --oneline --graph --all --decorate
|
|
90
|
+
notes: Add `--first-parent` to flatten merge-heavy branches.
|
|
91
|
+
tags: [log, history, graph, decorate]
|
|
92
|
+
|
|
93
|
+
- action: Show what changed in the last commit
|
|
94
|
+
command: git show
|
|
95
|
+
notes: Append a sha or ref (e.g. `git show HEAD~2`) to inspect any commit.
|
|
96
|
+
tags: [show, diff, last, commit]
|
|
97
|
+
|
|
98
|
+
- action: Diff staged vs unstaged changes
|
|
99
|
+
steps:
|
|
100
|
+
- "`git diff` shows unstaged changes (working tree vs index)"
|
|
101
|
+
- "`git diff --staged` shows staged changes (index vs HEAD); --cached is the same"
|
|
102
|
+
notes: Add `-- <path>` to either to scope to a file.
|
|
103
|
+
tags: [diff, staged, cached, unstaged]
|
|
104
|
+
|
|
105
|
+
- action: Blame a file showing commit and author per line
|
|
106
|
+
command: git blame <file>
|
|
107
|
+
notes: Add `-L 10,20` to limit to a line range; `-w` ignores whitespace-only changes.
|
|
108
|
+
tags: [blame, who, author, line]
|
|
109
|
+
|
|
110
|
+
- action: Find which commit introduced a string (pickaxe search)
|
|
111
|
+
command: git log -S "<text>" --source --all
|
|
112
|
+
notes: -S finds commits that change the literal-string count; use -G for a regex.
|
|
113
|
+
tags: [search, pickaxe, history, find, who]
|
|
114
|
+
|
|
115
|
+
# ---- Remote ----
|
|
116
|
+
- action: Force-push safely (refuses to overwrite remote work you haven't seen)
|
|
117
|
+
command: git push --force-with-lease
|
|
118
|
+
notes: >-
|
|
119
|
+
Prefer this over plain --force. It errors if the remote moved since your
|
|
120
|
+
last fetch, preventing accidental overwrites of teammates' commits.
|
|
121
|
+
tags: [push, force, force-with-lease, safe]
|
|
122
|
+
|
|
123
|
+
- action: Fetch all remotes and prune deleted upstream branches
|
|
124
|
+
command: git fetch --all --prune
|
|
125
|
+
tags: [fetch, prune, cleanup, remote]
|
|
126
|
+
|
|
127
|
+
- action: Pull with rebase (replay your local commits on top of upstream)
|
|
128
|
+
command: git pull --rebase
|
|
129
|
+
notes: Set `git config --global pull.rebase true` to make this the default.
|
|
130
|
+
tags: [pull, rebase, update]
|
|
131
|
+
|
|
132
|
+
- action: Push the current branch and set its upstream
|
|
133
|
+
command: git push -u origin HEAD
|
|
134
|
+
notes: After this, plain `git push` / `git pull` work without arguments on this branch.
|
|
135
|
+
tags: [push, upstream, set-upstream]
|
|
136
|
+
|
|
137
|
+
# ---- Rebase, merge, conflicts ----
|
|
138
|
+
- action: Interactive rebase the last N commits
|
|
139
|
+
command: git rebase -i HEAD~<N>
|
|
140
|
+
notes: >-
|
|
141
|
+
In the editor: change `pick` to `reword` (edit message), `squash` (fold into
|
|
142
|
+
previous, keep both messages), `fixup` (squash and drop message), `drop`
|
|
143
|
+
(remove the commit), or reorder lines. Resume with `git rebase --continue`.
|
|
144
|
+
tags: [rebase, interactive, squash, reword, fixup]
|
|
145
|
+
|
|
146
|
+
- action: Resolve a merge conflict
|
|
147
|
+
command: git merge --continue
|
|
148
|
+
steps:
|
|
149
|
+
- "`git status` lists the conflicting files (marked \"both modified\")"
|
|
150
|
+
- Open each file; replace the `<<<<<<<` / `=======` / `>>>>>>>` blocks with the desired content
|
|
151
|
+
- "`git add <files>` to mark each resolved file"
|
|
152
|
+
- "`git commit` (or `git merge --continue`) to finish the merge"
|
|
153
|
+
notes: >-
|
|
154
|
+
Bail out with `git merge --abort` (restores pre-merge state). During a
|
|
155
|
+
rebase, use `git rebase --continue` / `--abort` instead.
|
|
156
|
+
tags: [merge, conflict, resolve, fix]
|
|
157
|
+
|
|
158
|
+
- action: Take "ours" or "theirs" wholesale during a conflict
|
|
159
|
+
steps:
|
|
160
|
+
- "`git checkout --ours <file>` keeps the version from your current branch"
|
|
161
|
+
- "`git checkout --theirs <file>` takes the version from the other branch"
|
|
162
|
+
- "`git add <file>` then `git commit` (or `--continue`)"
|
|
163
|
+
notes: >-
|
|
164
|
+
During a rebase, ours/theirs are FLIPPED — rebase replays your commits onto
|
|
165
|
+
theirs, so "ours" refers to the branch you're rebasing onto.
|
|
166
|
+
tags: [conflict, ours, theirs, merge, rebase]
|
|
167
|
+
|
|
168
|
+
- action: Abort a merge in progress
|
|
169
|
+
command: git merge --abort
|
|
170
|
+
notes: Use `git rebase --abort` if you're mid-rebase instead.
|
|
171
|
+
tags: [merge, abort, cancel]
|
|
172
|
+
|
|
173
|
+
- action: Cherry-pick a commit onto the current branch
|
|
174
|
+
command: git cherry-pick <sha>
|
|
175
|
+
notes: Use a range like `<a>^..<b>` to pick a series in order.
|
|
176
|
+
tags: [cherry-pick, copy, apply]
|
|
177
|
+
|
|
178
|
+
- action: Safely undo a commit by adding an inverse commit
|
|
179
|
+
command: git revert <sha>
|
|
180
|
+
notes: Non-destructive — creates a new commit that undoes the target. Safe on shared branches.
|
|
181
|
+
tags: [revert, undo, safe, inverse]
|
|
182
|
+
|
|
183
|
+
# ---- History rewriting ----
|
|
184
|
+
- action: Remove a co-author trailer from every commit (cleanup after Claude etc.)
|
|
185
|
+
command: |
|
|
186
|
+
git filter-repo --message-callback '
|
|
187
|
+
import re
|
|
188
|
+
return re.sub(rb"^Co-authored-by:.*\n?", b"", message, flags=re.MULTILINE)
|
|
189
|
+
'
|
|
190
|
+
steps:
|
|
191
|
+
- "Install once: `brew install git-filter-repo`"
|
|
192
|
+
- Ensure the working tree is clean (no staged or unstaged changes)
|
|
193
|
+
- Run the filter-repo command (Enter copies it from the preview)
|
|
194
|
+
- "Re-add the remote (filter-repo strips it for safety): `git remote add origin <url>`"
|
|
195
|
+
- "Force-push: `git push --force-with-lease --all && git push --force-with-lease --tags`"
|
|
196
|
+
notes: >-
|
|
197
|
+
Destructive — every commit SHA changes. Coordinate with anyone who has
|
|
198
|
+
cloned the repo so they re-clone or hard-reset their local copy.
|
|
199
|
+
tags: [filter-repo, rewrite, co-author, cleanup, claude, history]
|
|
200
|
+
|
|
201
|
+
- action: Squash the last N commits into one
|
|
202
|
+
command: git rebase -i HEAD~<N>
|
|
203
|
+
steps:
|
|
204
|
+
- "`git rebase -i HEAD~<N>`"
|
|
205
|
+
- In the editor, change `pick` to `squash` on every line EXCEPT the first
|
|
206
|
+
- Save and quit; the next editor lets you craft the combined commit message
|
|
207
|
+
notes: Use `fixup` instead of `squash` to keep only the first commit's message.
|
|
208
|
+
tags: [squash, rebase, combine, history]
|
|
209
|
+
|
|
210
|
+
- action: Edit the message of an old commit
|
|
211
|
+
steps:
|
|
212
|
+
- "`git rebase -i HEAD~<N>` where N is far enough back to include the target"
|
|
213
|
+
- "Change `pick` to `reword` on the target line; save and quit"
|
|
214
|
+
- Edit the message in the next editor; save and quit
|
|
215
|
+
- "`git rebase --continue` if anything else needs attention"
|
|
216
|
+
command: git rebase -i HEAD~<N>
|
|
217
|
+
notes: SHAs from that commit onward change; force-push needed if already pushed.
|
|
218
|
+
tags: [rebase, reword, message, history]
|
|
219
|
+
|
|
220
|
+
- action: Drop a commit from history
|
|
221
|
+
steps:
|
|
222
|
+
- "`git rebase -i HEAD~<N>`"
|
|
223
|
+
- Delete the line (or change `pick` to `drop`) for the commit to remove
|
|
224
|
+
- Save and quit; resolve any conflicts with `--continue`
|
|
225
|
+
command: git rebase -i HEAD~<N>
|
|
226
|
+
notes: Prefer `git revert <sha>` if the commit is already shared with others.
|
|
227
|
+
tags: [rebase, drop, remove, history]
|
|
228
|
+
|
|
229
|
+
# ---- Recovery & tags ----
|
|
230
|
+
- action: View the reflog to find lost commits
|
|
231
|
+
command: git reflog
|
|
232
|
+
notes: >-
|
|
233
|
+
Every HEAD-changing operation leaves an entry. Recover with `git reset
|
|
234
|
+
--hard <sha>` or `git checkout <sha>`. Entries expire after 90 days by default.
|
|
235
|
+
tags: [reflog, recover, lost, history, undo]
|
|
236
|
+
|
|
237
|
+
- action: Recover after a bad reset or rebase
|
|
238
|
+
steps:
|
|
239
|
+
- "`git reflog` to find the entry just BEFORE the bad operation"
|
|
240
|
+
- "Note the sha or HEAD@{N} reference"
|
|
241
|
+
- "`git reset --hard HEAD@{N}` (or `git reset --hard <sha>`)"
|
|
242
|
+
command: git reflog
|
|
243
|
+
notes: Reflog entries expire after 90 days by default — act soon.
|
|
244
|
+
tags: [reflog, recover, undo, mistake, rescue]
|
|
245
|
+
|
|
246
|
+
- action: Find the commit that broke things (git bisect)
|
|
247
|
+
steps:
|
|
248
|
+
- "`git bisect start`"
|
|
249
|
+
- "`git bisect bad` to mark HEAD as bad"
|
|
250
|
+
- "`git bisect good <ref>` to mark a known-good past commit or tag"
|
|
251
|
+
- Test at each step; mark with `git bisect good` or `git bisect bad`
|
|
252
|
+
- "`git bisect reset` to return to where you started"
|
|
253
|
+
command: git bisect start
|
|
254
|
+
notes: Automate with `git bisect run <script>` for hands-off bisection.
|
|
255
|
+
tags: [bisect, regression, find, debug, broke]
|
|
256
|
+
|
|
257
|
+
- action: Create an annotated tag
|
|
258
|
+
command: git tag -a v<version> -m "<message>"
|
|
259
|
+
notes: Annotated tags carry author + date + message; prefer them over lightweight `git tag <name>`.
|
|
260
|
+
tags: [tag, release, annotated, version]
|
|
261
|
+
|
|
262
|
+
- action: Push a specific tag to the remote
|
|
263
|
+
command: git push origin v<version>
|
|
264
|
+
notes: Use `git push --tags` to push all local tags at once.
|
|
265
|
+
tags: [tag, push, release, remote]
|
|
266
|
+
|
|
267
|
+
- action: Delete a tag locally and on the remote
|
|
268
|
+
steps:
|
|
269
|
+
- "`git tag -d v<version>` removes it locally"
|
|
270
|
+
- "`git push origin --delete v<version>` removes it from the remote"
|
|
271
|
+
command: git push origin --delete v<version>
|
|
272
|
+
tags: [tag, delete, remove, remote]
|
package/seed/vim.yaml
CHANGED
|
@@ -64,3 +64,115 @@ entries:
|
|
|
64
64
|
- action: Indent / outdent the line or selection
|
|
65
65
|
keys: ">>, <<"
|
|
66
66
|
tags: [indent]
|
|
67
|
+
- action: Move to end of word / end of WORD
|
|
68
|
+
keys: "e, E"
|
|
69
|
+
notes: e treats punctuation as a word boundary; E spans punctuation (whitespace-separated WORD).
|
|
70
|
+
tags: [word, motion, end]
|
|
71
|
+
- action: Move forward / back by WORD (whitespace-separated)
|
|
72
|
+
keys: "W, B"
|
|
73
|
+
notes: Capital variants of w / b — jump past punctuation in one step.
|
|
74
|
+
tags: [word, motion, big]
|
|
75
|
+
- action: Find a character on the current line
|
|
76
|
+
keys: "f, F, t, T"
|
|
77
|
+
notes: >-
|
|
78
|
+
After pressing, type the target character. f / F land ON it (forward / back);
|
|
79
|
+
t / T land just before it. ; repeats the same find, , reverses direction.
|
|
80
|
+
Searches like "fa" (find 'a') are great when paired with d / c (e.g. dt, deletes to before next ',').
|
|
81
|
+
tags: [find, char, line, motion, tip]
|
|
82
|
+
- action: Search forward / back for the word under the cursor
|
|
83
|
+
keys: "*, #"
|
|
84
|
+
notes: Use n / N afterwards to step through matches. Great for tracing usages.
|
|
85
|
+
tags: [search, word, find, navigate, tip]
|
|
86
|
+
- action: Jump to top / middle / bottom of the screen
|
|
87
|
+
keys: "H, M, L"
|
|
88
|
+
notes: Cursor only — doesn't scroll the viewport. Combine with zz to recenter after.
|
|
89
|
+
tags: [screen, viewport, motion]
|
|
90
|
+
- action: Scroll half / full page down / up
|
|
91
|
+
keys: "⌃D, ⌃U, ⌃F, ⌃B"
|
|
92
|
+
notes: Half-page (⌃D / ⌃U) is usually less jarring than full-page (⌃F / ⌃B).
|
|
93
|
+
tags: [scroll, page, motion]
|
|
94
|
+
- action: Recenter / top / bottom the current line in the viewport
|
|
95
|
+
keys: "zz, zt, zb"
|
|
96
|
+
notes: zz centers the current line, zt puts it at the top of the window, zb at the bottom.
|
|
97
|
+
tags: [scroll, center, recenter, viewport]
|
|
98
|
+
- action: Jump back / forward in the cursor history
|
|
99
|
+
keys: "⌃O, ⌃I"
|
|
100
|
+
notes: >-
|
|
101
|
+
Vim records every jump >1 line. ⌃O retraces backwards through your trail, ⌃I goes forward.
|
|
102
|
+
Indispensable for diving into code and bouncing back where you were.
|
|
103
|
+
tags: [jump, history, navigate, tip, productivity]
|
|
104
|
+
- action: Jump to local declaration / global definition under cursor
|
|
105
|
+
keys: "gd, gD"
|
|
106
|
+
notes: gd searches the current function/scope for the identifier under the cursor; gD searches the whole file.
|
|
107
|
+
tags: [definition, declaration, goto, tip]
|
|
108
|
+
- action: Open a new line below / above the current
|
|
109
|
+
keys: "o, O"
|
|
110
|
+
notes: Both enter Insert mode immediately on the new line.
|
|
111
|
+
tags: [insert, new line, edit]
|
|
112
|
+
- action: Insert at start / append at end of line
|
|
113
|
+
keys: "I, A"
|
|
114
|
+
notes: I jumps to the first non-blank character and enters Insert; A appends after the last character.
|
|
115
|
+
tags: [insert, append, line, edit]
|
|
116
|
+
- action: Change / delete to end of line
|
|
117
|
+
keys: "C, D"
|
|
118
|
+
notes: C is shorthand for c$ (change to end of line); D is d$ (delete to end). Quick rewrites.
|
|
119
|
+
tags: [change, delete, line, edit]
|
|
120
|
+
- action: Replace a single character / enter Replace mode
|
|
121
|
+
keys: "r, R"
|
|
122
|
+
notes: After r, type the replacement char (one key, doesn't enter Insert). R overwrites characters as you type until ⎋.
|
|
123
|
+
tags: [replace, overwrite, edit]
|
|
124
|
+
- action: Join the next line onto the current one
|
|
125
|
+
keys: "J"
|
|
126
|
+
notes: Adds a space between them. gJ joins without inserting a space.
|
|
127
|
+
tags: [join, line, edit]
|
|
128
|
+
- action: Toggle case of character / inner word
|
|
129
|
+
keys: "~, g~iw"
|
|
130
|
+
notes: ~ flips one char's case; g~iw flips the word under the cursor. gu / gU make a motion lower / upper-case.
|
|
131
|
+
tags: [case, toggle, edit]
|
|
132
|
+
- action: Split the window horizontally / vertically
|
|
133
|
+
keys: ":split⏎, :vsplit⏎"
|
|
134
|
+
notes: Short forms :sp and :vsp. After splitting, :e <file> opens a file in the focused split.
|
|
135
|
+
tags: [split, window, layout]
|
|
136
|
+
- action: Move focus between splits
|
|
137
|
+
keys: "⌃Wh, ⌃Wj, ⌃Wk, ⌃Wl"
|
|
138
|
+
notes: ⌃W followed by an hjkl direction. ⌃W⌃W cycles forward through splits.
|
|
139
|
+
tags: [split, window, navigate]
|
|
140
|
+
- action: List and switch buffers
|
|
141
|
+
keys: ":ls⏎, :b <n>⏎, :bn, :bp"
|
|
142
|
+
notes: :ls lists all open buffers with their numbers. :bn / :bp cycle next / previous.
|
|
143
|
+
tags: [buffer, switch, list, tip]
|
|
144
|
+
- action: Close the current buffer (keep the split layout)
|
|
145
|
+
keys: ":bd⏎"
|
|
146
|
+
notes: Drops the buffer but keeps the window. Add ! to discard unsaved changes.
|
|
147
|
+
tags: [buffer, close]
|
|
148
|
+
- action: Set a mark / jump back to it
|
|
149
|
+
keys: "m, ', `"
|
|
150
|
+
notes: >-
|
|
151
|
+
m{letter} sets a mark at the cursor; '{letter} jumps to its line, `{letter} (backtick) to the exact column.
|
|
152
|
+
Lowercase marks a–z are file-local; capital A–Z are global across files.
|
|
153
|
+
tags: [mark, jump, bookmark, "ma", "'a", tip]
|
|
154
|
+
- action: Record / play back a macro
|
|
155
|
+
keys: "q...q, @, @@"
|
|
156
|
+
notes: >-
|
|
157
|
+
q{letter} starts recording into a register; q stops. @{letter} replays. @@ re-runs the last macro.
|
|
158
|
+
Workflow — record one edit on the first item, then @a (or whatever register) on each subsequent occurrence.
|
|
159
|
+
tags: [macro, record, replay, automation, tip, productivity]
|
|
160
|
+
- action: Repeat the last ex (:) command
|
|
161
|
+
keys: "@:"
|
|
162
|
+
notes: "After running any :command once, @: replays it; @@ then keeps replaying."
|
|
163
|
+
tags: [repeat, command, tip]
|
|
164
|
+
- action: Toggle relative / absolute line numbers
|
|
165
|
+
keys: ":set rnu⏎, :set nornu⏎, :set nu⏎"
|
|
166
|
+
notes: >-
|
|
167
|
+
Relative numbers (rnu) show each line's distance from the cursor — "5j" or "12dd" become trivial to count.
|
|
168
|
+
Combine `set nu` + `set rnu` for a hybrid view. Put both in your vimrc to make it permanent.
|
|
169
|
+
tags: [option, line numbers, relative, productivity, tip]
|
|
170
|
+
- action: Insert the same text on every line of a column selection
|
|
171
|
+
steps:
|
|
172
|
+
- "⌃V to enter Visual-Block mode"
|
|
173
|
+
- Use j / k to extend the block over the lines you want
|
|
174
|
+
- Press I (capital) to insert at the LEFT edge of the block
|
|
175
|
+
- Type the text you want repeated on every line
|
|
176
|
+
- "Press ⎋ — wait a moment — the text duplicates onto every selected line"
|
|
177
|
+
notes: A (capital) does the same at the RIGHT edge. Game-changer for adding comment markers, trailing commas, or any column prefix/suffix.
|
|
178
|
+
tags: [visual block, multi line, insert, comment, prefix, tip, productivity]
|