sync
让本地仓库与远程保持一致——fetch、prune、快进基线分支,并报告哪些地方产生了分叉。它从不 rebase 功能分支,从不解决冲突,也从不丢弃本地提交。不适用于发布改动、删除已合并分支或解决合并冲突。
- 分组
- 开发
- 版本
- 0.8.4
- 许可证
- MIT
- 参数
- [--base=<branch>] [--all]
安装
/plugin install dev@misoto22然后这样调用:
/dev:sync这个技能只做一件事:把本地和远程的差异摊开给你看,并且只执行绝对安全的那部分同步。它会 fetch 并 prune 掉远程已删除的引用,把基线分支快进到最新——只有在能快进的情况下才动,不能快进就如实报告分叉状态然后停下。它刻意不碰功能分支:不 rebase、不合并、不 stash。理由是这些操作都可能丢失工作,而判断该不该丢是人的决定,不是同步工具的。
什么时候会触发
CI 实际用来评测这个技能的 prompt——所以它们不会和技能的真实行为脱节。
会触发
- Pull the latest changes.
- 更新一下代码,跟 main 对齐
- Get me up to date with the remote.
- 同步一下,拉最新的
不会触发
- Ship this branch.
- Resolve this merge conflict for me.
- Delete the branches that already merged.
- 把这些改动提上去合了
Sync
Bring the local repository in line with its remote, and say plainly what did not line up.
Read shared/git.md first. Base resolution, force-push rules, and what "merged" means after a rebase all live there.
This skill only fast-forwards. Where a branch has diverged it reports and stops — choosing between rebase, merge, and reset is the user's call, and guessing it destroys work.
1. Fetch
git fetch --all --prune --tags
--prune deletes remote-tracking refs whose upstream is gone. It touches no local branch and no file.
It does not prune tags, and --prune-tags is deliberately not passed: that flag deletes every local tag the remote does not carry, including one made by hand five minutes ago, and a tag is sometimes the only thing keeping a commit reachable. A stale tag costs nothing. The alternative discards work, which is the one thing this skill never does.
git fetch writes what it did to stderr, not stdout — capture it. The fetched line in the final report counts those lines, and a run that discards the output has nothing left to count. Lines opening - [deleted] are the pruned refs; * [new tag] are the tags.
2. Report before writing
One command per line of the report. None of them writes:
git status --porcelain # working tree
git branch --show-current # current branch
git rev-list --left-right --count @{u}...HEAD # behind, then ahead — in that order
git rev-list --count <base>..origin/<base> # how far the base is behind
git branch -vv # upstream gone — the lines ending ': gone]'
git worktree list # worktrees, and which one holds the base
--left-right prints the left side first, and the left side is @{u} — the remote. So the first number is behind and the second is ahead, the reverse of the order the report reads them out in. Getting it backwards turns unpushed work into a report saying there is none.
@{u} exits non-zero when the branch has no upstream. That is an answer, not a failure — report no upstream and carry on.
Print this before changing anything:
Sync <repo> → <base>
working tree <clean | N files modified, M untracked>
current branch <branch> <ahead N, behind M | up to date | no upstream>
base <base> <behind N | up to date>
gone upstream <branches whose remote was pruned, or none>
worktrees <N, or none>
Every line comes from a command above it. A field no command could fill is reported as unknown — never estimated, never quietly dropped.
3. Fast-forward the base branch
Only when it is behind and carries no local commits of its own.
- Already on the base branch —
git merge --ff-only origin/<base>. This is the one path that writes to the working tree, so a dirty tree can refuse it: when the incoming commits touch a file you have modified, git stops withYour local changes to the following files would be overwritten by merge. It checks before it writes, so the refusal leaves the tree exactly as step 2 found it. Report the base as skipped, name the files that blocked it, and stop. Stashing on someone's behalf is how uncommitted work goes missing. - On a feature branch — update the base without leaving it:
That advances the local ref directly, fails loudly if it would not be a fast-forward, and never touches the working tree, so uncommitted work is not at risk.git fetch origin <base>:<base> - The base is checked out in another worktree — the command above is refused. Name the worktree holding it and skip. That is not an error.
If the fast-forward fails, the local base has commits the remote does not. Report the count and stop. Do not reset, rebase, or merge.
4. Report the feature branch, do not touch it
For the current branch when it is not the base:
| State | Report |
|---|---|
| No upstream | no upstream — push with -u to create one |
| Ahead only | ahead N — unpushed work |
| Behind only | behind N — rebase onto <base> when ready |
| Diverged | diverged: N ahead, M behind — rebase or merge, your call |
| Upstream gone | upstream gone — the pull request was probably merged and the branch deleted; /dev:cleanup removes it |
Never rebase automatically. A rebase rewrites commits, and one run without being asked is indistinguishable from losing work.
5. With --all
Enumerate first. One command classifies every local branch:
git for-each-ref --format='%(refname:short) %(upstream:short) %(upstream:track)' refs/heads/
%(upstream:track) is the whole decision:
| Track | Meaning | Action |
|---|---|---|
[behind N] | strictly behind | fast-forward |
[ahead N], [ahead N, behind M] | carries local commits | skip — step 4's rules apply |
[gone] | upstream pruned | skip, report; /dev:cleanup decides its fate |
| empty, with an upstream | up to date | nothing to do |
| empty, with no upstream | never pushed | skip, report |
Fast-forward each candidate without checking it out:
git fetch origin <branch>:<branch>
Skip the checked-out branch and any branch held by another worktree — that ref is pinned by a working tree and the command is refused, which is the same rule step 3 already applies to the base. Report each branch as fast-forwarded or skipped with its reason.
Reporting
Synced <repo>.
fetched <N refs, M pruned, T new tags>
base <base> <fast-forwarded A..B | already up to date | skipped: reason>
branch <name> <state from step 4>
attention <lines needing a decision, or none>
All three counts on the fetched line come from the stderr captured in step 1. If it was not captured, say so rather than printing a number nobody measured.
Everything under attention is something this skill deliberately refused to decide. State the options; do not pick one.