冲突处理
本节目标
从上一节的 C4 同时在 main 和功能分支修改同一个占位段落,产生一次确定的文本冲突。你将识别冲突标记,了解如何放弃未完成的合并,并将双方有效信息整理为规范的 C7。
开始前状态
目标必须是 GIT-03 的精确 C4:当前只有 main,最新主题是 merge: integrate git workflow,它的第一父是 C2,第二父是主题为 docs: record branch workflow 的 C3。工作区与 index 干净,没有 remote、额外引用或未完成的 merge/rebase。
必要原理
当两条历史都修改了同一段文本,Git 无法替你决定哪个意图更正确。它会停在合并中间态,保留 MERGE_HEAD,并在文件中写入三类标记:<<<<<<< HEAD 开始当前 main 的内容,======= 分隔两边,>>>>>>> feature/troubleshooting-guide 结束待合并分支的内容。
冲突标记是待处理的输入,不是最终答案。解决时要先理解双方意图,再删除标记并写入最终内容。
动手完成
迁移先用完整 refs/*、精确对象 ID、文件 SHA-256 和物理路径锁定 C4。它在功能分支生成 C6,在 main 生成 C5,然后要求合并必须失败为预期冲突。冲突文件的 identity 绑定后才写入 C7 规范结果;若路径被外来链接或 inode 替换,它保留诊断现场并失败。
课程文件先以非截断方式打开并绑定目标 FD。Linux 通过已经绑定的 /dev/fd/7 截断;macOS 的 /dev/fd 不接受带截断标志的重开,因此 Darwin 分支只在确认 /usr/bin/perl 可执行后,对继承的 FD 7 调用 ftruncate,绝不向适配器传入 pathname。适配器缺失或平台未知时会在写入前失败。
advance_git_conflict_resolution() (
set -euo pipefail
devlab_input="${DEVLAB_DIR:-}"
test -n "$devlab_input" || { printf 'DEVLAB_DIR 必须是明确的练习目录。\n' >&2; exit 1; }
test -d "$devlab_input" && test ! -L "$devlab_input" || exit 1
devlab_logical="$(cd -- "$devlab_input" && pwd -L)" || exit 1
devlab_dir="$(cd -- "$devlab_input" && pwd -P)" || exit 1
test "$devlab_logical" = "$devlab_dir" && test "$devlab_dir" != / || exit 1
test -d "$devlab_dir/.git" && test ! -L "$devlab_dir/.git" || exit 1
notes_target="$devlab_dir/notes/git-notes.md"
test -d "$devlab_dir/notes" && test ! -L "$devlab_dir/notes" || exit 1
test -f "$notes_target" && test ! -L "$notes_target" || exit 1
git_bin="$(PATH=/usr/bin:/bin command -v git)" || exit 1
read_local_config() {
/usr/bin/env -i PATH=/usr/bin:/bin LC_ALL=C HOME="$devlab_dir" \
GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
GIT_ATTR_NOSYSTEM=1 GIT_PAGER=cat GIT_OPTIONAL_LOCKS=0 \
"$git_bin" -C "$devlab_dir" config --local --no-includes "$@"
}
run_git() {
/usr/bin/env -i PATH=/usr/bin:/bin LC_ALL=C HOME="$devlab_dir" \
GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
GIT_ATTR_NOSYSTEM=1 GIT_PAGER=cat GIT_OPTIONAL_LOCKS=0 \
"$git_bin" -c core.hooksPath=/dev/null -c core.fsmonitor=false \
-c core.worktree="$devlab_dir" -c diff.external= -C "$devlab_dir" "$@"
}
file_hash() {
local output
if command -v sha256sum >/dev/null 2>&1; then output="$(sha256sum -- "$1")" || return 1
elif command -v shasum >/dev/null 2>&1; then output="$(shasum -a 256 -- "$1")" || return 1
else return 1
fi
printf '%s' "${output%% *}"
}
file_identity() {
local identity
if identity="$(LC_ALL=C stat -L -c '%d:%i' "$1" 2>/dev/null)"; then :
else identity="$(LC_ALL=C stat -L -f '%d:%i' "$1")" || return 1
fi
printf '%s' "$identity"
}
file_inode() {
local inode
if inode="$(LC_ALL=C stat -L -c '%i' "$1" 2>/dev/null)"; then :
else inode="$(LC_ALL=C stat -L -f '%i' "$1")" || return 1
fi
printf '%s' "$inode"
}
file_link_count() {
local links
if links="$(LC_ALL=C stat -L -c '%h' "$1" 2>/dev/null)"; then :
else links="$(LC_ALL=C stat -L -f '%l' "$1")" || return 1
fi
printf '%s' "$links"
}
fd_matches_course_identity() {
local fd_path="$1" target="$2" expected_identity="$3" fd_inode path_inode fd_links path_links path_identity
fd_inode="$(file_inode "$fd_path")" || return 1
path_inode="$(file_inode "$target")" || return 1
fd_links="$(file_link_count "$fd_path")" || return 1
path_links="$(file_link_count "$target")" || return 1
path_identity="$(file_identity "$target")" || return 1
test "$fd_inode" = "$path_inode" && test "$fd_links" -gt 0 && test "$fd_links" = "$path_links" \
&& test "$path_identity" = "$expected_identity"
}
open_course_rewrite_fd() {
local target="$1" expected_identity="$2" expected_hash="${3:-}" actual_hash
test -f "$target" && test ! -L "$target" || return 1
exec 7<> "$target" || return 1
fd_matches_course_identity /dev/fd/7 "$target" "$expected_identity" || { exec 7>&-; return 1; }
if test -n "$expected_hash"; then
actual_hash="$(file_hash "$target")" || { exec 7>&-; return 1; }
test "$actual_hash" = "$expected_hash" || { exec 7>&-; return 1; }
fd_matches_course_identity /dev/fd/7 "$target" "$expected_identity" || { exec 7>&-; return 1; }
fi
}
truncate_bound_course_fd() {
local platform="$1"
case "$platform" in
Darwin)
test -x /usr/bin/perl || return 1
/usr/bin/perl -e 'truncate(STDOUT, 0) or exit 1' >&7
;;
Linux) : > /dev/fd/7 ;;
*) return 1 ;;
esac
}
begin_bound_course_write() { :; }
begin_bound_course_rollback_write() { :; }
finish_course_write_fd() {
local target="$1" expected_identity="$2" expected_hash="$3" actual_hash
fd_matches_course_identity /dev/fd/7 "$target" "$expected_identity" || { exec 7>&-; return 1; }
exec 7>&-
actual_hash="$(file_hash "$target")" || return 1
test "$actual_hash" = "$expected_hash" || return 1
test "$(file_identity "$target")" = "$expected_identity" || return 1
}
blob_hash() {
local output
if command -v sha256sum >/dev/null 2>&1; then output="$(run_git cat-file blob "$1" | sha256sum)" || return 1
elif command -v shasum >/dev/null 2>&1; then output="$(run_git cat-file blob "$1" | shasum -a 256)" || return 1
else return 1
fi
printf '%s' "${output%% *}"
}
verify_local_config_contract() {
local config_names config_name optional_value
config_names="$(read_local_config --name-only --list)" || return 1
while IFS= read -r config_name; do
case "$config_name" in
core.repositoryformatversion|core.filemode|core.bare|core.logallrefupdates|core.ignorecase|core.precomposeunicode|core.hookspath|core.fsmonitor|user.name|user.email) ;;
*) printf '仓库配置包含课程未允许的键:%s\n' "$config_name" >&2; return 1 ;;
esac
done <<< "$config_names"
test "$(read_local_config --get-all core.repositoryformatversion)" = 0 || return 1
case "$(read_local_config --get-all core.filemode)" in true|false) ;; *) return 1 ;; esac
test "$(read_local_config --get-all core.bare)" = false || return 1
test "$(read_local_config --get-all core.logallrefupdates)" = true || return 1
test "$(read_local_config --get-all core.hooksPath)" = /dev/null || return 1
test "$(read_local_config --get-all core.fsmonitor)" = false || return 1
test "$(read_local_config --get-all user.name)" = 'Dev Lab' || return 1
test "$(read_local_config --get-all user.email)" = 'dev-lab@example.invalid' || return 1
for optional_value in core.ignorecase core.precomposeunicode; do
case "$(read_local_config --get-all "$optional_value" 2>/dev/null || true)" in ''|true|false) ;; *) return 1 ;; esac
done
}
verify_git_tree_manifest() {
local commit="$1" notes_hash="$2" expected_paths actual_manifest metadata relative_path mode type blob_oid expected_oid seen_paths=''
expected_paths='.gitignore
README.md
config/bashrc.dev-lab
config/env.dev-lab
data/access.log
data/health-events.log
notes/git-notes.md
notes/linux-notes.md
notes/shell-notes.md
output/health-report.txt
output/summary.txt
scripts/health-check.sh
scripts/permission-demo.sh
scripts/report.sh'
actual_manifest="$(run_git ls-tree -r "$commit")" || return 1
while IFS=$'\t' read -r metadata relative_path; do
set -- $metadata
test "$#" -eq 3 || return 1
mode="$1"; type="$2"; blob_oid="$3"
test "$type" = blob || return 1
case "$relative_path" in
.gitignore) expected_oid=ff6f87d4c48b221958e3f56a137fc3ff63be1fd5; test "$mode" = 100644 ;;
README.md) expected_oid=ac9a5a22aabc4a25cf371486326d0f8fc9c15076; test "$mode" = 100644 ;;
config/bashrc.dev-lab) expected_oid=e72213d91e299fe7783a31c4e95fdd54efb07fae; test "$mode" = 100644 ;;
config/env.dev-lab) expected_oid=53b6a1452902b3823ea78d96540d75f30c07d8a6; test "$mode" = 100644 ;;
data/access.log) expected_oid=2818d31895effbe22063a99a0c4da38e75e1f5b6; test "$mode" = 100644 ;;
data/health-events.log) expected_oid=61360929cb9cab8764ec5fc8ae2c3225385098d9; test "$mode" = 100644 ;;
notes/git-notes.md) expected_oid="$blob_oid"; test "$mode" = 100644 && test "$(blob_hash "$blob_oid")" = "$notes_hash" ;;
notes/linux-notes.md) expected_oid=9caebd63116c9ab87240e2c3324768533ee1cc6d; test "$mode" = 100644 ;;
notes/shell-notes.md) expected_oid=cc013334ea8e5bce8d419c0ea39cac1d348ebcdc; test "$mode" = 100644 ;;
output/health-report.txt) expected_oid=22f76e9e3cb4f9df91c98a918cb560a2e2e4461a; test "$mode" = 100644 ;;
output/summary.txt) expected_oid=26de3df8058ee2a0582284c451905026b8ee7a8b; test "$mode" = 100644 ;;
scripts/health-check.sh) expected_oid=60607552089d0082f930ba243df23802b3627ba0; test "$mode" = 100755 ;;
scripts/permission-demo.sh) expected_oid=87e6bf27dd8f4c2029951ef72bb800678d23b640; test "$mode" = 100755 ;;
scripts/report.sh) expected_oid=ae08bbe9f23450b9676b2c87a70451525bd9851f; test "$mode" = 100755 ;;
*) return 1 ;;
esac || return 1
test "$blob_oid" = "$expected_oid" || return 1
seen_paths="${seen_paths}${seen_paths:+$'\n'}${relative_path}"
done <<< "$actual_manifest"
test "$seen_paths" = "$expected_paths" || return 1
}
verify_git_notes_blob() {
local commit="$1" expected_hash="$2" entry blob_oid
entry="$(run_git ls-tree "$commit" -- notes/git-notes.md)" || return 1
set -- $entry
test "$#" -eq 4 && test "$1" = 100644 && test "$2" = blob || return 1
blob_oid="$3"
test "$(blob_hash "$blob_oid")" = "$expected_hash" || return 1
}
write_c3_notes() {
printf '%s\n' '# Git 练习记录' '' '## 工作区模型' '' \
'- 工作区保存当前文件。' '- 暂存区保存下一次提交的候选快照。' '- HEAD 指向当前分支的提交。' '' \
'## 分支工作流' '' '- 在功能分支完成独立修改。' '- 合并前先确认当前分支和工作区状态。' '' \
'## 排错顺序' '' '课程将在冲突练习中补充这里。' '' '## 安全恢复' '' '课程将在恢复练习中补充这里。'
}
write_c5_notes() {
printf '%s\n' '# Git 练习记录' '' '## 工作区模型' '' \
'- 工作区保存当前文件。' '- 暂存区保存下一次提交的候选快照。' '- HEAD 指向当前分支的提交。' '' \
'## 分支工作流' '' '- 在功能分支完成独立修改。' '- 合并前先确认当前分支和工作区状态。' '' \
'## 排错顺序' '' '- 先查看状态与差异。' '' '## 安全恢复' '' '课程将在恢复练习中补充这里。'
}
write_c6_notes() {
printf '%s\n' '# Git 练习记录' '' '## 工作区模型' '' \
'- 工作区保存当前文件。' '- 暂存区保存下一次提交的候选快照。' '- HEAD 指向当前分支的提交。' '' \
'## 分支工作流' '' '- 在功能分支完成独立修改。' '- 合并前先确认当前分支和工作区状态。' '' \
'## 排错顺序' '' '- 先确认当前分支和目标。' '' '## 安全恢复' '' '课程将在恢复练习中补充这里。'
}
write_c7_notes() {
printf '%s\n' '# Git 练习记录' '' '## 工作区模型' '' \
'- 工作区保存当前文件。' '- 暂存区保存下一次提交的候选快照。' '- HEAD 指向当前分支的提交。' '' \
'## 分支工作流' '' '- 在功能分支完成独立修改。' '- 合并前先确认当前分支和工作区状态。' '' \
'## 排错顺序' '' '- 先查看状态与差异。' '- 再确认当前分支和目标。' '' \
'## 安全恢复' '' '课程将在恢复练习中补充这里。'
}
test -f "$devlab_dir/.git/config" && test ! -L "$devlab_dir/.git/config" || exit 1
test -f "$devlab_dir/.git/index" && test ! -L "$devlab_dir/.git/index" || exit 1
config_identity="$(file_identity "$devlab_dir/.git/config")" || exit 1
config_hash="$(file_hash "$devlab_dir/.git/config")" || exit 1
index_identity="$(file_identity "$devlab_dir/.git/index")" || exit 1
index_hash="$(file_hash "$devlab_dir/.git/index")" || exit 1
verify_local_config_contract || exit 1
test "$(file_identity "$devlab_dir/.git/config")" = "$config_identity" || exit 1
test "$(file_hash "$devlab_dir/.git/config")" = "$config_hash" || exit 1
test "$(run_git rev-parse --show-toplevel)" = "$devlab_dir" || exit 1
test "$(run_git branch --show-current)" = main || exit 1
test "$(run_git for-each-ref --format='%(refname)' refs)" = refs/heads/main || exit 1
test -z "$(run_git remote)" || exit 1
test -z "$(run_git status --porcelain=v1 --untracked-files=all)" || exit 1
for state_path in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD rebase-apply rebase-merge; do
test ! -e "$devlab_dir/.git/$state_path" && test ! -L "$devlab_dir/.git/$state_path" || exit 1
done
c4_head="$(run_git rev-parse HEAD)" || exit 1
test "$(run_git log -1 --format=%s "$c4_head")" = 'merge: integrate git workflow' || exit 1
test "$(run_git rev-list --count "$c4_head")" = 4 || exit 1
c2_head="$(run_git rev-parse "$c4_head^1")" || exit 1
c3_head="$(run_git rev-parse "$c4_head^2")" || exit 1
test "$(run_git log -1 --format=%s "$c2_head")" = 'docs: add git practice journal' || exit 1
test "$(run_git log -1 --format=%s "$c3_head")" = 'docs: record branch workflow' || exit 1
test "$(run_git rev-parse "$c3_head^")" = "$c2_head" || exit 1
test "$(file_hash "$notes_target")" = 9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 || exit 1
test "$(run_git hash-object "$notes_target")" = "$(run_git rev-parse "$c4_head:notes/git-notes.md")" || exit 1
verify_git_tree_manifest "$c2_head" 5250834cba284fc1d9017836f1ac056432d341020531b6c484fa6da4ae1834be || exit 1
verify_git_notes_blob "$c3_head" 9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 || exit 1
verify_git_tree_manifest "$c4_head" 9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 || exit 1
test "$(run_git diff-tree --no-commit-id --name-only -r "$c2_head" "$c3_head")" = notes/git-notes.md || exit 1
test "$(run_git rev-parse "$c4_head^{tree}")" = "$(run_git rev-parse "$c3_head^{tree}")" || exit 1
test "$(file_identity "$devlab_dir/.git/index")" = "$index_identity" || exit 1
test "$(file_hash "$devlab_dir/.git/index")" = "$index_hash" || exit 1
test "$(run_git write-tree)" = "$(run_git rev-parse "$c4_head^{tree}")" || exit 1
devlab_parent="${devlab_dir%/*}"; test -n "$devlab_parent" || devlab_parent=/
devlab_parent_identity="$(file_identity "$devlab_parent")" || exit 1
devlab_identity="$(file_identity "$devlab_dir")" || exit 1
git_identity="$(file_identity "$devlab_dir/.git")" || exit 1
notes_dir_identity="$(file_identity "$devlab_dir/notes")" || exit 1
notes_identity="$(file_identity "$notes_target")" || exit 1
feature_ref_oid=''; c5_head=''; c6_tip=''; c7_head=''; conflict_merge_handoff=0; conflict_file_verified=0
verify_conflict_index() {
local actual_index metadata relative_path mode blob_oid stage expected_oid expected_hash seen_entries=''
local expected_entries='.gitignore:0
README.md:0
config/bashrc.dev-lab:0
config/env.dev-lab:0
data/access.log:0
data/health-events.log:0
notes/git-notes.md:1
notes/git-notes.md:2
notes/git-notes.md:3
notes/linux-notes.md:0
notes/shell-notes.md:0
output/health-report.txt:0
output/summary.txt:0
scripts/health-check.sh:0
scripts/permission-demo.sh:0
scripts/report.sh:0'
actual_index="$(run_git ls-files --stage)" || return 1
while IFS=$'\t' read -r metadata relative_path; do
set -- $metadata
test "$#" -eq 3 || return 1
mode="$1"; blob_oid="$2"; stage="$3"
case "$relative_path" in
.gitignore) expected_oid=ff6f87d4c48b221958e3f56a137fc3ff63be1fd5; test "$mode:$stage" = 100644:0 ;;
README.md) expected_oid=ac9a5a22aabc4a25cf371486326d0f8fc9c15076; test "$mode:$stage" = 100644:0 ;;
config/bashrc.dev-lab) expected_oid=e72213d91e299fe7783a31c4e95fdd54efb07fae; test "$mode:$stage" = 100644:0 ;;
config/env.dev-lab) expected_oid=53b6a1452902b3823ea78d96540d75f30c07d8a6; test "$mode:$stage" = 100644:0 ;;
data/access.log) expected_oid=2818d31895effbe22063a99a0c4da38e75e1f5b6; test "$mode:$stage" = 100644:0 ;;
data/health-events.log) expected_oid=61360929cb9cab8764ec5fc8ae2c3225385098d9; test "$mode:$stage" = 100644:0 ;;
notes/git-notes.md)
expected_oid="$blob_oid"; test "$mode" = 100644 || return 1
case "$stage" in
1) expected_hash=9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 ;;
2) expected_hash=91407f0b23c8f27c9baee81d07d736673c2ff49d661379e63caa0c1479cd5860 ;;
3) expected_hash=f0aa82ddc8dad5b9f1072fc48f2b376385680a214659cb83cc596bfe03583022 ;;
*) return 1 ;;
esac
test "$(blob_hash "$blob_oid")" = "$expected_hash"
;;
notes/linux-notes.md) expected_oid=9caebd63116c9ab87240e2c3324768533ee1cc6d; test "$mode:$stage" = 100644:0 ;;
notes/shell-notes.md) expected_oid=cc013334ea8e5bce8d419c0ea39cac1d348ebcdc; test "$mode:$stage" = 100644:0 ;;
output/health-report.txt) expected_oid=22f76e9e3cb4f9df91c98a918cb560a2e2e4461a; test "$mode:$stage" = 100644:0 ;;
output/summary.txt) expected_oid=26de3df8058ee2a0582284c451905026b8ee7a8b; test "$mode:$stage" = 100644:0 ;;
scripts/health-check.sh) expected_oid=60607552089d0082f930ba243df23802b3627ba0; test "$mode:$stage" = 100755:0 ;;
scripts/permission-demo.sh) expected_oid=87e6bf27dd8f4c2029951ef72bb800678d23b640; test "$mode:$stage" = 100755:0 ;;
scripts/report.sh) expected_oid=ae08bbe9f23450b9676b2c87a70451525bd9851f; test "$mode:$stage" = 100755:0 ;;
*) return 1 ;;
esac || return 1
test "$blob_oid" = "$expected_oid" || return 1
seen_entries="${seen_entries}${seen_entries:+$'\n'}${relative_path}:${stage}"
done <<< "$actual_index"
test "$seen_entries" = "$expected_entries" || return 1
test "$(run_git diff --name-only --diff-filter=U)" = notes/git-notes.md || return 1
}
bind_owned_index() {
local expected_state="$1" index_tree candidate_index_identity candidate_index_hash
test -f "$devlab_dir/.git/index" && test ! -L "$devlab_dir/.git/index" || return 1
if test "$expected_state" = conflict; then
candidate_index_identity="$(file_identity "$devlab_dir/.git/index")" || return 1
verify_conflict_index || return 1
else
index_tree="$(run_git write-tree)" || return 1
candidate_index_identity="$(file_identity "$devlab_dir/.git/index")" || return 1
verify_git_tree_manifest "$index_tree" "$expected_state" || return 1
fi
candidate_index_hash="$(file_hash "$devlab_dir/.git/index")" || return 1
test "$(file_identity "$devlab_dir/.git/index")" = "$candidate_index_identity" || return 1
index_identity="$candidate_index_identity"
index_hash="$candidate_index_hash"
}
rebind_owned_config_after_git() {
local candidate_config_identity candidate_config_hash
test -f "$devlab_dir/.git/config" && test ! -L "$devlab_dir/.git/config" || return 1
candidate_config_identity="$(file_identity "$devlab_dir/.git/config")" || return 1
candidate_config_hash="$(file_hash "$devlab_dir/.git/config")" || return 1
test "$candidate_config_hash" = "$config_hash" || return 1
test "$(file_identity "$devlab_dir/.git/config")" = "$candidate_config_identity" || return 1
config_identity="$candidate_config_identity"
}
bind_owned_notes() {
local expected_hash="$1" candidate_notes_identity
test -f "$notes_target" && test ! -L "$notes_target" || return 1
candidate_notes_identity="$(file_identity "$notes_target")" || return 1
test "$(file_hash "$notes_target")" = "$expected_hash" || return 1
test "$(file_identity "$notes_target")" = "$candidate_notes_identity" || return 1
notes_identity="$candidate_notes_identity"
}
verify_branch_creation_candidate() {
local candidate="$1"
test "$candidate" = "$c4_head" || return 1
test "$(run_git rev-parse HEAD)" = "$candidate" || return 1
test "$(run_git rev-parse refs/heads/feature/troubleshooting-guide)" = "$candidate" || return 1
test "$(run_git log -1 --format=%s "$candidate")" = 'merge: integrate git workflow' || return 1
verify_git_tree_manifest "$candidate" 9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 || return 1
test "$(run_git rev-parse refs/heads/feature/troubleshooting-guide)" = "$candidate" || return 1
}
verify_linear_candidate() {
local candidate="$1" ref_name="$2" old_oid="$3" expected_subject="$4" expected_hash="$5" parents
test "$(run_git rev-parse HEAD)" = "$candidate" || return 1
test "$(run_git rev-parse "$ref_name")" = "$candidate" || return 1
test "$(run_git log -1 --format=%s "$candidate")" = "$expected_subject" || return 1
parents="$(run_git rev-list --parents -n 1 "$candidate")" || return 1
set -- $parents
test "$#" -eq 2 && test "$2" = "$old_oid" || return 1
verify_git_tree_manifest "$candidate" "$expected_hash" || return 1
test "$(run_git diff-tree --no-commit-id --name-only -r "$old_oid" "$candidate")" = notes/git-notes.md || return 1
test "$(run_git rev-parse "$ref_name")" = "$candidate" || return 1
}
verify_c7_candidate() {
local candidate="$1" parents
test "$(run_git rev-parse HEAD)" = "$candidate" || return 1
test "$(run_git rev-parse refs/heads/main)" = "$candidate" || return 1
test "$(run_git log -1 --format=%s "$candidate")" = 'merge: resolve troubleshooting guide' || return 1
parents="$(run_git rev-list --parents -n 1 "$candidate")" || return 1
set -- $parents
test "$#" -eq 3 && test "$2" = "$c5_head" && test "$3" = "$c6_tip" || return 1
verify_git_tree_manifest "$candidate" 42fa2adba9c1aa31981c95e6389c452d30754840095f9dbe45317994291a2677 || return 1
test "$(run_git diff-tree --no-commit-id --name-only -r "$c5_head" "$candidate")" = notes/git-notes.md || return 1
test "$(run_git rev-parse refs/heads/main)" = "$candidate" || return 1
}
verify_owned_config_and_index() {
test -f "$devlab_dir/.git/config" && test ! -L "$devlab_dir/.git/config" || return 1
test "$(file_identity "$devlab_dir/.git/config")" = "$config_identity" || return 1
test "$(file_hash "$devlab_dir/.git/config")" = "$config_hash" || return 1
verify_local_config_contract || return 1
test -f "$devlab_dir/.git/index" && test ! -L "$devlab_dir/.git/index" || return 1
test "$(file_identity "$devlab_dir/.git/index")" = "$index_identity" || return 1
test "$(file_hash "$devlab_dir/.git/index")" = "$index_hash" || return 1
}
restore_conflict_transition() {
local current_head current_feature current_main
test "$conflict_merge_handoff" -eq 0 || return 1
test "$(file_identity "$devlab_parent")" = "$devlab_parent_identity" || return 1
test "$(file_identity "$devlab_dir")" = "$devlab_identity" || return 1
test "$(file_identity "$devlab_dir/.git")" = "$git_identity" || return 1
verify_owned_config_and_index || return 1
test "$(file_identity "$devlab_dir/notes")" = "$notes_dir_identity" || return 1
test -f "$notes_target" && test ! -L "$notes_target" || return 1
test "$(file_identity "$notes_target")" = "$notes_identity" || return 1
if test -f "$devlab_dir/.git/MERGE_HEAD" && test ! -L "$devlab_dir/.git/MERGE_HEAD"; then
test "$conflict_file_verified" -eq 1 || return 1
run_git merge --abort || return 1
bind_owned_index 91407f0b23c8f27c9baee81d07d736673c2ff49d661379e63caa0c1479cd5860 || return 1
bind_owned_notes 91407f0b23c8f27c9baee81d07d736673c2ff49d661379e63caa0c1479cd5860 || return 1
conflict_file_verified=0
fi
current_head="$(run_git rev-parse HEAD)" || return 1
current_main="$(run_git rev-parse refs/heads/main)" || return 1
if test "$current_main" = "$c4_head"; then :
elif test -n "$c5_head" && test "$current_main" = "$c5_head"; then run_git update-ref refs/heads/main "$c4_head" "$c5_head" || return 1
elif test -n "$c7_head" && test "$current_main" = "$c7_head"; then run_git update-ref refs/heads/main "$c4_head" "$c7_head" || return 1
else return 1; fi
if test "$current_head" = "$c4_head"; then :
elif test -n "$feature_ref_oid" && test "$current_head" = "$feature_ref_oid"; then :
elif test -n "$c5_head" && test "$current_head" = "$c5_head"; then :
elif test -n "$c7_head" && test "$current_head" = "$c7_head"; then :
else return 1; fi
if current_feature="$(run_git rev-parse --verify refs/heads/feature/troubleshooting-guide 2>/dev/null)"; then
test -n "$feature_ref_oid" && test "$current_feature" = "$feature_ref_oid" || return 1
run_git update-ref -d refs/heads/feature/troubleshooting-guide "$feature_ref_oid" || return 1
fi
run_git symbolic-ref HEAD refs/heads/main || return 1
run_git read-tree "$c4_head^{tree}" || return 1
bind_owned_index 9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 || return 1
open_course_rewrite_fd "$notes_target" "$notes_identity" || return 1
begin_bound_course_rollback_write
truncate_bound_course_fd "$(/usr/bin/uname -s)" || { exec 7>&-; return 1; }
write_c3_notes >&7 || { exec 7>&-; return 1; }
finish_course_write_fd "$notes_target" "$notes_identity" 9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 || return 1
}
cleanup_conflict_transition_on_exit() {
local transition_status="$?"
trap - EXIT HUP INT TERM
restore_conflict_transition || transition_status=1
exit "$transition_status"
}
arm_conflict_signals() { trap 'exit 129' HUP; trap 'exit 130' INT; trap 'exit 143' TERM; }
ignore_conflict_signals() { trap '' HUP INT TERM; }
verify_bound_git_conflict_markers() {
local expected_identity="$1"
test -f "$notes_target" && test ! -L "$notes_target" || return 1
test "$(file_identity "$notes_target")" = "$expected_identity" || return 1
grep -F '<<<<<<< HEAD' "$notes_target" >/dev/null || return 1
grep -F '=======' "$notes_target" >/dev/null || return 1
grep -F '>>>>>>> feature/troubleshooting-guide' "$notes_target" >/dev/null || return 1
test "$(grep -F -c '<<<<<<< HEAD' "$notes_target")" = 1 || return 1
test "$(grep -F -c '=======' "$notes_target")" = 1 || return 1
test "$(grep -F -c '>>>>>>> feature/troubleshooting-guide' "$notes_target")" = 1 || return 1
}
promote_verified_conflict_file() {
local candidate_conflict_identity
test -f "$notes_target" && test ! -L "$notes_target" || return 1
candidate_conflict_identity="$(file_identity "$notes_target")" || return 1
test -f "$devlab_dir/.git/MERGE_HEAD" && test ! -L "$devlab_dir/.git/MERGE_HEAD" || return 1
test "$(run_git rev-parse MERGE_HEAD)" = "$c6_tip" || return 1
verify_owned_config_and_index || return 1
verify_conflict_index || return 1
test "$(file_hash "$notes_target")" = 9284dca8931de3385f8cbed3423e8323bea74490cfb016f05daa98d6f211462f || return 1
verify_bound_git_conflict_markers "$candidate_conflict_identity" || return 1
test "$(file_identity "$notes_target")" = "$candidate_conflict_identity" || return 1
notes_identity="$candidate_conflict_identity"
conflict_file_verified=1
}
begin_conflict_resolution_write() {
test -f "$notes_target" && test ! -L "$notes_target" || return 1
test "$(file_identity "$notes_target")" = "$notes_identity" || return 1
}
begin_conflict_merge_handoff() { trap '' HUP INT TERM; conflict_merge_handoff=1; }
verify_conflict_merge_before_delete() {
verify_owned_config_and_index || return 1
test "$(run_git branch --show-current)" = main || return 1
test "$(run_git rev-parse HEAD)" = "$c7_head" || return 1
test "$(run_git rev-parse HEAD^1)" = "$c5_head" || return 1
test "$(run_git rev-parse HEAD^2)" = "$c6_tip" || return 1
test "$(run_git rev-parse refs/heads/feature/troubleshooting-guide)" = "$c6_tip" || return 1
test "$(file_identity "$notes_target")" = "$notes_identity" || return 1
test "$(file_hash "$notes_target")" = 42fa2adba9c1aa31981c95e6389c452d30754840095f9dbe45317994291a2677 || return 1
verify_git_notes_blob "$c7_head" 42fa2adba9c1aa31981c95e6389c452d30754840095f9dbe45317994291a2677 || return 1
test "$(run_git diff-tree --no-commit-id --name-only -r "$c5_head" "$c7_head")" = notes/git-notes.md || return 1
}
verify_conflict_merge_handoff() {
test "$(file_identity "$devlab_parent")" = "$devlab_parent_identity" || return 1
test "$(file_identity "$devlab_dir")" = "$devlab_identity" || return 1
test "$(file_identity "$devlab_dir/.git")" = "$git_identity" || return 1
verify_owned_config_and_index || return 1
test "$(file_identity "$devlab_dir/notes")" = "$notes_dir_identity" || return 1
test "$(run_git branch --show-current)" = main || return 1
test "$(run_git rev-parse HEAD)" = "$c7_head" || return 1
test "$(run_git rev-parse HEAD^1)" = "$c5_head" || return 1
test "$(run_git rev-parse HEAD^2)" = "$c6_tip" || return 1
test "$(file_identity "$notes_target")" = "$notes_identity" || return 1
test "$(file_hash "$notes_target")" = 42fa2adba9c1aa31981c95e6389c452d30754840095f9dbe45317994291a2677 || return 1
verify_git_notes_blob "$c7_head" 42fa2adba9c1aa31981c95e6389c452d30754840095f9dbe45317994291a2677 || return 1
test "$(run_git for-each-ref --format='%(refname)' refs)" = refs/heads/main || return 1
test -z "$(run_git remote)" || return 1
test -z "$(run_git status --porcelain=v1 --untracked-files=all)" || return 1
for state_path in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD rebase-apply rebase-merge; do
test ! -e "$devlab_dir/.git/$state_path" && test ! -L "$devlab_dir/.git/$state_path" || return 1
done
}
trap cleanup_conflict_transition_on_exit EXIT
arm_conflict_signals
ignore_conflict_signals
run_git switch -c feature/troubleshooting-guide || exit 1
feature_ref_oid="$c4_head"
candidate_feature_ref="$(run_git rev-parse refs/heads/feature/troubleshooting-guide)"
verify_branch_creation_candidate "$candidate_feature_ref" || exit 1
feature_ref_oid="$candidate_feature_ref"
bind_owned_index 9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 || exit 1
arm_conflict_signals
open_course_rewrite_fd "$notes_target" "$notes_identity" 9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 || exit 1
begin_bound_course_write
truncate_bound_course_fd "$(/usr/bin/uname -s)" || { exec 7>&-; exit 1; }
write_c6_notes >&7 || { exec 7>&-; exit 1; }
finish_course_write_fd "$notes_target" "$notes_identity" f0aa82ddc8dad5b9f1072fc48f2b376385680a214659cb83cc596bfe03583022 || exit 1
ignore_conflict_signals
run_git add -- notes/git-notes.md
bind_owned_index f0aa82ddc8dad5b9f1072fc48f2b376385680a214659cb83cc596bfe03583022 || exit 1
run_git commit -m 'docs: record branch troubleshooting step'
candidate_c6_tip="$(run_git rev-parse refs/heads/feature/troubleshooting-guide)"
verify_linear_candidate "$candidate_c6_tip" refs/heads/feature/troubleshooting-guide "$c4_head" \
'docs: record branch troubleshooting step' f0aa82ddc8dad5b9f1072fc48f2b376385680a214659cb83cc596bfe03583022 || exit 1
c6_tip="$candidate_c6_tip"
feature_ref_oid="$candidate_c6_tip"
bind_owned_index f0aa82ddc8dad5b9f1072fc48f2b376385680a214659cb83cc596bfe03583022 || exit 1
arm_conflict_signals
ignore_conflict_signals
run_git switch main
bind_owned_notes 9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 || exit 1
bind_owned_index 9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 || exit 1
arm_conflict_signals
test "$(run_git rev-parse HEAD)" = "$c4_head" || exit 1
test "$(run_git rev-parse refs/heads/main)" = "$c4_head" || exit 1
test "$(run_git rev-parse refs/heads/feature/troubleshooting-guide)" = "$c6_tip" || exit 1
open_course_rewrite_fd "$notes_target" "$notes_identity" 9796b2da5468c67a7b4274d063d7a9fb10b417c1a8caabc6ae10ac095047f342 || exit 1
begin_bound_course_write
truncate_bound_course_fd "$(/usr/bin/uname -s)" || { exec 7>&-; exit 1; }
write_c5_notes >&7 || { exec 7>&-; exit 1; }
finish_course_write_fd "$notes_target" "$notes_identity" 91407f0b23c8f27c9baee81d07d736673c2ff49d661379e63caa0c1479cd5860 || exit 1
ignore_conflict_signals
run_git add -- notes/git-notes.md
bind_owned_index 91407f0b23c8f27c9baee81d07d736673c2ff49d661379e63caa0c1479cd5860 || exit 1
run_git commit -m 'docs: record main troubleshooting step'
candidate_c5_head="$(run_git rev-parse refs/heads/main)"
verify_linear_candidate "$candidate_c5_head" refs/heads/main "$c4_head" \
'docs: record main troubleshooting step' 91407f0b23c8f27c9baee81d07d736673c2ff49d661379e63caa0c1479cd5860 || exit 1
c5_head="$candidate_c5_head"
bind_owned_index 91407f0b23c8f27c9baee81d07d736673c2ff49d661379e63caa0c1479cd5860 || exit 1
arm_conflict_signals
test "$(run_git rev-parse refs/heads/feature/troubleshooting-guide)" = "$c6_tip" || exit 1
ignore_conflict_signals
if run_git merge --no-ff feature/troubleshooting-guide; then
printf '预期的教学冲突没有发生。\n' >&2
exit 1
fi
bind_owned_index conflict || exit 1
promote_verified_conflict_file || exit 1
begin_conflict_resolution_write || exit 1
arm_conflict_signals
open_course_rewrite_fd "$notes_target" "$notes_identity" 9284dca8931de3385f8cbed3423e8323bea74490cfb016f05daa98d6f211462f || exit 1
begin_bound_course_write
truncate_bound_course_fd "$(/usr/bin/uname -s)" || { exec 7>&-; exit 1; }
write_c7_notes >&7 || { exec 7>&-; exit 1; }
finish_course_write_fd "$notes_target" "$notes_identity" 42fa2adba9c1aa31981c95e6389c452d30754840095f9dbe45317994291a2677 || exit 1
ignore_conflict_signals
run_git add -- notes/git-notes.md
bind_owned_index 42fa2adba9c1aa31981c95e6389c452d30754840095f9dbe45317994291a2677 || exit 1
run_git commit -m 'merge: resolve troubleshooting guide'
candidate_c7_head="$(run_git rev-parse HEAD)"
verify_c7_candidate "$candidate_c7_head" || exit 1
c7_head="$candidate_c7_head"
bind_owned_index 42fa2adba9c1aa31981c95e6389c452d30754840095f9dbe45317994291a2677 || exit 1
arm_conflict_signals
begin_conflict_merge_handoff
verify_conflict_merge_before_delete || exit 1
run_git branch -d feature/troubleshooting-guide
rebind_owned_config_after_git || exit 1
verify_conflict_merge_handoff || exit 1
trap - EXIT HUP INT TERM
)
advance_git_conflict_resolution || { unset -f advance_git_conflict_resolution; exit 1; }
unset -f advance_git_conflict_resolution
观察结果
合并失败后,先用 git status --short 看到未合并路径,再打开 notes/git-notes.md 分辨 HEAD 区域、分隔线和功能分支区域。规范结果不保留标记,而是同时保留两个有效步骤:
- 先查看状态与差异。
- 再确认当前分支和目标。
如果暂时不想解决,可以在未完成的课程合并中执行:
git status --short
git merge --abort
git status --short
git merge --abort 会回到合并前的 C5,但不会删除 feature/troubleshooting-guide。只有成功解决、提交 C7,并验证第二父仍是该分支尖端后,才删除这个课程分支。
常见问题
- 合并竟然成功: 停止。两边没有从同一 C4 修改指定占位段落,当前状态不是本节的确定实验。
- 文件中还有标记: 不要暂存。确认三类标记全部删除,再检查两条规范文本都在。
- abort 后分支仍存在: 这正是预期。abort 放弃的是未完成合并,不是已有的分支历史。
- 路径或分支在操作中被替换: 迁移保留冲突或合并现场并失败,不会强制删除外来资源。
完成检查
确认 main 最新主题为 merge: resolve troubleshooting guide,第一父是主线 C5,第二父是功能分支 C6。notes/git-notes.md 中有两条规范排错步骤且没有冲突标记;工作区干净,只剩 refs/heads/main,没有 remote 或未完成操作。
下一步
下一节将在这个 C7 上分辨未暂存、已暂存、已提交和已共享四种边界,用受控的 restore 和 amend 完成本地 Git 历史。