#!/usr/bin/env bash
# Integration tests for: gl fold (staged / amend / amend-all / fixup / move / uncommit / move-file / create)
set -euo pipefail
source "$(dirname "$0")/helpers.sh"

# ── PRECONDITIONS ─────────────────────────────────────────────────────────────

describe "precond: not in a git repository"
new_tmpdir TMP_NOGIT
CODE=0
(cd "$TMP_NOGIT" && NO_COLOR=1 GIT_TERMINAL_PROMPT=0 "$GL_BIN" fold HEAD >/dev/null 2>&1) || CODE=$?
assert_exit_fail "$CODE" "precond_not_git_repo"
rm -rf "$TMP_NOGIT"

describe "precond: single-arg with nothing staged"
setup_repo_with_remote
commit_file "Clean commit" "clean.txt"
gl_capture fold HEAD
assert_exit_fail "$CODE" "precond_nothing_staged_fail"
assert_contains "$OUT" "Nothing to commit" "precond_nothing_staged_msg"

describe "precond: single-arg with branch as target (non-commit)"
setup_repo_with_remote
commit_file "Staged source" "staged.txt"
write_file "staged.txt" "changed"
git -C "$WORK" add staged.txt
create_feature_branch "g-notcommit"
gl_capture fold g-notcommit
assert_exit_fail "$CODE" "precond_single_branch_fail"
assert_contains "$OUT" "did not resolve to a commit" "precond_single_branch_msg"

describe "precond: file + branch target is rejected"
setup_repo_with_remote
commit_file "Base commit" "base.txt"
write_file "base.txt" "changed"
create_feature_branch "g-file-branch"
gl_capture fold base.txt g-file-branch
assert_exit_fail "$CODE" "precond_file_branch_fail"
assert_contains "$OUT" "Cannot fold files into a branch" "precond_file_branch_msg"

describe "precond: branch as source is rejected"
setup_repo_with_remote
commit_file "Base commit" "base.txt"
create_feature_branch "g-src-branch"
commit_file "Target commit" "target.txt"
target_hash="$(head_hash)"
gl_capture fold g-src-branch "$target_hash"
assert_exit_fail "$CODE" "precond_branch_src_fail"
assert_contains "$OUT" "did not resolve" "precond_branch_src_msg"

describe "precond: zz + branch target is rejected"
setup_repo_with_remote
commit_file "Base commit" "base.txt"
write_file "base.txt" "changed"
create_feature_branch "g-zz-branch"
gl_capture fold zz g-zz-branch
assert_exit_fail "$CODE" "precond_zz_branch_fail"
assert_contains "$OUT" "Cannot fold files into a branch" "precond_zz_branch_msg"

describe "precond: zz with clean working tree"
setup_repo_with_remote
commit_file "Clean commit" "clean.txt"
target_hash="$(head_hash)"
gl_capture fold zz "$target_hash"
assert_exit_fail "$CODE" "precond_zz_clean_fail"
assert_contains "$OUT" "No changes to fold" "precond_zz_clean_msg"

describe "precond: multiple commit sources are rejected"
setup_repo_with_remote
commit_file "Commit A" "a.txt"
hash_a="$(head_hash)"
commit_file "Commit B" "b.txt"
hash_b="$(head_hash)"
commit_file "Commit C" "c.txt"
hash_c="$(head_hash)"
gl_capture fold "$hash_b" "$hash_a" "$hash_c"
assert_exit_fail "$CODE" "precond_multi_commit_src_fail"
assert_contains "$OUT" "Only one commit source" "precond_multi_commit_src_msg"

describe "precond: mixed file and commit sources are rejected"
setup_repo_with_remote
commit_file "Commit A" "mix-a.txt"
hash_a="$(head_hash)"
commit_file "Commit B" "mix-b.txt"
hash_b="$(head_hash)"
write_file "mix-a.txt" "changed"
gl_capture fold mix-a.txt "$hash_a" "$hash_b"
assert_exit_fail "$CODE" "precond_mixed_src_fail"
assert_contains "$OUT" "Cannot mix" "precond_mixed_src_msg"

describe "precond: commitfile + branch target is rejected"
setup_repo_with_remote
create_feature_branch "g-cf-branch"
echo "cf content" > "$WORK/cf.txt"
git -C "$WORK" add cf.txt
git -C "$WORK" commit -q -m "cf commit"
cf_ref=$(gl status -f | grep "cf.txt" | grep -oE '[0-9a-z]+:[0-9]+' | head -1)
gl_capture fold "$cf_ref" g-cf-branch
assert_exit_fail "$CODE" "precond_commitfile_branch_fail"
assert_contains "$OUT" "Cannot fold a commit file into a branch" "precond_commitfile_branch_msg"

describe "precond: file with no changes to fold"
setup_repo_with_remote
commit_file "Base commit" "nochange.txt"
target_hash="$(head_hash)"
gl_capture fold nochange.txt "$target_hash"
assert_exit_fail "$CODE" "precond_no_file_changes_fail"
assert_contains "$OUT" "has no changes" "precond_no_file_changes_msg"

# ── CASE 0: STAGED FILES + COMMIT (SINGLE-ARGUMENT) ───────────────────────────

describe "staged: fold staged file into HEAD using full hash"
setup_repo_with_remote
commit_file "Staged target" "staged-head.txt"
old_hash="$(head_hash)"
write_file "staged-head.txt" "amended staged content"
git -C "$WORK" add staged-head.txt
out=$(gl fold "$old_hash")
assert_exit_ok $? "staged_full_hash_ok"
assert_ne "$old_hash" "$(head_hash)" "staged_full_hash_rewrote"
assert_head_msg "Staged target" "staged_full_hash_msg"
assert_file_content "staged-head.txt" "amended staged content" "staged_full_hash_content"

describe "staged: fold staged file into HEAD using short ID"
setup_repo_with_remote
commit_file "Staged short" "staged-short.txt"
old_hash="$(head_hash)"
target_sid=$(commit_sid_from_status "Staged short")
write_file "staged-short.txt" "short id staged content"
git -C "$WORK" add staged-short.txt
out=$(gl fold "$target_sid")
assert_exit_ok $? "staged_short_id_ok"
assert_ne "$old_hash" "$(head_hash)" "staged_short_id_rewrote"
assert_head_msg "Staged short" "staged_short_id_msg"

describe "staged: fold staged file into non-HEAD commit"
setup_repo_with_remote
commit_file "Base commit" "staged-base.txt"
base_hash="$(head_hash)"
commit_file "Top commit" "staged-top.txt"
old_top_hash="$(head_hash)"
write_file "staged-base.txt" "staged into base"
git -C "$WORK" add staged-base.txt
out=$(gl fold "$base_hash")
assert_exit_ok $? "staged_non_head_ok"
assert_head_msg "Top commit" "staged_non_head_top_msg"
assert_ne "$old_top_hash" "$(head_hash)" "staged_non_head_top_rewrote"
assert_file_content "staged-base.txt" "staged into base" "staged_non_head_content"

describe "staged: unstaged changes to same file are preserved"
setup_repo_with_remote
commit_file "Preserve unstaged" "preserve.txt"
target_hash="$(head_hash)"
# Stage one change, then modify the file again (unstaged)
write_file "preserve.txt" "staged line"
git -C "$WORK" add preserve.txt
write_file "preserve.txt" "unstaged line"
out=$(gl fold "$target_hash")
assert_exit_ok $? "staged_preserve_unstaged_ok"
assert_file_content "preserve.txt" "unstaged line" "staged_preserve_unstaged_wt"

# ── CASE 1: FILE(S) + COMMIT (AMEND) ──────────────────────────────────────────

describe "amend: fold single file into HEAD using full hash"
setup_repo_with_remote
commit_file "First commit" "file.txt"
old_hash="$(head_hash)"
write_file "file.txt" "amended content"
out=$(gl fold file.txt "$old_hash")
assert_exit_ok $? "amend_single_head_ok"
assert_ne "$old_hash" "$(head_hash)" "amend_single_head_rewrote"
assert_head_msg "First commit" "amend_single_head_msg"
assert_file_content "file.txt" "amended content" "amend_single_head_content"

describe "amend: fold file into HEAD using HEAD reference"
setup_repo_with_remote
commit_file "Head ref commit" "headref.txt"
old_hash="$(head_hash)"
write_file "headref.txt" "head ref amended"
out=$(gl fold headref.txt HEAD)
assert_exit_ok $? "amend_head_ref_ok"
assert_ne "$old_hash" "$(head_hash)" "amend_head_ref_rewrote"
assert_head_msg "Head ref commit" "amend_head_ref_msg"

describe "amend: fold file into HEAD using short ID"
setup_repo_with_remote
commit_file "Short ID target" "shortid.txt"
old_hash="$(head_hash)"
target_sid=$(commit_sid_from_status "Short ID target")
write_file "shortid.txt" "short id amended"
out=$(gl fold shortid.txt "$target_sid")
assert_exit_ok $? "amend_short_id_ok"
assert_ne "$old_hash" "$(head_hash)" "amend_short_id_rewrote"
assert_head_msg "Short ID target" "amend_short_id_msg"

describe "amend: fold file into non-HEAD commit using full hash"
setup_repo_with_remote
commit_file "Base commit" "base.txt"
target_hash="$(head_hash)"
commit_file "Top commit" "top.txt"
write_file "base.txt" "amended base"
out=$(gl fold base.txt "$target_hash")
assert_exit_ok $? "amend_non_head_ok"
assert_head_msg "Top commit" "amend_non_head_top_msg"
assert_file_content "base.txt" "amended base" "amend_non_head_content"

describe "amend: fold multiple files into HEAD"
setup_repo_with_remote
commit_file "Multi file commit" "multi-a.txt"
echo "multi-b content" > "$WORK/multi-b.txt"
git -C "$WORK" add multi-b.txt
git -C "$WORK" commit -q -m "Multi file commit"
old_hash="$(head_hash)"
write_file "multi-a.txt" "amended multi-a"
write_file "multi-b.txt" "amended multi-b"
out=$(gl fold multi-a.txt multi-b.txt HEAD)
assert_exit_ok $? "amend_multi_ok"
assert_ne "$old_hash" "$(head_hash)" "amend_multi_rewrote"
assert_file_content "multi-a.txt" "amended multi-a" "amend_multi_content_a"
assert_file_content "multi-b.txt" "amended multi-b" "amend_multi_content_b"

describe "amend: other uncommitted files are preserved"
setup_repo_with_remote
commit_file "Amend preserve" "amend-target.txt"
target_hash="$(head_hash)"
commit_file "Preserve this" "preserve-other.txt"
write_file "amend-target.txt" "amended"
write_file "preserve-other.txt" "should stay dirty"
out=$(gl fold amend-target.txt "$target_hash")
assert_exit_ok $? "amend_preserve_other_ok"
assert_file_content "preserve-other.txt" "should stay dirty" "amend_preserve_other_content"

# ── CASE 1b: UNSTAGED (zz) + COMMIT (AMEND ALL) ───────────────────────────────

describe "amend-all: fold all working tree changes into HEAD"
setup_repo_with_remote
commit_file "All changes target" "all-a.txt"
echo "all-b content" > "$WORK/all-b.txt"
git -C "$WORK" add all-b.txt
git -C "$WORK" commit -q -m "All changes target"
old_hash="$(head_hash)"
write_file "all-a.txt" "modified a"
write_file "all-b.txt" "modified b"
out=$(gl fold zz HEAD)
assert_exit_ok $? "amend_all_ok"
assert_ne "$old_hash" "$(head_hash)" "amend_all_rewrote"
assert_head_msg "All changes target" "amend_all_msg"
assert_file_content "all-a.txt" "modified a" "amend_all_content_a"
assert_file_content "all-b.txt" "modified b" "amend_all_content_b"

describe "amend-all: fold all changes into non-HEAD commit using short ID"
setup_repo_with_remote
commit_file "ZZ base" "zz-base.txt"
base_sid=$(commit_sid_from_status "ZZ base")
commit_file "ZZ top" "zz-top.txt"
write_file "zz-base.txt" "zz modified base"
out=$(gl fold zz "$base_sid")
assert_exit_ok $? "amend_all_non_head_ok"
assert_head_msg "ZZ top" "amend_all_non_head_top_msg"
assert_file_content "zz-base.txt" "zz modified base" "amend_all_non_head_content"

# ── CASE 2: COMMIT + COMMIT (FIXUP) ───────────────────────────────────────────

describe "fixup: source commit absorbed into target using full hashes"
setup_repo_with_remote
commit_file "Target commit" "target.txt"
target_hash="$(head_hash)"
commit_file "Source commit" "source.txt"
source_hash="$(head_hash)"
out=$(gl fold "$source_hash" "$target_hash")
assert_exit_ok $? "fixup_full_hash_ok"
assert_commit_not_in_log "$source_hash" "fixup_full_hash_source_gone"
assert_log_contains "Target commit" "fixup_full_hash_target_present"
assert_head_msg "Target commit" "fixup_full_hash_msg_preserved"

describe "fixup: source commit absorbed into target using short IDs"
setup_repo_with_remote
commit_file "Fixup target" "fixup-t.txt"
commit_file "Fixup source" "fixup-s.txt"
source_sid=$(commit_sid_from_status "Fixup source")
target_sid=$(commit_sid_from_status "Fixup target")
source_hash="$(head_hash)"
out=$(gl fold "$source_sid" "$target_sid")
assert_exit_ok $? "fixup_short_id_ok"
assert_commit_not_in_log "$source_hash" "fixup_short_id_source_gone"
assert_head_msg "Fixup target" "fixup_short_id_msg_preserved"

describe "fixup: source must be newer than target"
setup_repo_with_remote
commit_file "Old commit" "old.txt"
old_hash="$(head_hash)"
commit_file "New commit" "new.txt"
new_hash="$(head_hash)"
# Try to fold older commit INTO newer one (wrong direction)
gl_capture fold "$old_hash" "$new_hash"
assert_exit_fail "$CODE" "fixup_wrong_order_fail"
assert_contains "$OUT" "Source commit must be newer than target" "fixup_wrong_order_msg"

describe "fixup: target commit message is preserved, source disappears"
setup_repo_with_remote
commit_file "Keep this message" "keep.txt"
target_hash="$(head_hash)"
commit_file "Discard this message" "discard.txt"
source_hash="$(head_hash)"
out=$(gl fold "$source_hash" "$target_hash")
assert_exit_ok $? "fixup_msg_ok"
assert_head_msg "Keep this message" "fixup_msg_preserved"
assert_log_not_contains "Discard this message" "fixup_source_msg_gone"

describe "fixup: uncommitted changes are preserved"
setup_repo_with_remote
commit_file "Fixup preserve target" "fp-target.txt"
target_hash="$(head_hash)"
commit_file "Fixup preserve source" "fp-source.txt"
source_hash="$(head_hash)"
write_file "fp-target.txt" "uncommitted dirty"
out=$(gl fold "$source_hash" "$target_hash")
assert_exit_ok $? "fixup_preserve_wt_ok"
assert_file_content "fp-target.txt" "uncommitted dirty" "fixup_preserve_wt_content"

# ── CASE 3: COMMIT + BRANCH (MOVE) ────────────────────────────────────────────

describe "move: commit relocated to target branch by branch name"
setup_repo_with_remote
create_feature_branch "g-move-src"
switch_to g-move-src
commit_file "Move me" "move-me.txt"
switch_to integration
weave_branch "g-move-src"
create_feature_branch "h-move-dst"
switch_to h-move-dst
commit_file "Dst existing" "dst-existing.txt"
switch_to integration
weave_branch "h-move-dst"
move_sid=$(commit_sid_from_status "Move me")
out=$(gl fold "$move_sid" h-move-dst)
assert_exit_ok $? "move_branch_name_ok"
assert_contains "$(git -C "$WORK" log h-move-dst --oneline)" "Move me" "move_branch_name_on_dst"
# Commit should no longer be in g-move-src's unique commits (not between src and its base)
upstream_oid=$(upstream_oid)
assert_not_contains "$(git -C "$WORK" log g-move-src ^"$upstream_oid" --oneline 2>/dev/null)" "Move me" "move_branch_name_not_in_src"

describe "move: commit relocated to target branch by short ID"
setup_repo_with_remote
create_feature_branch "g-sid-src"
switch_to g-sid-src
commit_file "Move by sid" "sid-move.txt"
switch_to integration
weave_branch "g-sid-src"
create_feature_branch "h-sid-dst"
switch_to h-sid-dst
commit_file "Dst sid existing" "dst-sid.txt"
switch_to integration
weave_branch "h-sid-dst"
move_csid=$(commit_sid_from_status "Move by sid")
dst_bsid=$(branch_sid_from_status "h-sid-dst")
out=$(gl fold "$move_csid" "$dst_bsid")
assert_exit_ok $? "move_short_id_ok"
assert_contains "$(git -C "$WORK" log h-sid-dst --oneline)" "Move by sid" "move_short_id_on_dst"

describe "move: several commits go to an existing branch in history order"
setup_repo_with_remote
create_feature_branch "m-multi-dst"
switch_to m-multi-dst
commit_file "Dst base" "dst-base.txt"
switch_to integration
weave_branch "m-multi-dst"
commit_file "First loose" "first-loose.txt"
commit_file "Second loose" "second-loose.txt"
first_sid=$(commit_sid_from_status "First loose")
second_sid=$(commit_sid_from_status "Second loose")
out=$(gl fold "$second_sid" "$first_sid" m-multi-dst)
assert_exit_ok $? "move_multi_ok"
assert_eq "$(git -C "$WORK" log -1 --format=%s m-multi-dst)" "Second loose" "move_multi_tip"
assert_eq "$(git -C "$WORK" log -1 --format=%s m-multi-dst~1)" "First loose" "move_multi_mid"
assert_eq "$(git -C "$WORK" log -1 --format=%s m-multi-dst~2)" "Dst base" "move_multi_base"

describe "move --below: commit reordered within its branch"
setup_repo_with_remote
create_feature_branch "r-order"
switch_to r-order
commit_file "R one" "r-one.txt"
commit_file "R two" "r-two.txt"
commit_file "R three" "r-three.txt"
switch_to integration
weave_branch "r-order"
one_sid=$(commit_sid_from_status "R one")
three_sid=$(commit_sid_from_status "R three")
gl_capture fold "$three_sid" --below "$one_sid"
assert_exit_ok "$CODE" "move_below_ok"
assert_contains "$OUT" "below" "move_below_msg"
assert_eq "$(git -C "$WORK" log -1 --format=%s r-order)" "R two" "move_below_tip"
assert_eq "$(git -C "$WORK" log -1 --format=%s r-order~1)" "R one" "move_below_mid"
assert_eq "$(git -C "$WORK" log -1 --format=%s r-order~2)" "R three" "move_below_bottom"
assert_no_state_file "move_below_no_state"

describe "move --below: commit crosses into another branch, source parked"
setup_repo_with_remote
create_feature_branch "s-src"
switch_to s-src
commit_file "S move" "s-move.txt"
switch_to integration
weave_branch "s-src"
create_feature_branch "s-dst"
switch_to s-dst
commit_file "D one" "d-one.txt"
commit_file "D two" "d-two.txt"
switch_to integration
weave_branch "s-dst"
move_sid=$(commit_sid_from_status "S move")
two_sid=$(commit_sid_from_status "D two")
gl_capture fold "$move_sid" --below "$two_sid"
assert_exit_ok "$CODE" "move_below_cross_ok"
assert_contains "$OUT" "now empty, at the base" "move_below_cross_parked_msg"
assert_eq "$(git -C "$WORK" log -1 --format=%s s-dst)" "D two" "move_below_cross_tip"
assert_eq "$(git -C "$WORK" log -1 --format=%s s-dst~1)" "S move" "move_below_cross_mid"
assert_eq "$(git -C "$WORK" log -1 --format=%s s-dst~2)" "D one" "move_below_cross_bottom"
assert_eq "$(branch_oid s-src)" "$(upstream_oid)" "move_below_cross_src_parked"

describe "move --above: several commits land above a branch tip in history order"
setup_repo_with_remote
create_feature_branch "t-dst"
switch_to t-dst
commit_file "T base" "t-base.txt"
switch_to integration
weave_branch "t-dst"
commit_file "First loose" "first-loose.txt"
commit_file "Second loose" "second-loose.txt"
first_sid=$(commit_sid_from_status "First loose")
second_sid=$(commit_sid_from_status "Second loose")
base_sid=$(commit_sid_from_status "T base")
gl_capture fold "$second_sid" "$first_sid" --above "$base_sid"
assert_exit_ok "$CODE" "move_above_multi_ok"
assert_contains "$OUT" "Moved 2 commit(s) above" "move_above_multi_msg"
assert_eq "$(git -C "$WORK" log -1 --format=%s t-dst)" "Second loose" "move_above_multi_tip"
assert_eq "$(git -C "$WORK" log -1 --format=%s t-dst~1)" "First loose" "move_above_multi_mid"
assert_eq "$(git -C "$WORK" log -1 --format=%s t-dst~2)" "T base" "move_above_multi_base"

describe "move --above/--below: refused when already in place or not a commit"
setup_repo_with_remote
create_feature_branch "u-place"
switch_to u-place
commit_file "U one" "u-one.txt"
commit_file "U two" "u-two.txt"
switch_to integration
weave_branch "u-place"
one_sid=$(commit_sid_from_status "U one")
two_sid=$(commit_sid_from_status "U two")
old_head=$(head_hash)
gl_capture fold "$two_sid" --above "$one_sid"
assert_exit_fail "$CODE" "move_above_in_place_fails"
assert_contains "$OUT" "is already directly above" "move_above_in_place_msg"
gl_capture fold "$one_sid" --below "$two_sid"
assert_exit_fail "$CODE" "move_below_in_place_fails"
assert_contains "$OUT" "is already directly below" "move_below_in_place_msg"
gl_capture fold "$one_sid" --above u-place
assert_exit_fail "$CODE" "move_above_branch_fails"
assert_contains "$OUT" "did not resolve to a commit" "move_above_branch_msg"
gl_capture fold "$one_sid" --above "$one_sid"
assert_exit_fail "$CODE" "move_above_self_fails"
assert_contains "$OUT" "Source and target are the same commit" "move_above_self_msg"
assert_eq "$old_head" "$(head_hash)" "move_relative_refused_head_unchanged"

describe "move --above: several commits that conflict — rolled back, staged work kept"
setup_repo_with_remote
create_feature_branch "y-roll-dst"
switch_to y-roll-dst
commit_file "Y dst base" "y-dst.txt"
switch_to integration
weave_branch "y-roll-dst"
create_feature_branch "y-roll-other"
switch_to y-roll-other
commit_file "Y other one" "y-other.txt"
switch_to integration
weave_branch "y-roll-other"
# Both loose commits need y-other.txt, which y-roll-dst's section has not got,
# so replaying them above its commit fails.
commit_file "Y first loose" "y-other.txt"
commit_file "Y second loose" "y-second.txt"
write_file ".gitkeep" "staged work"
git -C "$WORK" add .gitkeep
write_file "y-added.txt" "brand new"
git -C "$WORK" add y-added.txt
echo "and more on top" >> "$WORK/.gitkeep"
porcelain_before="$(git -C "$WORK" status --porcelain | sort)"
assert_contains "$porcelain_before" "MM .gitkeep" "move_rel_rollback_is_staged"
assert_contains "$porcelain_before" "A  y-added.txt" "move_rel_rollback_has_added"
head_before="$(head_hash)"
dst_before="$(branch_oid y-roll-dst)"
base_sid=$(commit_sid_from_status "Y dst base")
first_sid=$(commit_sid_from_status "Y first loose")
second_sid=$(commit_sid_from_status "Y second loose")
gl_capture fold "$first_sid" "$second_sid" --above "$base_sid"
assert_exit_fail "$CODE" "move_rel_rollback_fails"
assert_contains "$OUT" "conflicts" "move_rel_rollback_reason"
assert_eq "$(head_hash)" "$head_before" "move_rel_rollback_head"
assert_eq "$(branch_oid y-roll-dst)" "$dst_before" "move_rel_rollback_dst"
assert_eq "$(git -C "$WORK" status --porcelain | sort)" "$porcelain_before" "move_rel_rollback_index"
assert_no_state_file "move_rel_rollback_no_state"
assert_branch_not_exists "_loom-track" "move_rel_rollback_track_gone"

describe "move --below: conflict → continue → commit reordered"
setup_repo_with_remote
create_feature_branch "w-cont"
switch_to w-cont
printf "A\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "W one"
printf "B\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "W two"
printf "C\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "W three"
switch_to integration
weave_branch "w-cont"
one_sid=$(commit_sid_from_status "W one")
three_sid=$(commit_sid_from_status "W three")
gl_capture fold "$three_sid" --below "$one_sid"
assert_state_file "move_relative_cont_state"
assert_contains "$OUT" "loom continue" "move_relative_cont_hint"
# Sending one commit under the two that rewrote the same file conflicts at
# each replayed step; resolve with distinct content so none becomes empty.
cont_round=0
while [[ -f "$WORK/.git/loom/state.json" && $cont_round -lt 6 ]]; do
    cont_round=$((cont_round + 1))
    printf "resolved %s\n" "$cont_round" > "$WORK/shared.txt"
    git -C "$WORK" add shared.txt
    gl_capture continue
done
assert_exit_ok "$CODE" "move_relative_cont_ok"
assert_no_state_file "move_relative_cont_state_removed"
assert_contains "$OUT" "below" "move_relative_cont_msg"
assert_branch_not_exists "_loom-track" "move_relative_cont_track_gone"
assert_eq "$(git -C "$WORK" log -1 --format=%s w-cont~2)" "W three" "move_relative_cont_bottom"
assert_log_contains "W one" "move_relative_cont_one_kept"
assert_log_contains "W two" "move_relative_cont_two_kept"

describe "move --below: rebase refused up front leaves no temp branch"
setup_repo_with_remote
create_feature_branch "x-wt"
switch_to x-wt
commit_file "X one" "x-one.txt"
commit_file "X two" "x-two.txt"
switch_to integration
weave_branch "x-wt"
git -C "$WORK" worktree add -q "$WORK/../x-wt-co" x-wt
one_sid=$(commit_sid_from_status "X one")
two_sid=$(commit_sid_from_status "X two")
gl_capture fold "$two_sid" --below "$one_sid"
assert_exit_fail "$CODE" "move_relative_refused_fails"
assert_branch_not_exists "_loom-track" "move_relative_refused_track_gone"
assert_no_state_file "move_relative_refused_state_gone"

describe "move --above: conflict → abort restores history"
setup_repo_with_remote
create_feature_branch "v-conf"
switch_to v-conf
printf "A\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "V one"
printf "B\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "V two"
printf "C\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "V three"
switch_to integration
weave_branch "v-conf"
one_sid=$(commit_sid_from_status "V one")
three_sid=$(commit_sid_from_status "V three")
old_head=$(head_hash)
# The abort replays the autostash unstaged, so only `saved_staged_patch` can
# bring the index back; an untracked file would pass without it.
write_file ".gitkeep" "staged work"
git -C "$WORK" add .gitkeep
porcelain_before="$(git -C "$WORK" status --porcelain | sort)"
assert_contains "$porcelain_before" "M  .gitkeep" "move_relative_abort_is_staged"
gl_capture fold "$three_sid" --below "$one_sid"
assert_state_file "move_relative_conflict_state"
assert_contains "$OUT" "loom continue" "move_relative_conflict_hint"
gl_capture abort
assert_exit_ok "$CODE" "move_relative_abort_ok"
assert_no_state_file "move_relative_abort_state_removed"
assert_eq "$old_head" "$(head_hash)" "move_relative_abort_head_restored"
assert_branch_not_exists "_loom-track" "move_relative_abort_track_gone"
assert_eq "$(git -C "$WORK" status --porcelain | sort)" "$porcelain_before" "move_relative_abort_index"

describe "move --below: several commits that conflict — rolled back, index kept"
setup_repo_with_remote
create_feature_branch "y-roll"
switch_to y-roll
printf "A\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "Y one"
printf "B\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "Y two"
printf "C\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "Y three"
printf "D\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "Y four"
switch_to integration
weave_branch "y-roll"
# Staged-and-modified-again, and newly added: the two shapes the autostash
# restores differently from a plain staged edit.
write_file ".gitkeep" "staged work"
git -C "$WORK" add .gitkeep
write_file "added-yroll.txt" "brand new"
git -C "$WORK" add added-yroll.txt
echo "and more on top" >> "$WORK/.gitkeep"
porcelain_before="$(git -C "$WORK" status --porcelain | sort)"
assert_contains "$porcelain_before" "MM .gitkeep" "move_relative_rollback_is_staged"
assert_contains "$porcelain_before" "A  added-yroll.txt" "move_relative_rollback_has_added"
head_before="$(head_hash)"
branch_before="$(git -C "$WORK" rev-parse y-roll)"
one_sid=$(commit_sid_from_status "Y one")
three_sid=$(commit_sid_from_status "Y three")
four_sid=$(commit_sid_from_status "Y four")
# Y three and Y four rewrite shared.txt from the content Y one and Y two put
# there, so replaying them under Y one conflicts.
gl_capture fold "$three_sid" "$four_sid" --below "$one_sid"
assert_exit_fail "$CODE" "move_relative_rollback_fails"
# It must fail on the replay, not for some setup reason that would leave the
# refs untouched anyway and pass the checks below for free.
assert_contains "$OUT" "conflicts" "move_relative_rollback_reason"
assert_eq "$(head_hash)" "$head_before" "move_relative_rollback_head"
assert_eq "$(git -C "$WORK" rev-parse y-roll)" "$branch_before" "move_relative_rollback_branch"
assert_eq "$(git -C "$WORK" status --porcelain | sort)" "$porcelain_before" "move_relative_rollback_index"
assert_no_state_file "move_relative_rollback_no_state"
assert_branch_not_exists "_loom-track" "move_relative_rollback_track_gone"

describe "move: one commit, conflict → abort keeps staged changes"
setup_repo_with_remote
create_feature_branch "s-abort-dst"
switch_to s-abort-dst
commit_file "Dst base" "s-dst.txt"
switch_to integration
weave_branch "s-abort-dst"
create_feature_branch "s-abort-other"
switch_to s-abort-other
commit_file "Other one" "s-other.txt"
switch_to integration
weave_branch "s-abort-other"
commit_file "Conflicting" "s-other.txt"
write_file ".gitkeep" "staged work"
git -C "$WORK" add .gitkeep
write_file "s-added.txt" "brand new"
git -C "$WORK" add s-added.txt
porcelain_before="$(git -C "$WORK" status --porcelain | sort)"
assert_contains "$porcelain_before" "M  .gitkeep" "single_abort_is_staged"
assert_contains "$porcelain_before" "A  s-added.txt" "single_abort_has_added"
conflicting=$(commit_sid_from_status "Conflicting")
gl_capture fold "$conflicting" s-abort-dst
assert_state_file "single_abort_state"
gl_capture abort
assert_exit_ok "$CODE" "single_abort_ok"
assert_eq "$(git -C "$WORK" status --porcelain | sort)" "$porcelain_before" "single_abort_index"

describe "move: several commits that conflict — rolled back, working tree kept"
setup_repo_with_remote
create_feature_branch "n-roll-dst"
switch_to n-roll-dst
commit_file "Dst base" "dst-roll.txt"
switch_to integration
weave_branch "n-roll-dst"
create_feature_branch "n-roll-other"
switch_to n-roll-other
commit_file "Other one" "other-roll.txt"
switch_to integration
weave_branch "n-roll-other"
# Both loose commits need other-roll.txt, which n-roll-dst's section has not
# got, so replaying them there fails.
commit_file "First loose" "other-roll.txt"
commit_file "Second loose" "second-roll.txt"
# A tracked file, staged: the rebase has to autostash it to start at all, and
# its abort replays the autostash into the working tree only. An untracked or
# unstaged file would come back looking right without the index being restored.
write_file ".gitkeep" "staged work"
git -C "$WORK" add .gitkeep
write_file "added-roll.txt" "brand new"
git -C "$WORK" add added-roll.txt
echo "and more on top" >> "$WORK/.gitkeep"
porcelain_before="$(git -C "$WORK" status --porcelain | sort)"
# Staged-and-modified-again, and newly added: the two shapes the autostash
# restores differently from a plain staged edit.
assert_contains "$porcelain_before" "MM .gitkeep" "move_rollback_is_staged"
assert_contains "$porcelain_before" "A  added-roll.txt" "move_rollback_has_added"
head_before="$(head_hash)"
dst_before="$(git -C "$WORK" rev-parse n-roll-dst)"
first_sid=$(commit_sid_from_status "First loose")
second_sid=$(commit_sid_from_status "Second loose")
gl_capture fold "$first_sid" "$second_sid" n-roll-dst
assert_exit_fail "$CODE" "move_rollback_fails"
# It must fail on the replay, not for some setup reason that would leave the
# refs untouched anyway and pass the checks below for free.
assert_contains "$OUT" "conflicts" "move_rollback_reason"
assert_eq "$(head_hash)" "$head_before" "move_rollback_head"
assert_eq "$(git -C "$WORK" rev-parse n-roll-dst)" "$dst_before" "move_rollback_dst"
assert_eq "$(git -C "$WORK" status --porcelain | sort)" "$porcelain_before" "move_rollback_index"
assert_no_state_file "move_rollback_no_state"

describe "move: a branch that landed upstream leaves its commit movable"
setup_repo_with_remote
create_feature_branch "q-landed"
switch_to q-landed
commit_file "Landed one" "landed.txt"
switch_to integration
weave_branch "q-landed"
create_feature_branch "q-other"
switch_to q-other
commit_file "Other one" "q-other.txt"
switch_to integration
weave_branch "q-other"
# q-landed lands upstream as a fast-forward, so the merge-base becomes its own
# tip — a commit that is still inside the weave. The scope check has to use the
# weave's base, not the merge-base, or it refuses a commit loom can move.
git -C "$WORK" push -q origin q-landed:"$BASE_BRANCH"
git -C "$WORK" fetch -q origin
landed=$(git -C "$WORK" rev-parse q-landed)
assert_eq "$(git -C "$WORK" merge-base HEAD "origin/$BASE_BRANCH")" "$landed" "move_landed_precond"
gl_capture fold "$landed" q-other
assert_exit_ok "$CODE" "move_landed_ok"
assert_eq "$(git -C "$WORK" log -1 --format=%s q-other)" "Landed one" "move_landed_on_target"

describe "create: a branch that landed upstream leaves -c working"
setup_repo_with_remote
create_feature_branch "r-landed"
switch_to r-landed
commit_file "Landed one" "r-landed.txt"
switch_to integration
weave_branch "r-landed"
create_feature_branch "r-other"
switch_to r-other
commit_file "Other one" "r-other.txt"
switch_to integration
weave_branch "r-other"
# The merge-base becomes r-landed's own tip, a commit still inside the weave.
# Creating there would root the new branch on the landed commit and dissolve
# r-landed's merge along with it, so `-c` creates at the weave's base instead —
# the same base the scope check uses.
weave_base=$(git -C "$WORK" rev-parse "origin/$BASE_BRANCH")
git -C "$WORK" push -q origin r-landed:"$BASE_BRANCH"
git -C "$WORK" fetch -q origin
other_tip=$(git -C "$WORK" rev-parse r-other)
gl_capture fold -c "$other_tip" r-created
assert_exit_ok "$CODE" "create_landed_ok"
assert_branch_exists "r-created" "create_landed_branch"
assert_eq "$(git -C "$WORK" log -1 --format=%s r-created)" "Other one" "create_landed_commit"
# Its own section at the weave base: the commit that landed is not its parent,
# and r-landed keeps the merge it was woven in by.
assert_eq "$(git -C "$WORK" rev-parse r-created^)" "$weave_base" "create_landed_parent"
assert_log_contains "Merge r-landed" "create_landed_keeps_merge"

describe "move: the same commit named twice stays resumable"
setup_repo_with_remote
create_feature_branch "p-dup-dst"
switch_to p-dup-dst
commit_file "Dst base" "dst-dup.txt"
switch_to integration
weave_branch "p-dup-dst"
create_feature_branch "p-dup-other"
switch_to p-dup-other
commit_file "Other one" "other-dup.txt"
switch_to integration
weave_branch "p-dup-other"
commit_file "Dup loose" "other-dup.txt"
dup_sid=$(commit_sid_from_status "Dup loose")
# Named twice is still one commit, so this keeps the resumable single-commit
# path: it pauses with a state file rather than rolling back like a stack.
gl_capture fold "$dup_sid" "$dup_sid" p-dup-dst
assert_state_file "move_dup_state"
gl_capture abort
assert_exit_ok "$CODE" "move_dup_abort_ok"
assert_no_state_file "move_dup_state_removed"

describe "move: target is a branch stacked inside another"
setup_repo_with_remote
create_feature_branch "k-move-outer"
switch_to k-move-outer
commit_file "Inner one" "inner-one.txt"
git -C "$WORK" branch k-move-inner
commit_file "Outer one" "outer-one.txt"
switch_to integration
weave_branch "k-move-outer"
create_feature_branch "l-move-src"
switch_to l-move-src
commit_file "Move to inner" "move-to-inner.txt"
switch_to integration
weave_branch "l-move-src"
move_sid=$(commit_sid_from_status "Move to inner")
out=$(gl fold "$move_sid" k-move-inner)
assert_exit_ok $? "move_inner_ok"
assert_contains "$out" "branch l-move-src now empty, at the base" "move_inner_src_parked"
assert_eq "$(git -C "$WORK" log -1 --format=%s k-move-inner)" "Move to inner" "move_inner_tip"
assert_eq "$(git -C "$WORK" log -1 --format=%s k-move-inner~1)" "Inner one" "move_inner_after_old_tip"
assert_eq "$(git -C "$WORK" log -1 --format=%s k-move-outer)" "Outer one" "move_inner_outer_tip"
assert_eq "$(git -C "$WORK" rev-parse k-move-outer~1)" "$(git -C "$WORK" rev-parse k-move-inner)" "move_inner_outer_on_top"
assert_eq "$(git -C "$WORK" rev-parse l-move-src)" "$(upstream_oid)" "move_inner_src_at_base"

describe "move: co-located target branches — only the target advances"
setup_repo_with_remote
# g-move-to-coloc is the commit source
create_feature_branch "g-move-to-coloc"
switch_to g-move-to-coloc
commit_file "Will be moved" "will-be-moved.txt"
switch_to integration
weave_branch "g-move-to-coloc"
# h-coloc-dst and i-coloc-dst share the same tip (co-located)
create_feature_branch "h-coloc-dst"
switch_to h-coloc-dst
commit_file "Coloc target existing" "coloc-target.txt"
switch_to integration
git -C "$WORK" branch i-coloc-dst h-coloc-dst
weave_branch "h-coloc-dst"
# i-coloc-dst-oid should NOT change after we move the commit to h-coloc-dst
coloc_oid_before=$(branch_oid "i-coloc-dst")
move_sid=$(commit_sid_from_status "Will be moved")
out=$(gl fold "$move_sid" h-coloc-dst)
assert_exit_ok $? "move_coloc_target_ok"
assert_contains "$(git -C "$WORK" log h-coloc-dst --oneline)" "Will be moved" "move_coloc_target_on_dst"
assert_eq "$coloc_oid_before" "$(branch_oid i-coloc-dst)" "move_coloc_target_colocated_unaffected"

describe "move: uncommitted changes are preserved"
setup_repo_with_remote
create_feature_branch "g-move-preserve"
switch_to g-move-preserve
commit_file "Preserve move" "preserve-move.txt"
switch_to integration
weave_branch "g-move-preserve"
create_feature_branch "h-move-preserve-dst"
switch_to h-move-preserve-dst
commit_file "Preserve dst" "preserve-dst.txt"
switch_to integration
weave_branch "h-move-preserve-dst"
write_file "preserve-move.txt" "dirty during move"
move_sid=$(commit_sid_from_status "Preserve move")
out=$(gl fold "$move_sid" h-move-preserve-dst)
assert_exit_ok $? "move_preserve_wt_ok"
assert_file_content "preserve-move.txt" "dirty during move" "move_preserve_wt_content"

# ── CASE 4: COMMIT + zz (UNCOMMIT) ────────────────────────────────────────────

describe "uncommit: HEAD commit removed, changes land in working directory"
setup_repo_with_remote
commit_file "Uncommit me" "uncommit.txt"
old_hash="$(head_hash)"
commit_file "Stay in history" "stay.txt"
uncommit_hash="$(git -C "$WORK" rev-parse HEAD~1)"
uncommit_sid=$(commit_sid_from_status "Uncommit me")
out=$(gl fold "$uncommit_sid" zz)
assert_exit_ok $? "uncommit_non_head_ok"
assert_commit_not_in_log "$uncommit_hash" "uncommit_non_head_gone"
assert_log_contains "Stay in history" "uncommit_non_head_stay"
# The uncommitted change should be in the working directory
status_out=$(gl status)
assert_contains "$status_out" "uncommit.txt" "uncommit_non_head_in_wt"

describe "uncommit: HEAD commit (mixed reset path)"
setup_repo_with_remote
commit_file "Uncommit HEAD" "uncommit-head.txt"
old_hash="$(head_hash)"
head_sid=$(commit_sid_from_status "Uncommit HEAD")
out=$(gl fold "$head_sid" zz)
assert_exit_ok $? "uncommit_head_ok"
assert_commit_not_in_log "$old_hash" "uncommit_head_gone"
# File should be unstaged in working directory
status_out=$(gl status)
assert_contains "$status_out" "uncommit-head.txt" "uncommit_head_in_wt"

describe "uncommit: existing uncommitted changes are preserved"
setup_repo_with_remote
commit_file "Uncommit preserve" "uncommit-preserve.txt"
uncommit_sid=$(commit_sid_from_status "Uncommit preserve")
commit_file "Stay commit" "stay-commit.txt"
write_file "stay-commit.txt" "dirty during uncommit"
out=$(gl fold "$uncommit_sid" zz)
assert_exit_ok $? "uncommit_preserve_ok"
assert_file_content "stay-commit.txt" "dirty during uncommit" "uncommit_preserve_wt"

# ── CASE 5: COMMITFILE + zz (UNCOMMIT FILE) ───────────────────────────────────

describe "uncommit-file: remove one file from a commit to working directory"
setup_repo_with_remote
# Create a commit touching two files
echo "auth content" > "$WORK/auth.txt"
echo "main content" > "$WORK/main.txt"
git -C "$WORK" add auth.txt main.txt
git -C "$WORK" commit -q -m "Two file commit"
# Get the commitfile ref for main.txt (index 1)
cf_ref=$(gl status -f | grep "main.txt" | grep -oE '[0-9a-z]+:[0-9]+' | head -1)
old_hash="$(head_hash)"
out=$(gl fold "$cf_ref" zz)
assert_exit_ok $? "uncommit_file_ok"
assert_ne "$old_hash" "$(head_hash)" "uncommit_file_commit_rewrote"
assert_head_msg "Two file commit" "uncommit_file_msg_preserved"
# main.txt changes should be in working directory
status_out=$(gl status)
assert_contains "$status_out" "main.txt" "uncommit_file_in_wt"

describe "uncommit-file: non-HEAD commit, file removed and returned to working dir"
setup_repo_with_remote
echo "feat-a" > "$WORK/feat-a.txt"
echo "feat-b" > "$WORK/feat-b.txt"
git -C "$WORK" add feat-a.txt feat-b.txt
git -C "$WORK" commit -q -m "Two feat commit"
# Make a descendant commit
commit_file "Descendant commit" "desc.txt"
cf_ref=$(gl status -f | grep "feat-b.txt" | grep -oE '[0-9a-z]+:[0-9]+' | head -1)
out=$(gl fold "$cf_ref" zz)
assert_exit_ok $? "uncommit_file_non_head_ok"
assert_head_msg "Descendant commit" "uncommit_file_non_head_top_msg"
status_out=$(gl status)
assert_contains "$status_out" "feat-b.txt" "uncommit_file_non_head_in_wt"

describe "uncommit-file: other files in the same commit are preserved"
setup_repo_with_remote
echo "keep content" > "$WORK/keep.txt"
echo "extract content" > "$WORK/extract.txt"
git -C "$WORK" add keep.txt extract.txt
git -C "$WORK" commit -q -m "Keep and extract commit"
cf_ref=$(gl status -f | grep "extract.txt" | grep -oE '[0-9a-z]+:[0-9]+' | head -1)
out=$(gl fold "$cf_ref" zz)
assert_exit_ok $? "uncommit_file_other_ok"
# keep.txt must still be part of the commit (not in working dir as modified)
status_out=$(gl status)
assert_not_contains "$status_out" "keep.txt" "uncommit_file_keep_in_commit"

# ── CASE 6: COMMITFILE + COMMIT (MOVE FILE) ───────────────────────────────────

describe "move-file: move a file's changes from one commit to another"
setup_repo_with_remote
commit_file "Commit Alpha" "alpha.txt"
echo "other content" > "$WORK/other.txt"
git -C "$WORK" add other.txt
git -C "$WORK" commit -q -m "Commit Beta"
# other.txt is in Commit Beta but should be in Commit Alpha
cf_ref=$(gl status -f | grep "other.txt" | grep -oE '[0-9a-z]+:[0-9]+' | head -1)
alpha_sid=$(commit_sid_from_status "Commit Alpha")
out=$(gl fold "$cf_ref" "$alpha_sid")
assert_exit_ok $? "move_file_ok"
assert_contains "$(git -C "$WORK" show HEAD~1 --name-only)" "other.txt" "move_file_in_target"
assert_not_contains "$(git -C "$WORK" show HEAD --name-only)" "other.txt" "move_file_not_in_src"
assert_head_msg "Commit Beta" "move_file_src_msg_preserved"

describe "move-file: source and target must be different commits"
setup_repo_with_remote
commit_file "Same commit" "same.txt"
cf_ref=$(gl status -f | grep "same.txt" | grep -oE '[0-9a-z]+:[0-9]+' | head -1)
same_sid=$(commit_sid_from_status "Same commit")
gl_capture fold "$cf_ref" "$same_sid"
assert_exit_fail "$CODE" "move_file_same_commit_fail"
assert_contains "$OUT" "Source and target are the same commit" "move_file_same_commit_msg"

describe "move-file: uncommitted changes are preserved"
setup_repo_with_remote
commit_file "Move file preserve src" "mfp-src.txt"
echo "mfp other" > "$WORK/mfp-other.txt"
git -C "$WORK" add mfp-other.txt
git -C "$WORK" commit -q -m "Move file preserve dst"
write_file "mfp-src.txt" "dirty during move file"
cf_ref=$(gl status -f | grep "mfp-other.txt" | grep -oE '[0-9a-z]+:[0-9]+' | head -1)
target_sid=$(commit_sid_from_status "Move file preserve src")
out=$(gl fold "$cf_ref" "$target_sid")
assert_exit_ok $? "move_file_preserve_wt_ok"
assert_file_content "mfp-src.txt" "dirty during move file" "move_file_preserve_wt_content"

# ── --CREATE FLAG (NEW BRANCH FROM COMMIT) ────────────────────────────────────

describe "create: move commit to a new branch"
setup_repo_with_remote
commit_file "Loose commit" "loose.txt"
loose_sid=$(commit_sid_from_status "Loose commit")
out=$(gl fold --create "$loose_sid" g-new-branch)
assert_exit_ok $? "create_ok"
assert_branch_exists "g-new-branch" "create_branch_exists"
assert_contains "$(git -C "$WORK" log g-new-branch --oneline)" "Loose commit" "create_commit_on_branch"

describe "create: target branch already exists — refused"
setup_repo_with_remote
commit_file "Existing branch commit" "existing.txt"
loose_sid=$(commit_sid_from_status "Existing branch commit")
create_feature_branch "g-already-exists"
gl_capture fold --create "$loose_sid" g-already-exists
assert_exit_fail "$CODE" "create_branch_exists_refused"
assert_contains "$OUT" "already exists" "create_branch_exists_msg"
assert_contains "$OUT" "loom fold" "create_branch_exists_hint"
assert_not_contains "$(git -C "$WORK" log g-already-exists --oneline)" "Existing branch commit" "create_branch_exists_untouched"

describe "create: using full hash as source"
setup_repo_with_remote
commit_file "Create from hash" "hash-create.txt"
loose_hash="$(head_hash)"
out=$(gl fold --create "$loose_hash" h-from-hash)
assert_exit_ok $? "create_full_hash_ok"
assert_branch_exists "h-from-hash" "create_full_hash_branch_exists"
assert_contains "$(git -C "$WORK" log h-from-hash --oneline)" "Create from hash" "create_full_hash_on_branch"

# ── EMPTIED BRANCHES ──────────────────────────────────────────────────────────

describe "uncommit the only commit of a stacked branch: branch parked at the base"
setup_repo_with_remote
base_hash=$(git -C "$WORK" rev-parse @{u})
create_feature_branch "inner"
switch_to inner
commit_file "Inner I1" "inner1.txt"
i1_hash=$(head_hash)
git -C "$WORK" branch outer inner
switch_to outer
commit_file "Outer O1" "outer1.txt"
switch_to integration
weave_branch "outer"
out=$(gl fold "$i1_hash" zz)
assert_exit_ok $? "uncommit_inner_ok"
assert_contains "$out" "branch inner now empty" "uncommit_inner_msg"
assert_eq "$(git -C "$WORK" rev-parse inner)" "$base_hash" "uncommit_inner_parked"
assert_eq "$(git -C "$WORK" log --format=%s -1 outer)" "Outer O1" "uncommit_inner_outer_kept"
assert_eq "$(git -C "$WORK" rev-parse outer^)" "$base_hash" "uncommit_inner_gone_from_outer"
assert_file_content "inner1.txt" "Inner I1" "uncommit_inner_file_in_worktree"

describe "uncommit the only commit of a woven branch: merge gone, branch at the base"
setup_repo_with_remote
base_hash=$(git -C "$WORK" rev-parse @{u})
create_feature_branch "solo"
switch_to solo
commit_file "Solo S1" "solo1.txt"
s1_hash=$(head_hash)
switch_to integration
weave_branch "solo"
out=$(gl fold "$s1_hash" zz)
assert_exit_ok $? "uncommit_solo_ok"
assert_eq "$(git -C "$WORK" rev-parse solo)" "$base_hash" "uncommit_solo_parked"
assert_eq "$(head_hash)" "$base_hash" "uncommit_solo_merge_gone"
assert_branch_exists "solo" "uncommit_solo_branch_kept"

describe "move the only commit of a stacked branch: inner branch stays behind"
setup_repo_with_remote
base_hash=$(git -C "$WORK" rev-parse @{u})
create_feature_branch "inner"
switch_to inner
commit_file "Inner I1" "inner1.txt"
i1_hash=$(head_hash)
git -C "$WORK" branch outer inner
switch_to outer
commit_file "Outer O1" "outer1.txt"
switch_to integration
create_feature_branch "other"
switch_to other
commit_file "Other X1" "other1.txt"
switch_to integration
weave_branch "outer"
weave_branch "other"
out=$(gl fold "$i1_hash" other)
assert_exit_ok $? "move_inner_ok"
assert_contains "$out" "branch inner now empty" "move_inner_msg"
assert_eq "$(git -C "$WORK" rev-parse inner)" "$base_hash" "move_inner_parked"
assert_eq "$(git -C "$WORK" log --format=%s -1 other)" "Inner I1" "move_inner_on_other"
assert_eq "$(git -C "$WORK" log --format=%s -1 outer)" "Outer O1" "move_inner_outer_kept"

# ── GIT ARGUMENT FORWARDING ───────────────────────────────────────────────────

# The global config may point core.hooksPath elsewhere; aim it back at the repo.
install_failing_pre_commit() {
  mkdir -p "$WORK/.git/hooks"
  git -C "$WORK" config core.hooksPath "$WORK/.git/hooks"
  printf '#!/bin/sh\nexit 1\n' > "$WORK/.git/hooks/pre-commit"
  chmod +x "$WORK/.git/hooks/pre-commit"
}

describe "amend: a pre-commit hook is skipped with -- --no-verify"
setup_repo_with_remote
commit_file "Hooked base" "hooked.txt"
install_failing_pre_commit
write_file "hooked.txt" "amended"
gl_capture fold hooked.txt HEAD
assert_exit_fail "$CODE" "fold_hook_blocks"
gl_capture fold hooked.txt HEAD -- --no-verify
assert_exit_ok "$CODE" "fold_no_verify_ok"
assert_eq "amended" "$(git -C "$WORK" show HEAD:hooked.txt)" "fold_no_verify_amended"

describe "fixup: the hook on the non-HEAD path is skipped too"
setup_repo_with_remote
commit_file "H1" "h1.txt"
h1_hash=$(head_hash)
commit_file "H2" "h2.txt"
install_failing_pre_commit
write_file "h1.txt" "folded"
gl_capture fold h1.txt "$h1_hash"
assert_exit_fail "$CODE" "fold_fixup_hook_blocks"
gl_capture fold h1.txt "$h1_hash" -- --no-verify
assert_exit_ok "$CODE" "fold_fixup_no_verify_ok"
assert_eq "folded" "$(git -C "$WORK" show HEAD~1:h1.txt)" "fold_fixup_no_verify_content"
assert_log_contains "H2" "fold_fixup_no_verify_keeps_the_commit_above"

describe "a fold that only rebases takes no arguments after --"
setup_repo_with_remote
commit_file "R1" "r1.txt"
r1_hash=$(head_hash)
commit_file "R2" "r2.txt"
r2_hash=$(head_hash)
gl_capture fold "$r2_hash" "$r1_hash" -- --no-verify
assert_exit_fail "$CODE" "fold_rebase_only_fail"
assert_contains "$OUT" 'runs no git commit' "fold_rebase_only_msg"

describe "a forwarded --amend on the fixup path rolls back"
setup_repo_with_remote
commit_file "P1" "p1.txt"
p1_hash=$(head_hash)
commit_file "P2" "p2.txt"
write_file "p1.txt" "changed"
gl_capture fold p1.txt "$p1_hash" -- --amend
assert_exit_fail "$CODE" "fold_amend_fail"
assert_contains "$OUT" "left no new commit on HEAD" "fold_amend_msg"
assert_log_contains "P2" "fold_amend_history_intact"
assert_eq "changed" "$(cat "$WORK/p1.txt")" "fold_amend_change_kept"

describe "a forwarded --dry-run leaves the fixup path nothing to squash"
setup_repo_with_remote
commit_file "D1" "d1.txt"
d1_hash=$(head_hash)
commit_file "D2" "d2.txt"
write_file "d1.txt" "changed"
gl_capture fold d1.txt "$d1_hash" -- --dry-run
assert_exit_fail "$CODE" "fold_fixup_dry_run_fail"
assert_contains "$OUT" "left no new commit on HEAD" "fold_fixup_dry_run_msg"
assert_log_contains "D2" "fold_fixup_dry_run_history_intact"
assert_eq "changed" "$(cat "$WORK/d1.txt")" "fold_fixup_dry_run_change_kept"

describe "an empty fixup commit is refused before the squash"
setup_repo_with_remote
commit_file "E1" "e1.txt"
e1_hash=$(head_hash)
commit_file "E2" "e2.txt"
write_file "e1.txt" "changed"
gl_capture fold e1.txt "$e1_hash" -- --only --allow-empty
assert_exit_fail "$CODE" "fold_empty_fixup_fail"
assert_contains "$OUT" "empty" "fold_empty_fixup_msg"
assert_log_contains "E2" "fold_empty_fixup_history_intact"
assert_eq "changed" "$(cat "$WORK/e1.txt")" "fold_empty_fixup_change_kept"

describe "a forwarded --dry-run leaves the amend to roll back"
setup_repo_with_remote
commit_file "Dry base" "dry.txt"
write_file "dry.txt" "changed"
gl_capture fold dry.txt HEAD -- --dry-run
assert_exit_fail "$CODE" "fold_dry_run_fail"
assert_contains "$OUT" "nothing was amended" "fold_dry_run_msg"

describe "a status format is a dry run, and rolls back like one"
gl_capture fold dry.txt HEAD -- --porcelain
assert_exit_fail "$CODE" "fold_porcelain_fail"
assert_contains "$OUT" "nothing was amended" "fold_porcelain_msg"
assert_eq "changed" "$(cat "$WORK/dry.txt")" "fold_porcelain_change_kept"

# ── CONTINUE / ABORT ──────────────────────────────────────────────────────────
# Shared conflict setup: C1 changes A→B, C2 changes B→C.
# Uncommitting C1 (fold zz) drops it from history, forcing C2 to cherry-pick
# onto A → 3-way merge conflict.

describe "fold uncommit: conflict → continue → commit removed from history"
setup_repo_with_remote
printf "A\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "Base"
printf "B\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "C1"
printf "C\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "C2"
c1_hash=$(git -C "$WORK" rev-parse HEAD~1)
gl_capture fold "$c1_hash" zz
assert_state_file   "fold_cont_state"
assert_contains "$OUT" "loom continue" "fold_cont_hint"
printf "resolved\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt
gl_capture continue
assert_exit_ok  "$CODE" "fold_cont_ok"
assert_no_state_file   "fold_cont_state_removed"
assert_contains "$OUT" "Uncommitted" "fold_cont_msg"
assert_log_not_contains "C1" "fold_cont_c1_gone"

describe "fold uncommit: conflict → abort → original state restored"
setup_repo_with_remote
printf "A\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "Base"
printf "B\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "C1"
printf "C\n" > "$WORK/shared.txt"; git -C "$WORK" add shared.txt; git -C "$WORK" commit -q -m "C2"
c1_hash=$(git -C "$WORK" rev-parse HEAD~1)
old_head=$(head_hash)
gl_capture fold "$c1_hash" zz
assert_state_file "fold_abort_state"
gl_capture abort
assert_exit_ok  "$CODE" "fold_abort_ok"
assert_contains "$OUT" "Aborted" "fold_abort_msg"
assert_no_state_file   "fold_abort_state_removed"
assert_eq "$old_head" "$(head_hash)" "fold_abort_head_restored"
assert_log_contains "C1" "fold_abort_c1_preserved"

pass
