Git 工作区模型与首次提交
本节目标
把已经通过 Linux checkpoint 检查的同一个 dev-lab 初始化为本地 Git 仓库,建立 main 分支和第一笔提交。完成后,你应能区分工作区、暂存区与 HEAD,并知道提交记录的是一份明确选择的文件快照。
开始前状态
从完整的 linux checkpoint 开始。目标必须由你通过 DEVLAB_DIR 明确指定;它不能是符号链接、用户主目录、其他仓库的子目录,也不能已经包含 .git。本节不会猜测当前目录、HOME 或父项目,更不会修改其他仓库。
先确认 Git 已安装:Linux 或 WSL 通常由发行版包管理器提供 Git,macOS 可以使用系统开发者工具或你已选择的包管理器。这里只检查版本,不自动安装软件。
git --version
必要原理
工作区是你正在读写的文件;暂存区是“下一次提交候选快照”的清单;提交则把这份候选快照写入本地历史。暂存区不会自动保存文件,也不等同于备份。HEAD 指向当前分支所指的提交,本节完成后它会指向 main 上的首次提交。
.git/ 保存仓库元数据,.gitignore 只声明哪些未跟踪路径不进入日常状态提示。忽略规则不会删除文件,也不会自动把已跟踪文件移出历史。
动手完成
下面的受保护迁移先验证 linux checkpoint 的 12 个文件、5 个目录、字节与执行位类别,并拒绝额外路径、已有 .git 或外层仓库。所有 Git 调用都在清洁环境中执行,只写仓库级教学身份 Dev Lab <dev-lab@example.invalid>;全局身份不会被读取或修改。
运行位置: 任意安全目录,但必须显式设置 DEVLAB_DIR。预期结果: 创建 .gitignore 与本地 .git/,在 main 上得到主题为 chore: establish dev-lab baseline 的根提交。失败处理: 根据错误检查目标路径;不要删除或覆盖身份不明的 .git。
advance_git_baseline() (
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" || {
printf 'DEVLAB_DIR 必须是真实且非链接的目录。\n' >&2
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" || { printf 'DEVLAB_DIR 不得经过符号链接。\n' >&2; exit 1; }
test "$devlab_dir" != / || exit 1
test ! -e "$devlab_dir/.git" && test ! -L "$devlab_dir/.git" || {
printf '目标已经包含 .git;不会覆盖现有仓库。\n' >&2
exit 1
}
ancestor_dir="${devlab_dir%/*}"
test -n "$ancestor_dir" || ancestor_dir=/
while :; do
test ! -e "$ancestor_dir/.git" && test ! -L "$ancestor_dir/.git" || {
printf '目标位于另一个 Git 仓库内;请改用独立的 dev-lab。\n' >&2
exit 1
}
test "$ancestor_dir" = / && break
ancestor_dir="${ancestor_dir%/*}"
test -n "$ancestor_dir" || ancestor_dir=/
done
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
printf '需要 sha256sum 或 shasum 来验证 checkpoint。\n' >&2
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"
}
same_file_as_fd() {
local path_inode fd_inode
path_inode="$(file_inode "$1")" || return 1
fd_inode="$(file_inode "$2")" || return 1
test "$path_inode" = "$fd_inode"
}
has_any_execute_bit() {
local mode_text
mode_text="$(LC_ALL=C ls -ld "$1")" || return 1
mode_text="${mode_text%% *}"
[[ "$mode_text" == *[xstST]* ]]
}
require_directory() {
test -d "$devlab_dir/$1" && test ! -L "$devlab_dir/$1" || exit 1
}
require_linux_file() {
local expected_hash="$1"
local relative_path="$2"
local expected_class="$3"
test -f "$devlab_dir/$relative_path" && test ! -L "$devlab_dir/$relative_path" || exit 1
case "$expected_class" in
x) test -x "$devlab_dir/$relative_path" || exit 1 ;;
-) ! has_any_execute_bit "$devlab_dir/$relative_path" || exit 1 ;;
*) exit 1 ;;
esac
test "$(file_hash "$devlab_dir/$relative_path")" = "$expected_hash" || exit 1
}
for relative_path in config data notes output scripts; do require_directory "$relative_path"; done
expected_paths='README.md
config
config/bashrc.dev-lab
config/env.dev-lab
data
data/access.log
data/health-events.log
notes
notes/linux-notes.md
notes/shell-notes.md
output
output/health-report.txt
output/summary.txt
scripts
scripts/health-check.sh
scripts/permission-demo.sh
scripts/report.sh'
actual_paths="$(cd "$devlab_dir" && find . -mindepth 1 -print | sed 's#^\./##' | LC_ALL=C sort)"
test "$actual_paths" = "$expected_paths" || { printf 'linux checkpoint 路径集合不匹配。\n' >&2; exit 1; }
while read -r expected_hash relative_path expected_class; do
require_linux_file "$expected_hash" "$relative_path" "$expected_class"
done <<'EXPECTED_LINUX_FILES'
f35c009dec20278b5df50693da43c54ae4b8e228a173fcb9f59e1b3aea5a2dc5 README.md -
882ef7d9502cc5d8145c5f8b0321dbd17b0c7d431db9421cbe390f1f86e94e12 config/bashrc.dev-lab -
00aec1c6d894bb334bef59a03fad5d6392f0a2c1e5517cb89b788c4ad72b390a config/env.dev-lab -
1df6e34d28da0483cc1e62523acc777597ce3178a77d089bf7ffcabb4b0ab254 data/access.log -
fe0119bfe33a541d816c33740f38b32280896ffce54bd7841985c7b1e6bd06e5 data/health-events.log -
e54276c7c8a2c2818aa48891030e31f7712fde6f5f42b70257c1a3e9582086dc notes/linux-notes.md -
d937ab49e62af7d2f5bbb92f457476afa2a9632a9d1c68135a30a98ffe9aed7d notes/shell-notes.md -
32c0404e72a9692ecac0a2877a98b8ae09fa90090c2af9dbdbc3c9da18a1cf1e output/health-report.txt -
fd45eca2af0f073a58b8a6291f64804b6e5eade93aa773c6d5e49832edd65591 output/summary.txt -
5fa78e594202c38e9004b65fe69c0281c509b00ad7c10b0c48cfd2b383720e4f scripts/health-check.sh x
3f54de46ae71a2b3af65bd3aeaf9c28bbf91409abc67c87f718c16c4961445e8 scripts/permission-demo.sh x
eb8bf1b36e7bd043f64665680b71d086a0a9aeb3c5b5a045fe2fb3e40bb52e37 scripts/report.sh x
EXPECTED_LINUX_FILES
devlab_parent="${devlab_dir%/*}"
test -n "$devlab_parent" || devlab_parent=/
devlab_identity="$(file_identity "$devlab_dir")" || exit 1
devlab_parent_identity="$(file_identity "$devlab_parent")" || exit 1
gitignore_identity=''
git_dir_identity=''
git_owner_marker_identity=''
git_config_identity=''
course_tree=''
course_head=''
owned_git_objects=''
remove_owned_gitignore() {
test -n "$gitignore_identity" || return 0
(
cd -P -- "$devlab_dir" || exit 1
test "$(file_identity .)" = "$devlab_identity" || exit 1
test "$(file_identity ..)" = "$devlab_parent_identity" || exit 1
if test ! -e ./.gitignore && test ! -L ./.gitignore; then exit 0; fi
test -f ./.gitignore && test ! -L ./.gitignore || exit 1
test "$(file_identity ./.gitignore)" = "$gitignore_identity" || exit 1
/bin/rm -f -- ./.gitignore
)
}
remove_owned_git_directory() {
test -n "$git_dir_identity" || return 0
(
cd -P -- "$devlab_dir" || exit 1
test "$(file_identity .)" = "$devlab_identity" || exit 1
test "$(file_identity ..)" = "$devlab_parent_identity" || exit 1
if test ! -e ./.git && test ! -L ./.git; then exit 0; fi
test -d ./.git && test ! -L ./.git || exit 1
test "$(file_identity ./.git)" = "$git_dir_identity" || exit 1
test -f ./.git/dev-lab-course-owner && test ! -L ./.git/dev-lab-course-owner || exit 1
test "$(file_identity ./.git/dev-lab-course-owner)" = "$git_owner_marker_identity" || exit 1
test "$(file_hash ./.git/dev-lab-course-owner)" = cb6d721d0a9324c23fde3e6b9f64a4d9a3462d7e00ceb6e0dc198cc9be819dd1 || exit 1
! has_any_execute_bit ./.git/dev-lab-course-owner || exit 1
git_baseline_repository_is_owned || exit 1
/bin/rm -rf -- ./.git
)
}
remove_git_owner_marker() {
test -n "$git_owner_marker_identity" || return 1
(
cd -P -- "$devlab_dir" || exit 1
test "$(file_identity .)" = "$devlab_identity" || exit 1
test "$(file_identity ..)" = "$devlab_parent_identity" || exit 1
test -d ./.git && test ! -L ./.git || exit 1
test "$(file_identity ./.git)" = "$git_dir_identity" || exit 1
test -f ./.git/dev-lab-course-owner && test ! -L ./.git/dev-lab-course-owner || exit 1
test "$(file_identity ./.git/dev-lab-course-owner)" = "$git_owner_marker_identity" || exit 1
/bin/rm -f -- ./.git/dev-lab-course-owner
)
}
cleanup_git_baseline() {
local cleanup_status=0
remove_owned_git_directory || cleanup_status=1
remove_owned_gitignore || cleanup_status=1
return "$cleanup_status"
}
cleanup_git_baseline_on_exit() {
local transition_status="$?"
trap - EXIT HUP INT TERM
cleanup_git_baseline || transition_status=1
exit "$transition_status"
}
arm_git_baseline_cleanup_signals() {
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
}
ignore_git_baseline_acquire_signals() {
trap '' HUP INT TERM
}
begin_git_baseline_handoff() {
trap '' HUP INT TERM
}
trap cleanup_git_baseline_on_exit EXIT
arm_git_baseline_cleanup_signals
gitignore_path="$devlab_dir/.gitignore"
ignore_git_baseline_acquire_signals
set -o noclobber
if exec 8> "$gitignore_path"; then
:
else
set +o noclobber
printf '.gitignore 已存在;不会覆盖未知文件。\n' >&2
exit 1
fi
set +o noclobber
if candidate_gitignore_identity="$(file_identity "$gitignore_path")"; then
:
else
if same_file_as_fd "$gitignore_path" /dev/fd/8; then /bin/rm -f -- "$gitignore_path"; fi
exec 8>&-
exit 1
fi
same_file_as_fd "$gitignore_path" /dev/fd/8 || { exec 8>&-; exit 1; }
gitignore_identity="$candidate_gitignore_identity"
printf '.DS_Store\n.tmp-*\n' >&8 || { exec 8>&-; exit 1; }
exec 8>&-
arm_git_baseline_cleanup_signals
test -f "$gitignore_path" && test ! -L "$gitignore_path" || exit 1
test "$(file_hash "$gitignore_path")" = 04950b968fe37d63b990bf4d5cb3a5b5684fd59320205c10282df38a8120fdbe || exit 1
! has_any_execute_bit "$gitignore_path" || exit 1
git_bin="$(PATH=/usr/bin:/bin command -v git)" || {
printf '未检测到 Git;请先按平台说明安装。\n' >&2
exit 1
}
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" "$@"
}
all_git_object_ids() {
run_git cat-file --batch-all-objects --batch-check='%(objectname)' | LC_ALL=C sort
}
reachable_git_object_ids() {
run_git rev-list --objects "$1" | sed 's/ .*//' | LC_ALL=C sort
}
git_baseline_repository_is_owned() {
local actual_objects actual_refs config_name config_names optional_value preinit_paths state_path
if test ! -e "$devlab_dir/.git/HEAD" && test ! -L "$devlab_dir/.git/HEAD"; then
preinit_paths="$(cd "$devlab_dir/.git" && find . -mindepth 1 -print | LC_ALL=C sort)" || return 1
test "$preinit_paths" = './dev-lab-course-owner'
return
fi
actual_objects="$(all_git_object_ids)" || return 1
test "$actual_objects" = "$owned_git_objects" || return 1
test -n "$git_config_identity" || return 1
test -f "$devlab_dir/.git/config" && test ! -L "$devlab_dir/.git/config" || return 1
test "$(file_identity "$devlab_dir/.git/config")" = "$git_config_identity" || return 1
config_names="$(run_git config --local --no-includes --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) ;;
*) return 1 ;;
esac
done <<< "$config_names"
test "$(run_git config --local --no-includes --get-all core.repositoryformatversion)" = 0 || return 1
case "$(run_git config --local --no-includes --get-all core.filemode)" in true|false) ;; *) return 1 ;; esac
test "$(run_git config --local --no-includes --get-all core.bare)" = false || return 1
test "$(run_git config --local --no-includes --get-all core.logallrefupdates)" = true || return 1
for optional_value in core.ignorecase core.precomposeunicode; do
case "$(run_git config --local --no-includes --get-all "$optional_value" 2>/dev/null || true)" in ''|true|false) ;; *) return 1 ;; esac
done
case "$(run_git config --local --no-includes --get-all user.name 2>/dev/null || true)" in ''|'Dev Lab') ;; *) return 1 ;; esac
case "$(run_git config --local --no-includes --get-all user.email 2>/dev/null || true)" in ''|'dev-lab@example.invalid') ;; *) return 1 ;; esac
case "$(run_git config --local --no-includes --get-all core.hooksPath 2>/dev/null || true)" in ''|/dev/null) ;; *) return 1 ;; esac
case "$(run_git config --local --no-includes --get-all core.fsmonitor 2>/dev/null || true)" in ''|false) ;; *) return 1 ;; esac
if test -z "$course_tree"; then
test ! -e "$devlab_dir/.git/index" && test ! -L "$devlab_dir/.git/index" || return 1
else
test -f "$devlab_dir/.git/index" && test ! -L "$devlab_dir/.git/index" || return 1
test "$(run_git write-tree)" = "$course_tree" || return 1
fi
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
actual_refs="$(run_git for-each-ref --format='%(refname)' refs)" || return 1
test -z "$(run_git remote)" || return 1
if test -z "$course_head"; then
test -z "$actual_refs" || return 1
else
test "$actual_refs" = refs/heads/main || return 1
test "$(run_git rev-parse HEAD)" = "$course_head" || return 1
test -z "$(run_git status --porcelain=v1 --untracked-files=all)" || return 1
fi
}
verify_git_baseline_handoff() {
test "$(file_identity "$devlab_dir")" = "$devlab_identity" || return 1
test "$(file_identity "$devlab_parent")" = "$devlab_parent_identity" || return 1
test -f "$gitignore_path" && test ! -L "$gitignore_path" || return 1
test "$(file_identity "$gitignore_path")" = "$gitignore_identity" || return 1
test "$(file_hash "$gitignore_path")" = 04950b968fe37d63b990bf4d5cb3a5b5684fd59320205c10282df38a8120fdbe || return 1
! has_any_execute_bit "$gitignore_path" || return 1
test -d "$devlab_dir/.git" && test ! -L "$devlab_dir/.git" || return 1
test "$(file_identity "$devlab_dir/.git")" = "$git_dir_identity" || return 1
test "$(file_identity "$devlab_dir/.git/config")" = "$git_config_identity" || return 1
test -f "$devlab_dir/.git/dev-lab-course-owner" && test ! -L "$devlab_dir/.git/dev-lab-course-owner" || return 1
test "$(file_identity "$devlab_dir/.git/dev-lab-course-owner")" = "$git_owner_marker_identity" || return 1
test "$(file_hash "$devlab_dir/.git/dev-lab-course-owner")" = cb6d721d0a9324c23fde3e6b9f64a4d9a3462d7e00ceb6e0dc198cc9be819dd1 || return 1
! has_any_execute_bit "$devlab_dir/.git/dev-lab-course-owner" || return 1
git_baseline_repository_is_owned || return 1
test -n "$course_head" || return 1
test "$(run_git log -1 --format=%s)" = 'chore: establish dev-lab baseline' || return 1
test "$(run_git rev-list --count HEAD)" = 1 || return 1
test -z "$(run_git status --porcelain=v1 --untracked-files=all)" || return 1
test "$(run_git config --local --no-includes --get user.name)" = 'Dev Lab' || return 1
test "$(run_git config --local --no-includes --get user.email)" = 'dev-lab@example.invalid' || return 1
}
ignore_git_baseline_acquire_signals
/bin/mkdir -- "$devlab_dir/.git" || exit 1
if exec 7< "$devlab_dir/.git"; then
:
else
/bin/rmdir -- "$devlab_dir/.git" 2>/dev/null || true
exit 1
fi
if candidate_git_dir_identity="$(file_identity "$devlab_dir/.git")"; then
:
else
if same_file_as_fd "$devlab_dir/.git" /dev/fd/7; then /bin/rmdir -- "$devlab_dir/.git" 2>/dev/null || true; fi
exec 7<&-
exit 1
fi
same_file_as_fd "$devlab_dir/.git" /dev/fd/7 || { exec 7<&-; exit 1; }
git_dir_identity="$candidate_git_dir_identity"
set -o noclobber
if exec 6> "$devlab_dir/.git/dev-lab-course-owner"; then
:
else
set +o noclobber
if same_file_as_fd "$devlab_dir/.git" /dev/fd/7; then /bin/rmdir -- "$devlab_dir/.git" 2>/dev/null || true; fi
exec 7<&-
exit 1
fi
set +o noclobber
printf 'dev-lab Git course owner\n' >&6 || { exec 6>&-; exec 7<&-; exit 1; }
if candidate_git_owner_marker_identity="$(file_identity "$devlab_dir/.git/dev-lab-course-owner")"; then
:
else
if same_file_as_fd "$devlab_dir/.git/dev-lab-course-owner" /dev/fd/6; then
/bin/rm -f -- "$devlab_dir/.git/dev-lab-course-owner"
fi
exec 6>&-
if same_file_as_fd "$devlab_dir/.git" /dev/fd/7; then /bin/rmdir -- "$devlab_dir/.git" 2>/dev/null || true; fi
exec 7<&-
exit 1
fi
same_file_as_fd "$devlab_dir/.git/dev-lab-course-owner" /dev/fd/6 || { exec 6>&-; exec 7<&-; exit 1; }
if same_file_as_fd "$devlab_dir/.git" /dev/fd/7; then
:
else
if same_file_as_fd "$devlab_dir/.git/dev-lab-course-owner" /dev/fd/6; then
/bin/rm -f -- "$devlab_dir/.git/dev-lab-course-owner"
fi
exec 6>&-
exec 7<&-
exit 1
fi
git_owner_marker_identity="$candidate_git_owner_marker_identity"
exec 6>&-
exec 7<&-
arm_git_baseline_cleanup_signals
ignore_git_baseline_acquire_signals
if run_git init --template= -b main >/dev/null 2>&1; then
:
else
if run_git init --template= >/dev/null; then
:
else
exit 1
fi
run_git branch -M main
fi
git_config_identity="$(file_identity "$devlab_dir/.git/config")" || exit 1
arm_git_baseline_cleanup_signals
ignore_git_baseline_acquire_signals
run_git config --local user.name 'Dev Lab'
git_config_identity="$(file_identity "$devlab_dir/.git/config")" || exit 1
run_git config --local user.email 'dev-lab@example.invalid'
git_config_identity="$(file_identity "$devlab_dir/.git/config")" || exit 1
run_git config --local core.hooksPath /dev/null
git_config_identity="$(file_identity "$devlab_dir/.git/config")" || exit 1
run_git config --local core.fsmonitor false
git_config_identity="$(file_identity "$devlab_dir/.git/config")" || exit 1
arm_git_baseline_cleanup_signals
run_git status --short
ignore_git_baseline_acquire_signals
run_git add -- .gitignore README.md config data notes output scripts
course_tree="$(run_git write-tree)" || exit 1
owned_git_objects="$(reachable_git_object_ids "$course_tree")" || exit 1
arm_git_baseline_cleanup_signals
run_git status --short
# 先绑定精确提交对象,再用空 ref 的 compare-and-swap 发布 main;历史语义与
# `git commit -m 'chore: establish dev-lab baseline'` 相同,但失败清理无需猜测所有权。
ignore_git_baseline_acquire_signals
course_head="$(printf '%s\n' 'chore: establish dev-lab baseline' | run_git commit-tree "$course_tree")" || exit 1
owned_git_objects="$(reachable_git_object_ids "$course_head")" || exit 1
empty_blob_oid="$(printf '' | run_git hash-object --stdin)" || exit 1
null_oid="$(printf '%*s' "${#empty_blob_oid}" '' | tr ' ' 0)" || exit 1
run_git update-ref refs/heads/main "$course_head" "$null_oid" || exit 1
arm_git_baseline_cleanup_signals
test "$(run_git branch --show-current)" = main
test "$(run_git log -1 --format=%s)" = 'chore: establish dev-lab baseline'
set -- $(run_git rev-list --parents -n 1 HEAD)
test "$#" -eq 1
test "$(run_git rev-list --count HEAD)" = 1
test -z "$(run_git status --porcelain=v1 --untracked-files=all)"
test "$(run_git config --local --no-includes --get user.name)" = 'Dev Lab'
test "$(run_git config --local --no-includes --get user.email)" = 'dev-lab@example.invalid'
test "$(run_git for-each-ref --format='%(refname)' refs)" = refs/heads/main
test -z "$(run_git remote)"
# 在同一窄 handoff 中最后复核绑定资源,再执行不会失败的 unlink 与 trap 操作。
# 这样 marker 删除前仍可安全失败,删除后也不会留下可观察的半初始化状态。
begin_git_baseline_handoff
verify_git_baseline_handoff || exit 1
remove_git_owner_marker || exit 1
trap - EXIT
)
advance_git_baseline || {
unset -f advance_git_baseline
exit 1
}
unset -f advance_git_baseline
观察结果
迁移中的第一条 git status --short 会把 checkpoint 文件显示为未跟踪;git add 之后,同一命令会显示它们已进入下一次提交的候选快照;提交完成后状态为空。三次观察分别对应工作区、暂存区和 HEAD 的变化,不表示文件被自动备份。
可以用只读命令确认当前分支、最近一次提交与仓库级身份:
git status --short
git branch --show-current
git log -1 --oneline --decorate
git config --local --get user.name
git config --local --get user.email
git remote
最后一条命令没有输出,表示当前本地课程仓库没有 remote;它不会添加或修改远程配置。
常见问题
- 提示 checkpoint 路径或字节不匹配: 重新运行 Linux checker,确认没有遗漏文件、额外文件或执行位变化。
- 提示已有
.git: 停止并确认这个目录属于谁。课程不会覆盖或删除现有仓库。 - 提交主题与预期不同: 不要继续下一节;先确认你运行的是完整受保护迁移,而不是在别的仓库手工提交。
- Git 未安装: 按当前平台的正式安装方式安装后重试,不要从未知脚本下载。
完成检查
确认当前分支是 main,git status --short 无输出,最近提交主题是 chore: establish dev-lab baseline,仓库级身份精确为课程身份,并且 git remote 无输出。全局 Git 配置不属于本节检查范围,也没有被本节改写。
下一步
下一节继续使用这个根仓库创建 notes/git-notes.md,通过同一文件观察未跟踪、未暂存、已暂存和已提交状态,再用简短历史回答“仓库刚刚发生了什么”。