diff --git a/doc-onevcat/fork-sync-and-release.md b/doc-onevcat/fork-sync-and-release.md index d7c7fe40..dc7d9f10 100644 --- a/doc-onevcat/fork-sync-and-release.md +++ b/doc-onevcat/fork-sync-and-release.md @@ -36,7 +36,8 @@ Out of the box, these workflows are not fork-friendly: ## One-Time Setup ```bash -git fetch origin upstream --prune +git fetch origin --prune +git fetch upstream --prune git config rerere.enabled true git config rerere.autoupdate true ``` @@ -47,8 +48,9 @@ git config rerere.autoupdate true ```bash git switch main -git fetch origin upstream --prune -git pull --ff-only origin main +git fetch origin --prune +git fetch upstream --prune +git merge --ff-only origin/main git merge --no-ff upstream/main make build-app make test @@ -57,6 +59,15 @@ git push origin main If conflicts happen, resolve once, commit, and `rerere` will likely auto-apply next time. +## Common Pitfalls and Fixes + +- `git fetch origin upstream --prune` is invalid for this use case. + `upstream` is interpreted as a refspec, which may fail with `fatal: couldn't find remote ref upstream`. + Use two fetch commands (or `git fetch --all --prune`) instead. +- Prefer `git merge --ff-only origin/main` in scripted sync flow. + It is deterministic and avoids `git pull` edge cases around `FETCH_HEAD`. +- Keep working tree clean before sync (`git status --short` should be empty), otherwise abort and stash/commit first. + ## Personal Release Strategy (Fork Release Page) The release helper now supports automatic notarization for personal fork releases. diff --git a/doc-onevcat/scripts/sync-upstream-main.sh b/doc-onevcat/scripts/sync-upstream-main.sh index 8cfb6e35..ada15be4 100755 --- a/doc-onevcat/scripts/sync-upstream-main.sh +++ b/doc-onevcat/scripts/sync-upstream-main.sh @@ -3,14 +3,45 @@ set -euo pipefail TARGET_BRANCH="${1:-main}" +die() { + echo "[error] $*" >&2 + exit 1 +} + +ensure_remote_exists() { + local remote_name="$1" + git remote get-url "${remote_name}" >/dev/null 2>&1 || die "missing git remote: ${remote_name}" +} + +ensure_clean_worktree() { + if ! git diff --quiet || ! git diff --cached --quiet; then + die "working tree is not clean. commit or stash your changes before syncing." + fi +} + +ensure_remote_branch_exists() { + local remote_name="$1" + local branch_name="$2" + git show-ref --verify --quiet "refs/remotes/${remote_name}/${branch_name}" || \ + die "remote branch not found: ${remote_name}/${branch_name}" +} + +echo "[sync] preflight checks" +ensure_clean_worktree +ensure_remote_exists origin +ensure_remote_exists upstream + echo "[sync] fetch remotes" -git fetch origin upstream --prune +git fetch origin --prune +git fetch upstream --prune +ensure_remote_branch_exists origin "${TARGET_BRANCH}" +ensure_remote_branch_exists upstream "main" echo "[sync] switch to ${TARGET_BRANCH}" git switch "${TARGET_BRANCH}" -echo "[sync] fast-forward from origin/${TARGET_BRANCH}" -git pull --ff-only origin "${TARGET_BRANCH}" +echo "[sync] fast-forward from origin/${TARGET_BRANCH} (deterministic merge-based flow)" +git merge --ff-only "origin/${TARGET_BRANCH}" echo "[sync] merge upstream/main into ${TARGET_BRANCH}" git merge --no-ff upstream/main