skillwiki 0.9.63 → 0.10.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/dist/chunk-C5OLZRRM.js +357 -0
- package/dist/chunk-IZABIE44.js +647 -0
- package/dist/chunk-R6BKJWVC.js +890 -0
- package/dist/chunk-SCGC7YNM.js +213 -0
- package/dist/{chunk-TUFQZ5K4.js → chunk-XSTXPA34.js} +834 -1981
- package/dist/cli.js +1780 -697
- package/dist/index-projection-ERFX76U5.js +10 -0
- package/dist/managed-write-preflight-PW4OOOMV.js +11 -0
- package/dist/skillwiki-mcp.js +4 -1
- package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
- package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
- package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
- package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
- package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
- package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
- package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
- package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
- package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
- package/dist/vault-sync/scripts/lib/platform.sh +184 -0
- package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
- package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
- package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
- package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
- package/dist/vault-sync/scripts/wiki-push.sh +364 -0
- package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
- package/package.json +2 -2
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/README.md +13 -0
- package/skills/package.json +1 -1
- package/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/using-skillwiki/SKILL.md +24 -0
- package/skills/skills/wiki-crystallize/SKILL.md +3 -0
- package/skills/using-skillwiki/SKILL.md +24 -0
- package/skills/wiki-crystallize/SKILL.md +3 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# git-operation-journal.sh — helper-owned vault-sync operation journal.
|
|
3
|
+
# Bash 3.2 compatible. Source from vault-sync scripts.
|
|
4
|
+
|
|
5
|
+
vault_sync_op_journal_dir() {
|
|
6
|
+
local repo="${1:-.}"
|
|
7
|
+
local path
|
|
8
|
+
path="$(git -C "$repo" rev-parse --git-path vault-sync/operations 2>/dev/null)" || return 1
|
|
9
|
+
case "$path" in
|
|
10
|
+
/*) printf '%s\n' "$path" ;;
|
|
11
|
+
*) printf '%s\n' "$repo/$path" ;;
|
|
12
|
+
esac
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
vault_sync_op_journal_path() {
|
|
16
|
+
local repo="$1" op_id="$2"
|
|
17
|
+
local dir
|
|
18
|
+
dir="$(vault_sync_op_journal_dir "$repo")" || return 1
|
|
19
|
+
printf '%s/%s.env\n' "$dir" "$op_id"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
vault_sync_op_get_field() {
|
|
23
|
+
local repo="$1" op_id="$2" key="$3"
|
|
24
|
+
local file
|
|
25
|
+
file="$(vault_sync_op_journal_path "$repo" "$op_id")" || return 1
|
|
26
|
+
[ -f "$file" ] || return 1
|
|
27
|
+
# Line-oriented key=value; print value after first '=' (values may contain '=').
|
|
28
|
+
awk -F= -v k="$key" '$1==k {print substr($0, index($0,"=")+1); exit}' "$file"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
vault_sync_op_set_field() {
|
|
32
|
+
local repo="$1" op_id="$2" key="$3" value="$4"
|
|
33
|
+
vault_sync_op_set_fields "$repo" "$op_id" "$key" "$value"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Batch journal field updates in one rewrite.
|
|
37
|
+
# Usage: vault_sync_op_set_fields <repo> <op_id> <key> <value> [<key> <value> ...]
|
|
38
|
+
vault_sync_op_set_fields() {
|
|
39
|
+
local repo="$1" op_id="$2"
|
|
40
|
+
shift 2
|
|
41
|
+
local file tmp
|
|
42
|
+
file="$(vault_sync_op_journal_path "$repo" "$op_id")" || return 1
|
|
43
|
+
tmp="$(mktemp)" || return 1
|
|
44
|
+
if [ -f "$file" ]; then
|
|
45
|
+
cp "$file" "$tmp" || { rm -f "$tmp"; return 1; }
|
|
46
|
+
else
|
|
47
|
+
: >"$tmp"
|
|
48
|
+
fi
|
|
49
|
+
while [ "$#" -ge 2 ]; do
|
|
50
|
+
awk -F= -v k="$1" -v v="$2" '
|
|
51
|
+
BEGIN{found=0}
|
|
52
|
+
$1==k {print k"="v; found=1; next}
|
|
53
|
+
{print}
|
|
54
|
+
END{if(!found) print k"="v}
|
|
55
|
+
' "$tmp" >"${tmp}.new" || { rm -f "$tmp" "${tmp}.new"; return 1; }
|
|
56
|
+
mv "${tmp}.new" "$tmp"
|
|
57
|
+
shift 2
|
|
58
|
+
done
|
|
59
|
+
mv "$tmp" "$file"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
vault_sync_op_set_phase() {
|
|
63
|
+
vault_sync_op_set_field "$1" "$2" "phase" "$3"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
vault_sync_op_create_recovery_refs() {
|
|
67
|
+
local repo="$1" op_id="$2" original_head="$3" target_oid="$4"
|
|
68
|
+
|
|
69
|
+
# Prefer transactional create (CAS: fail if ref already exists).
|
|
70
|
+
if git -C "$repo" update-ref --stdin --create-reflog -m "vault-sync operation begin" >/dev/null 2>&1 <<EOF
|
|
71
|
+
start
|
|
72
|
+
create refs/vault-sync/recovery/${op_id}/original-head ${original_head}
|
|
73
|
+
create refs/vault-sync/recovery/${op_id}/target ${target_oid}
|
|
74
|
+
prepare
|
|
75
|
+
commit
|
|
76
|
+
EOF
|
|
77
|
+
then
|
|
78
|
+
return 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Fallback: create-only update-ref (empty oldoid means must not exist).
|
|
82
|
+
if ! git -C "$repo" update-ref \
|
|
83
|
+
"refs/vault-sync/recovery/${op_id}/original-head" "$original_head" "" 2>/dev/null; then
|
|
84
|
+
return 1
|
|
85
|
+
fi
|
|
86
|
+
if ! git -C "$repo" update-ref \
|
|
87
|
+
"refs/vault-sync/recovery/${op_id}/target" "$target_oid" "" 2>/dev/null; then
|
|
88
|
+
# Best-effort rollback of the first ref if second create fails.
|
|
89
|
+
git -C "$repo" update-ref -d "refs/vault-sync/recovery/${op_id}/original-head" 2>/dev/null || true
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# CAS-update the recovery target ref: only succeeds if current value is expected_old.
|
|
96
|
+
vault_sync_op_cas_recovery_target() {
|
|
97
|
+
local repo="$1" op_id="$2" new_oid="$3" expected_old="$4"
|
|
98
|
+
if git -C "$repo" update-ref \
|
|
99
|
+
"refs/vault-sync/recovery/${op_id}/target" "$new_oid" "$expected_old" 2>/dev/null; then
|
|
100
|
+
return 0
|
|
101
|
+
fi
|
|
102
|
+
return 1
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
vault_sync_op_begin() {
|
|
107
|
+
local repo="$1" op_id="$2" branch="$3" original_head="$4" target_oid="$5"
|
|
108
|
+
local lock_identity="$6" helper_version="$7" runtime_hash="$8"
|
|
109
|
+
local dir file worktree_path worktree_git_dir
|
|
110
|
+
dir="$(vault_sync_op_journal_dir "$repo")" || return 1
|
|
111
|
+
mkdir -p "$dir" || return 1
|
|
112
|
+
file="$(vault_sync_op_journal_path "$repo" "$op_id")" || return 1
|
|
113
|
+
if [ -f "$file" ]; then
|
|
114
|
+
return 1
|
|
115
|
+
fi
|
|
116
|
+
if ! vault_sync_op_create_recovery_refs "$repo" "$op_id" "$original_head" "$target_oid"; then
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
worktree_path="$(cd "$repo" && pwd -P)" || return 1
|
|
120
|
+
worktree_git_dir="$(git -C "$repo" rev-parse --absolute-git-dir 2>/dev/null)" || return 1
|
|
121
|
+
cat >"$file" <<EOF
|
|
122
|
+
operation_id=${op_id}
|
|
123
|
+
phase=prepared
|
|
124
|
+
retry_count=0
|
|
125
|
+
original_branch=${branch}
|
|
126
|
+
original_head=${original_head}
|
|
127
|
+
target_oid=${target_oid}
|
|
128
|
+
owned_stash_oid=
|
|
129
|
+
preservation_scope=none
|
|
130
|
+
lock_identity=${lock_identity}
|
|
131
|
+
helper_version=${helper_version}
|
|
132
|
+
deployed_runtime_hash=${runtime_hash}
|
|
133
|
+
conflict_identity=
|
|
134
|
+
handoff=0
|
|
135
|
+
reason=
|
|
136
|
+
worktree_path=${worktree_path}
|
|
137
|
+
worktree_git_dir=${worktree_git_dir}
|
|
138
|
+
EOF
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
vault_sync_op_find_review_required() {
|
|
143
|
+
local repo="$1" current_git_dir jdir jf op_id phase handoff journal_git_dir
|
|
144
|
+
current_git_dir="$(git -C "$repo" rev-parse --absolute-git-dir 2>/dev/null)" || return 1
|
|
145
|
+
jdir="$(vault_sync_op_journal_dir "$repo")" || return 1
|
|
146
|
+
[ -d "$jdir" ] || return 1
|
|
147
|
+
|
|
148
|
+
for jf in "$jdir"/*.env; do
|
|
149
|
+
[ -f "$jf" ] || continue
|
|
150
|
+
op_id="$(basename "$jf" .env)"
|
|
151
|
+
phase="$(vault_sync_op_get_field "$repo" "$op_id" phase 2>/dev/null || true)"
|
|
152
|
+
handoff="$(vault_sync_op_get_field "$repo" "$op_id" handoff 2>/dev/null || true)"
|
|
153
|
+
[ "$phase" = "review-required" ] && [ "$handoff" = "1" ] || continue
|
|
154
|
+
journal_git_dir="$(vault_sync_op_get_field "$repo" "$op_id" worktree_git_dir 2>/dev/null || true)"
|
|
155
|
+
if [ -z "$journal_git_dir" ] || [ "$journal_git_dir" = "$current_git_dir" ]; then
|
|
156
|
+
printf '%s\n' "$op_id"
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
done
|
|
160
|
+
return 1
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
vault_sync_op_preflight_blocker() {
|
|
164
|
+
local repo="$1" unmerged op_id git_dir
|
|
165
|
+
unmerged="$(git -C "$repo" diff --name-only --diff-filter=U 2>/dev/null || true)"
|
|
166
|
+
if [ -n "$unmerged" ]; then
|
|
167
|
+
op_id="$(vault_sync_op_find_review_required "$repo" 2>/dev/null || true)"
|
|
168
|
+
printf 'unmerged-paths\t%s\n' "$op_id"
|
|
169
|
+
return 0
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
git_dir="$(git -C "$repo" rev-parse --absolute-git-dir 2>/dev/null)" || return 1
|
|
173
|
+
if [ -f "$git_dir/MERGE_HEAD" ] || [ -f "$git_dir/CHERRY_PICK_HEAD" ] || [ -f "$git_dir/REVERT_HEAD" ]; then
|
|
174
|
+
printf 'git-operation-in-progress\t\n'
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
op_id="$(vault_sync_op_find_review_required "$repo" 2>/dev/null || true)"
|
|
179
|
+
if [ -n "$op_id" ]; then
|
|
180
|
+
printf 'review-required\t%s\n' "$op_id"
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
return 1
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
vault_sync_op_record_stash() {
|
|
187
|
+
vault_sync_op_set_fields "$1" "$2" \
|
|
188
|
+
"owned_stash_oid" "$3" \
|
|
189
|
+
"preservation_scope" "$4"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
vault_sync_op_record_inventory() {
|
|
193
|
+
local repo="$1" op_id="$2" inv="$3"
|
|
194
|
+
local dir dest
|
|
195
|
+
dir="$(vault_sync_op_journal_dir "$repo")" || return 1
|
|
196
|
+
dest="$dir/${op_id}.inventory"
|
|
197
|
+
cp "$inv" "$dest" || return 1
|
|
198
|
+
vault_sync_op_set_field "$repo" "$op_id" "inventory_path" "$dest"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
vault_sync_op_fingerprint_repo_state() {
|
|
202
|
+
local repo="$1"
|
|
203
|
+
local git_dir rebase_dir onto orig stopped head_name rebase_head index_tree porcelain
|
|
204
|
+
local unmerged_paths unmerged_content path blob
|
|
205
|
+
|
|
206
|
+
git_dir="$(git -C "$repo" rev-parse --git-dir)" || return 1
|
|
207
|
+
case "$git_dir" in
|
|
208
|
+
/*) ;;
|
|
209
|
+
*) git_dir="$repo/$git_dir" ;;
|
|
210
|
+
esac
|
|
211
|
+
|
|
212
|
+
rebase_dir=""
|
|
213
|
+
if [ -d "$git_dir/rebase-merge" ]; then
|
|
214
|
+
rebase_dir="$git_dir/rebase-merge"
|
|
215
|
+
elif [ -d "$git_dir/rebase-apply" ]; then
|
|
216
|
+
rebase_dir="$git_dir/rebase-apply"
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
onto=""
|
|
220
|
+
orig=""
|
|
221
|
+
stopped=""
|
|
222
|
+
head_name=""
|
|
223
|
+
if [ -n "$rebase_dir" ]; then
|
|
224
|
+
[ -f "$rebase_dir/onto" ] && onto="$(tr -d '[:space:]' <"$rebase_dir/onto")"
|
|
225
|
+
if [ -f "$rebase_dir/orig-head" ]; then
|
|
226
|
+
orig="$(tr -d '[:space:]' <"$rebase_dir/orig-head")"
|
|
227
|
+
elif [ -f "$rebase_dir/orig_head" ]; then
|
|
228
|
+
orig="$(tr -d '[:space:]' <"$rebase_dir/orig_head")"
|
|
229
|
+
fi
|
|
230
|
+
[ -f "$rebase_dir/stopped-sha" ] && stopped="$(tr -d '[:space:]' <"$rebase_dir/stopped-sha")"
|
|
231
|
+
[ -f "$rebase_dir/head-name" ] && head_name="$(tr -d '[:space:]' <"$rebase_dir/head-name")"
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
rebase_head="$(git -C "$repo" rev-parse -q --verify REBASE_HEAD 2>/dev/null || true)"
|
|
235
|
+
index_tree="$(git -C "$repo" write-tree 2>/dev/null || echo none)"
|
|
236
|
+
porcelain="$(git -C "$repo" status --porcelain=v1 --untracked-files=all 2>/dev/null | shasum -a 256 | awk '{print $1}')"
|
|
237
|
+
|
|
238
|
+
# Include worktree content of unmerged paths so human edits to conflicted
|
|
239
|
+
# files change the fingerprint (porcelain alone does not hash file bodies).
|
|
240
|
+
unmerged_paths="$(git -C "$repo" diff --name-only --diff-filter=U 2>/dev/null || true)"
|
|
241
|
+
unmerged_content=""
|
|
242
|
+
if [ -n "$unmerged_paths" ]; then
|
|
243
|
+
while IFS= read -r path; do
|
|
244
|
+
[ -n "$path" ] || continue
|
|
245
|
+
if [ -f "$repo/$path" ]; then
|
|
246
|
+
# hash-object needs a filesystem path; -C does not re-root relative paths.
|
|
247
|
+
blob="$(git -C "$repo" hash-object -- "$repo/$path" 2>/dev/null || echo missing)"
|
|
248
|
+
else
|
|
249
|
+
blob="missing"
|
|
250
|
+
fi
|
|
251
|
+
unmerged_content="${unmerged_content}${path}=${blob};"
|
|
252
|
+
done <<EOF
|
|
253
|
+
$unmerged_paths
|
|
254
|
+
EOF
|
|
255
|
+
fi
|
|
256
|
+
unmerged_content="$(printf '%s' "$unmerged_content" | shasum -a 256 | awk '{print $1}')"
|
|
257
|
+
|
|
258
|
+
printf 'onto=%s;orig=%s;stopped=%s;head_name=%s;rebase_head=%s;index_tree=%s;porcelain=%s;unmerged=%s\n' \
|
|
259
|
+
"$onto" "$orig" "$stopped" "$head_name" "$rebase_head" "$index_tree" "$porcelain" "$unmerged_content"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
vault_sync_op_record_conflict_identity() {
|
|
263
|
+
local repo="$1" op_id="$2"
|
|
264
|
+
local fp
|
|
265
|
+
fp="$(vault_sync_op_fingerprint_repo_state "$repo")" || return 1
|
|
266
|
+
vault_sync_op_set_field "$repo" "$op_id" "conflict_identity" "$fp"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
vault_sync_op_conflict_identity_unchanged() {
|
|
270
|
+
local repo="$1" op_id="$2"
|
|
271
|
+
local expected actual
|
|
272
|
+
expected="$(vault_sync_op_get_field "$repo" "$op_id" "conflict_identity")" || return 1
|
|
273
|
+
[ -n "$expected" ] || return 1
|
|
274
|
+
actual="$(vault_sync_op_fingerprint_repo_state "$repo")" || return 1
|
|
275
|
+
[ "$expected" = "$actual" ]
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Load common may_retry fields from one journal file read.
|
|
279
|
+
vault_sync_op_load_retry_fields() {
|
|
280
|
+
local repo="$1" op_id="$2"
|
|
281
|
+
local file
|
|
282
|
+
file="$(vault_sync_op_journal_path "$repo" "$op_id")" || return 1
|
|
283
|
+
[ -f "$file" ] || return 1
|
|
284
|
+
# shellcheck disable=SC2034
|
|
285
|
+
eval "$(awk -F= '
|
|
286
|
+
$1=="phase" || $1=="retry_count" || $1=="handoff" || $1=="target_oid" || $1=="conflict_identity" {
|
|
287
|
+
key=$1
|
|
288
|
+
val=substr($0, index($0,"=")+1)
|
|
289
|
+
gsub(/'\''/, "'\''\\'\'''\''", val)
|
|
290
|
+
printf "_vs_jf_%s='\''%s'\''\n", key, val
|
|
291
|
+
}
|
|
292
|
+
' "$file")"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
vault_sync_op_may_retry() {
|
|
296
|
+
local repo="$1" op_id="$2" live_remote_oid="$3"
|
|
297
|
+
local phase retry handoff target fp onto actual
|
|
298
|
+
|
|
299
|
+
_vs_jf_phase=""
|
|
300
|
+
_vs_jf_retry_count=""
|
|
301
|
+
_vs_jf_handoff=""
|
|
302
|
+
_vs_jf_target_oid=""
|
|
303
|
+
_vs_jf_conflict_identity=""
|
|
304
|
+
vault_sync_op_load_retry_fields "$repo" "$op_id" || return 1
|
|
305
|
+
phase="${_vs_jf_phase}"
|
|
306
|
+
retry="${_vs_jf_retry_count}"
|
|
307
|
+
handoff="${_vs_jf_handoff}"
|
|
308
|
+
target="${_vs_jf_target_oid}"
|
|
309
|
+
fp="${_vs_jf_conflict_identity}"
|
|
310
|
+
|
|
311
|
+
# Fail closed: only one retry, never after handoff.
|
|
312
|
+
[ "$handoff" = "0" ] || return 1
|
|
313
|
+
[ "$retry" = "0" ] || return 1
|
|
314
|
+
case "$phase" in
|
|
315
|
+
rebasing|retrying) ;;
|
|
316
|
+
*) return 1 ;;
|
|
317
|
+
esac
|
|
318
|
+
[ -n "$live_remote_oid" ] && [ "$live_remote_oid" != "$target" ] || return 1
|
|
319
|
+
|
|
320
|
+
# Require stable conflict identity (empty / no sequencer → fail closed).
|
|
321
|
+
[ -n "$fp" ] || return 1
|
|
322
|
+
actual="$(vault_sync_op_fingerprint_repo_state "$repo")" || return 1
|
|
323
|
+
[ "$fp" = "$actual" ] || return 1
|
|
324
|
+
|
|
325
|
+
# Journal ownership of sequencer: onto must match journaled target.
|
|
326
|
+
onto="$(printf '%s' "$fp" | sed -n 's/.*onto=\([^;]*\).*/\1/p')"
|
|
327
|
+
[ -n "$onto" ] && [ "$onto" = "$target" ] || return 1
|
|
328
|
+
return 0
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
vault_sync_op_mark_review_required() {
|
|
332
|
+
vault_sync_op_set_fields "$1" "$2" \
|
|
333
|
+
"phase" "review-required" \
|
|
334
|
+
"handoff" "1" \
|
|
335
|
+
"reason" "$3"
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
vault_sync_op_close_complete() {
|
|
339
|
+
vault_sync_op_set_fields "$1" "$2" \
|
|
340
|
+
"phase" "complete" \
|
|
341
|
+
"handoff" "1"
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# Dirty-state helpers used by pull integration
|
|
345
|
+
vault_sync_op_write_inventory() {
|
|
346
|
+
local repo="$1" out="$2"
|
|
347
|
+
{
|
|
348
|
+
echo "# staged"
|
|
349
|
+
git -C "$repo" diff --cached --name-only
|
|
350
|
+
echo "# tracked"
|
|
351
|
+
git -C "$repo" diff --name-only
|
|
352
|
+
echo "# untracked"
|
|
353
|
+
git -C "$repo" ls-files --others --exclude-standard
|
|
354
|
+
} >"$out"
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# Verify inventory after owned-stash apply.
|
|
358
|
+
# - tracked/staged paths that are blobs in the stash tree must reappear.
|
|
359
|
+
# - When skip_content_paths is non-empty (newline-separated), those paths only
|
|
360
|
+
# require presence (post-conflict auto-resolve may legitimately change content).
|
|
361
|
+
# - Other tracked paths must content-match the stash blob.
|
|
362
|
+
# - untracked inventory paths must reappear (and content-match untracked tree when present).
|
|
363
|
+
# Fail closed on any mismatch.
|
|
364
|
+
vault_sync_op_verify_inventory() {
|
|
365
|
+
local repo="$1" op_id="$2" stash_oid="${3:-}" skip_content_paths="${4:-}"
|
|
366
|
+
local inv section path expected_hash actual_hash unmerged skip has_u3
|
|
367
|
+
|
|
368
|
+
inv="$(vault_sync_op_get_field "$repo" "$op_id" inventory_path)" || return 1
|
|
369
|
+
[ -f "$inv" ] || return 1
|
|
370
|
+
|
|
371
|
+
unmerged="$(git -C "$repo" diff --name-only --diff-filter=U 2>/dev/null || true)"
|
|
372
|
+
has_u3=0
|
|
373
|
+
if [ -n "$stash_oid" ] && git -C "$repo" rev-parse -q --verify "${stash_oid}^3" >/dev/null 2>&1; then
|
|
374
|
+
has_u3=1
|
|
375
|
+
fi
|
|
376
|
+
section=""
|
|
377
|
+
while IFS= read -r path || [ -n "$path" ]; do
|
|
378
|
+
case "$path" in
|
|
379
|
+
"# staged") section="staged"; continue ;;
|
|
380
|
+
"# tracked") section="tracked"; continue ;;
|
|
381
|
+
"# untracked") section="untracked"; continue ;;
|
|
382
|
+
""|\#*) continue ;;
|
|
383
|
+
esac
|
|
384
|
+
[ -n "$path" ] || continue
|
|
385
|
+
[ -n "$section" ] || continue
|
|
386
|
+
|
|
387
|
+
# Skip content checks for currently-unmerged conflict paths.
|
|
388
|
+
if printf '%s\n' "$unmerged" | grep -Fxq -- "$path"; then
|
|
389
|
+
continue
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
skip=0
|
|
393
|
+
if [ -n "$skip_content_paths" ] && printf '%s\n' "$skip_content_paths" | grep -Fxq -- "$path"; then
|
|
394
|
+
skip=1
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
case "$section" in
|
|
398
|
+
staged|tracked)
|
|
399
|
+
if [ -n "$stash_oid" ] && git -C "$repo" cat-file -e "$stash_oid:$path" 2>/dev/null; then
|
|
400
|
+
if [ ! -f "$repo/$path" ] && [ ! -L "$repo/$path" ]; then
|
|
401
|
+
return 1
|
|
402
|
+
fi
|
|
403
|
+
if [ "$skip" -eq 0 ]; then
|
|
404
|
+
expected_hash="$(git -C "$repo" rev-parse "$stash_oid:$path" 2>/dev/null || echo missing)"
|
|
405
|
+
actual_hash="$(git -C "$repo" hash-object -- "$repo/$path" 2>/dev/null || echo missing)"
|
|
406
|
+
[ "$expected_hash" = "$actual_hash" ] || return 1
|
|
407
|
+
fi
|
|
408
|
+
fi
|
|
409
|
+
;;
|
|
410
|
+
untracked)
|
|
411
|
+
if [ ! -e "$repo/$path" ] && [ ! -L "$repo/$path" ]; then
|
|
412
|
+
return 1
|
|
413
|
+
fi
|
|
414
|
+
if [ "$skip" -eq 0 ] && [ "$has_u3" -eq 1 ]; then
|
|
415
|
+
if git -C "$repo" cat-file -e "${stash_oid}^3:$path" 2>/dev/null; then
|
|
416
|
+
expected_hash="$(git -C "$repo" rev-parse "${stash_oid}^3:$path" 2>/dev/null || echo missing)"
|
|
417
|
+
if [ -f "$repo/$path" ]; then
|
|
418
|
+
actual_hash="$(git -C "$repo" hash-object -- "$repo/$path" 2>/dev/null || echo missing)"
|
|
419
|
+
[ "$expected_hash" = "$actual_hash" ] || return 1
|
|
420
|
+
fi
|
|
421
|
+
fi
|
|
422
|
+
fi
|
|
423
|
+
;;
|
|
424
|
+
esac
|
|
425
|
+
done <"$inv"
|
|
426
|
+
return 0
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
vault_sync_op_stash_push_owned() {
|
|
430
|
+
# Usage: vault_sync_op_stash_push_owned <repo> <message> <include_untracked:0|1>
|
|
431
|
+
# Prints stash OID on stdout; returns 0 on success.
|
|
432
|
+
local repo="$1" msg="$2" include_u="$3"
|
|
433
|
+
local after
|
|
434
|
+
|
|
435
|
+
if [ "$include_u" = "1" ]; then
|
|
436
|
+
git -C "$repo" stash push -u -m "$msg" >/dev/null 2>&1 || return 1
|
|
437
|
+
else
|
|
438
|
+
git -C "$repo" stash push -m "$msg" >/dev/null 2>&1 || return 1
|
|
439
|
+
fi
|
|
440
|
+
after="$(git -C "$repo" rev-parse -q --verify refs/stash 2>/dev/null || true)"
|
|
441
|
+
[ -n "$after" ] || return 1
|
|
442
|
+
printf '%s\n' "$after"
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
vault_sync_op_stash_apply_owned() {
|
|
446
|
+
local repo="$1" oid="$2"
|
|
447
|
+
git -C "$repo" stash apply "$oid"
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
vault_sync_op_stash_drop_owned() {
|
|
451
|
+
# Drop only the stash list entry that still resolves to oid.
|
|
452
|
+
# Bash 3.2-safe: avoid process substitution.
|
|
453
|
+
local repo="$1" oid="$2"
|
|
454
|
+
local entry sha list
|
|
455
|
+
|
|
456
|
+
list="$(git -C "$repo" stash list --format='%gd' 2>/dev/null || true)"
|
|
457
|
+
[ -n "$list" ] || return 1
|
|
458
|
+
while IFS= read -r entry; do
|
|
459
|
+
[ -n "$entry" ] || continue
|
|
460
|
+
sha="$(git -C "$repo" rev-parse "$entry" 2>/dev/null || true)"
|
|
461
|
+
if [ "$sha" = "$oid" ]; then
|
|
462
|
+
git -C "$repo" stash drop "$entry"
|
|
463
|
+
return $?
|
|
464
|
+
fi
|
|
465
|
+
done <<EOF
|
|
466
|
+
$list
|
|
467
|
+
EOF
|
|
468
|
+
return 1
|
|
469
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# git-rebase-state.sh — classify and safely clear Git rebase sequencer state.
|
|
3
|
+
#
|
|
4
|
+
# Bash 3.2 compatible. Source from vault-sync scripts.
|
|
5
|
+
#
|
|
6
|
+
# Classification (stdout):
|
|
7
|
+
# none — no rebase sequencer directory
|
|
8
|
+
# active — real in-progress rebase (must not auto-clear)
|
|
9
|
+
# stale-clean — sequencer dir exists, worktree clean of unmerged paths,
|
|
10
|
+
# live tip differs from recorded orig-head
|
|
11
|
+
#
|
|
12
|
+
# Cleanup for stale-clean:
|
|
13
|
+
# 1. Create refs/vault-sync/recovery/<UTC timestamp> at current HEAD
|
|
14
|
+
# 2. git rebase --quit (never --abort, never raw rm -rf of sequencer)
|
|
15
|
+
# 3. Verify HEAD unchanged
|
|
16
|
+
|
|
17
|
+
# Returns 0 and prints classification on stdout.
|
|
18
|
+
vault_sync_rebase_state() {
|
|
19
|
+
local repo="${1:-.}"
|
|
20
|
+
local git_dir rebase_dir orig_head live_head unmerged rebase_head
|
|
21
|
+
|
|
22
|
+
if ! git_dir="$(git -C "$repo" rev-parse --git-dir 2>/dev/null)"; then
|
|
23
|
+
printf 'active\n'
|
|
24
|
+
return 0
|
|
25
|
+
fi
|
|
26
|
+
# Make absolute if relative
|
|
27
|
+
case "$git_dir" in
|
|
28
|
+
/*) ;;
|
|
29
|
+
*) git_dir="$repo/$git_dir" ;;
|
|
30
|
+
esac
|
|
31
|
+
|
|
32
|
+
rebase_dir=""
|
|
33
|
+
if [ -d "$git_dir/rebase-merge" ]; then
|
|
34
|
+
rebase_dir="$git_dir/rebase-merge"
|
|
35
|
+
elif [ -d "$git_dir/rebase-apply" ]; then
|
|
36
|
+
rebase_dir="$git_dir/rebase-apply"
|
|
37
|
+
else
|
|
38
|
+
printf 'none\n'
|
|
39
|
+
return 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Unmerged paths → always active
|
|
43
|
+
unmerged="$(git -C "$repo" diff --name-only --diff-filter=U 2>/dev/null || true)"
|
|
44
|
+
if [ -n "$unmerged" ]; then
|
|
45
|
+
printf 'active\n'
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# REBASE_HEAD present with matching in-progress context → active
|
|
50
|
+
if git -C "$repo" rev-parse -q --verify REBASE_HEAD >/dev/null 2>&1; then
|
|
51
|
+
rebase_head="$(git -C "$repo" rev-parse REBASE_HEAD 2>/dev/null || true)"
|
|
52
|
+
live_head="$(git -C "$repo" rev-parse HEAD 2>/dev/null || true)"
|
|
53
|
+
# If REBASE_HEAD resolves and sequencer has stopped-sha / head-name, treat as active
|
|
54
|
+
# unless we can prove stale-clean (tip advanced past orig-head with clean tree).
|
|
55
|
+
if [ -f "$rebase_dir/orig-head" ] || [ -f "$rebase_dir/orig_head" ]; then
|
|
56
|
+
:
|
|
57
|
+
else
|
|
58
|
+
printf 'active\n'
|
|
59
|
+
return 0
|
|
60
|
+
fi
|
|
61
|
+
# REBASE_HEAD + clean tree but tip still at orig-head context → active
|
|
62
|
+
orig_head=""
|
|
63
|
+
if [ -f "$rebase_dir/orig-head" ]; then
|
|
64
|
+
orig_head="$(tr -d '[:space:]' < "$rebase_dir/orig-head" 2>/dev/null || true)"
|
|
65
|
+
elif [ -f "$rebase_dir/orig_head" ]; then
|
|
66
|
+
orig_head="$(tr -d '[:space:]' < "$rebase_dir/orig_head" 2>/dev/null || true)"
|
|
67
|
+
fi
|
|
68
|
+
if [ -n "$orig_head" ] && [ -n "$live_head" ]; then
|
|
69
|
+
if [ "$live_head" = "$orig_head" ] || [ "$live_head" = "$rebase_head" ]; then
|
|
70
|
+
printf 'active\n'
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
# Tip advanced past orig-head with no unmerged paths → stale-clean
|
|
74
|
+
if git -C "$repo" merge-base --is-ancestor "$orig_head" "$live_head" 2>/dev/null \
|
|
75
|
+
&& [ "$live_head" != "$orig_head" ]; then
|
|
76
|
+
# Also require clean index/worktree (no unstaged/staged beyond rebase)
|
|
77
|
+
if git -C "$repo" diff --quiet 2>/dev/null \
|
|
78
|
+
&& git -C "$repo" diff --cached --quiet 2>/dev/null; then
|
|
79
|
+
printf 'stale-clean\n'
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
# Fail closed: unknown REBASE_HEAD context is active
|
|
85
|
+
printf 'active\n'
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# No REBASE_HEAD. Sequencer dir may be empty/corrupt or left after tip advance.
|
|
90
|
+
orig_head=""
|
|
91
|
+
if [ -f "$rebase_dir/orig-head" ]; then
|
|
92
|
+
orig_head="$(tr -d '[:space:]' < "$rebase_dir/orig-head" 2>/dev/null || true)"
|
|
93
|
+
elif [ -f "$rebase_dir/orig_head" ]; then
|
|
94
|
+
orig_head="$(tr -d '[:space:]' < "$rebase_dir/orig_head" 2>/dev/null || true)"
|
|
95
|
+
fi
|
|
96
|
+
live_head="$(git -C "$repo" rev-parse HEAD 2>/dev/null || true)"
|
|
97
|
+
|
|
98
|
+
if [ -n "$orig_head" ] && [ -n "$live_head" ] && [ "$live_head" != "$orig_head" ]; then
|
|
99
|
+
if git -C "$repo" diff --quiet 2>/dev/null \
|
|
100
|
+
&& git -C "$repo" diff --cached --quiet 2>/dev/null; then
|
|
101
|
+
printf 'stale-clean\n'
|
|
102
|
+
return 0
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Empty/corrupt sequencer with no REBASE_HEAD and tip == orig-head or no orig-head:
|
|
107
|
+
# treat as stale-clean only when tree is clean (legacy empty rebase-merge cleanup).
|
|
108
|
+
if [ -z "$(ls -A "$rebase_dir" 2>/dev/null || true)" ] \
|
|
109
|
+
|| { [ -z "$orig_head" ] || [ "$live_head" = "$orig_head" ]; }; then
|
|
110
|
+
if git -C "$repo" diff --quiet 2>/dev/null \
|
|
111
|
+
&& git -C "$repo" diff --cached --quiet 2>/dev/null; then
|
|
112
|
+
# Empty dir or tip still at orig-head with no REBASE_HEAD → safe quit
|
|
113
|
+
printf 'stale-clean\n'
|
|
114
|
+
return 0
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# Fail closed
|
|
119
|
+
printf 'active\n'
|
|
120
|
+
return 0
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Clear stale-clean rebase state without moving HEAD.
|
|
124
|
+
# Returns 0 on success, 1 if active / fail-closed / HEAD moved.
|
|
125
|
+
vault_sync_clear_stale_rebase() {
|
|
126
|
+
local repo="${1:-.}"
|
|
127
|
+
local state pre_head post_head recovery_ref ts
|
|
128
|
+
|
|
129
|
+
state="$(vault_sync_rebase_state "$repo")"
|
|
130
|
+
case "$state" in
|
|
131
|
+
none)
|
|
132
|
+
return 0
|
|
133
|
+
;;
|
|
134
|
+
active)
|
|
135
|
+
return 1
|
|
136
|
+
;;
|
|
137
|
+
stale-clean)
|
|
138
|
+
;;
|
|
139
|
+
*)
|
|
140
|
+
return 1
|
|
141
|
+
;;
|
|
142
|
+
esac
|
|
143
|
+
|
|
144
|
+
pre_head="$(git -C "$repo" rev-parse HEAD 2>/dev/null)" || return 1
|
|
145
|
+
ts="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
146
|
+
recovery_ref="refs/vault-sync/recovery/${ts}"
|
|
147
|
+
if ! git -C "$repo" update-ref "$recovery_ref" "$pre_head" 2>/dev/null; then
|
|
148
|
+
return 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# Prefer git rebase --quit (Git 2.12+). Never abort (would reset to orig-head).
|
|
152
|
+
if ! git -C "$repo" rebase --quit 2>/dev/null; then
|
|
153
|
+
# If quit fails but sequencer remains, fail closed — do not rm -rf.
|
|
154
|
+
return 1
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
post_head="$(git -C "$repo" rev-parse HEAD 2>/dev/null)" || return 1
|
|
158
|
+
if [ "$pre_head" != "$post_head" ]; then
|
|
159
|
+
# Attempt to restore tip if quit somehow moved HEAD (should not happen).
|
|
160
|
+
git -C "$repo" update-ref HEAD "$pre_head" 2>/dev/null || true
|
|
161
|
+
return 1
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# Confirm sequencer gone; if still present after quit, fail closed.
|
|
165
|
+
if [ -d "$(git -C "$repo" rev-parse --git-dir 2>/dev/null)/rebase-merge" ] \
|
|
166
|
+
|| [ -d "$(git -C "$repo" rev-parse --git-dir 2>/dev/null)/rebase-apply" ]; then
|
|
167
|
+
# Re-resolve absolute git dir
|
|
168
|
+
local git_dir
|
|
169
|
+
git_dir="$(git -C "$repo" rev-parse --git-dir 2>/dev/null)"
|
|
170
|
+
case "$git_dir" in
|
|
171
|
+
/*) ;;
|
|
172
|
+
*) git_dir="$repo/$git_dir" ;;
|
|
173
|
+
esac
|
|
174
|
+
if [ -d "$git_dir/rebase-merge" ] || [ -d "$git_dir/rebase-apply" ]; then
|
|
175
|
+
return 1
|
|
176
|
+
fi
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
return 0
|
|
180
|
+
}
|