使用Git与GitHub进行远程协作
参与 GitHub 上的开源项目时,我们通常没有原仓库的直接写权限。标准的协作方式不是把代码直接推送到原作者的仓库,而是先将项目 Fork 到自己的 GitHub 账号,再把自己的 Fork 克隆到本地。完成开发后,将功能分支推送到自己的 Fork,最后向原仓库发起 Pull Request。
本文不再孤立地罗列 Git 命令,而是通过一个完整案例,把从 Fork 到 Pull Request 合并后的整个流程串联起来。
1. 本文的协作场景
假设 GitHub 上有一个任务管理项目:
1 | 原作者仓库:https://github.com/octo-org/task-board |
我们要完成的任务是:修复空标题校验,补充测试,并向原项目提交 Pull Request。
最终会涉及三份仓库:
| 位置 | 仓库 | 作用 |
|---|---|---|
| GitHub 原仓库 | octo-org/task-board |
项目的正式仓库,通常称为上游仓库 |
| GitHub 个人 Fork | xiaoming/task-board |
自己拥有写权限的远程仓库 |
| 本地仓库 | 电脑中的 task-board/ |
编写、测试和提交代码的地方 |
完整的数据流如下:
1 | octo-org/task-board(原仓库) |
后文中的仓库名和账号都是示例。实际操作时,需要替换成真实的项目地址和自己的 GitHub 用户名。
2. 开始前的准备
2.1 安装并配置 Git
确认 Git 已经安装:
1 | git --version |
首次使用时,配置提交者姓名和邮箱:
1 | git config --global user.name "Xiao Ming" |
这些信息会记录在每次提交中。如果希望 GitHub 将提交关联到自己的账号,应使用 GitHub 账号中已经验证的邮箱,或者 GitHub 提供的 noreply 邮箱。
2.2 配置 GitHub 认证
本文使用 SSH 地址操作远程仓库。先测试当前计算机能否连接 GitHub:
1 | ssh -T git@github.com |
如果尚未配置 SSH,可以生成密钥:
1 | ssh-keygen -t ed25519 -C "xiaoming@example.com" |
然后将 ~/.ssh/id_ed25519.pub 中的公钥添加到 GitHub 的 Settings → SSH and GPG keys。私钥 ~/.ssh/id_ed25519 只能由自己保管,不能上传到 GitHub 或发送给他人。
也可以使用 HTTPS 地址,但 GitHub 的命令行 Git 操作不能使用账号密码认证,需要使用 GitHub CLI、凭据管理器或 Personal Access Token。
2.3 阅读项目的贡献规则
在 Fork 之前,先查看原仓库中的以下文件:
1 | README.md |
不同项目对分支名、提交格式、测试命令和 Pull Request 内容可能有不同要求。项目自身的贡献规范应优先于本文中的通用示例。
同时阅读准备处理的 Issue,确认它没有被其他人认领,修改方案符合维护者预期。对于较大的功能,最好先在 Issue 中讨论,避免完成开发后才发现方向不一致。
3. 第一步:Fork 原仓库
打开原项目页面:
1 | https://github.com/octo-org/task-board |
点击页面右上角的 Fork,选择自己的 GitHub 账号作为目标所有者,然后创建 Fork。
完成后,自己的账号下会出现:
1 | https://github.com/xiaoming/task-board |
此时两个仓库的关系是:
1 | octo-org/task-board 原仓库 |
Fork 发生在 GitHub 服务器上,它复制的是仓库,不会自动在本地创建项目目录。我们对个人 Fork 有写权限,但原仓库仍然由原作者维护。
如果是组织内部协作,并且自己已经拥有原仓库写权限,通常不需要 Fork,可以直接克隆原仓库并推送功能分支。本文讨论的是没有原仓库写权限的开源贡献场景。
4. 第二步:Clone 自己的 Fork 到本地
应克隆自己的 Fork,而不是原作者的仓库:
1 | git clone git@github.com:xiaoming/task-board.git |
克隆完成后查看远程配置:
1 | git remote -v |
输出类似:
1 | origin git@github.com:xiaoming/task-board.git (fetch) |
git clone 会自动将被克隆的仓库命名为 origin。因为这里克隆的是个人 Fork,所以:
1 | origin = xiaoming/task-board = 自己的远程仓库 |
后续执行 git push origin ... 时,代码会被推送到自己的 GitHub 仓库。
如果误将原仓库克隆为 origin,推送时很可能出现权限错误,因为普通贡献者没有向原仓库写入代码的权限。
5. 第三步:添加原仓库为 upstream
只配置个人 Fork 还不够。其他贡献者的代码会不断合并到原仓库,我们需要能够从原仓库获取这些更新。
将原仓库添加为名为 upstream 的远程:
1 | git remote add upstream git@github.com:octo-org/task-board.git |
再次检查:
1 | git remote -v |
此时应该看到:
1 | origin git@github.com:xiaoming/task-board.git (fetch) |
虽然列表中会显示 upstream 的 push 地址,但没有原仓库写权限时,实际推送仍会被 GitHub 拒绝。
这两个名称只是 Git 的本地别名,但社区通常遵循以下约定:
| 远程名 | 指向 | 常用操作 |
|---|---|---|
origin |
自己的 Fork | 拉取和推送 |
upstream |
原作者的仓库 | 获取最新代码 |
记忆方式是:从 upstream 获取项目的新变化,把自己的代码推送到 origin。
6. 第四步:从最新上游代码创建功能分支
Fork 创建一段时间后,个人 Fork 的 main 可能落后于原仓库。开始开发前,应先同步上游代码。
6.1 获取上游更新
1 | git fetch upstream |
fetch 只下载远程提交并更新 upstream/main 等远程跟踪分支,不会立刻修改当前工作区。
查看当前分支和状态:
1 | git status |
切换到本地 main,将它快进到上游最新位置:
1 | git switch main |
--ff-only 表示只接受快进更新。如果本地 main 包含上游没有的提交,命令会停止,而不是自动制造一次合并。这也是不在 main 上直接开发的原因之一。
6.2 同步个人 Fork 的主分支
本地 main 更新后,将它推送到个人 Fork:
1 | git push origin main |
此时三处主分支保持一致:
1 | upstream/main |
6.3 创建任务分支
从最新的 main 创建本次修复分支:
1 | git switch -c fix/issue-128-empty-title |
确认当前分支:
1 | git branch --show-current |
输出应该是:
1 | fix/issue-128-empty-title |
不要直接在 main 上开发。独立功能分支具有以下好处:
- 一条分支只对应一个 Issue 或任务;
- 可以随时同步干净的
main; - Pull Request 只包含与当前任务有关的提交;
- 多个任务可以互不干扰地并行进行。
7. 第五步:在本地开发和验证
现在开始修复 Issue #128。假设我们修改了表单校验和对应测试:
1 | src/components/TaskForm.js |
开发过程中随时查看状态:
1 | git status |
查看尚未暂存的具体变化:
1 | git diff |
完成修改后,执行原项目要求的测试和格式检查。例如项目使用 npm 时,可能是:
1 | npm test |
实际命令应以仓库的 README.md、CONTRIBUTING.md 和 package.json 为准。
这一阶段所有文件都只存在于本地工作区。GitHub 上的个人 Fork 和原仓库都还没有发生变化。
8. 第六步:检查并提交本地修改
先查看有哪些文件发生变化:
1 | git status |
明确暂存本次任务涉及的文件:
1 | git add src/components/TaskForm.js tests/TaskForm.test.js |
检查即将进入提交的内容:
1 | git diff --staged |
这一步非常重要,可以发现调试输出、无关格式化、临时文件或不应该公开的配置是否被误加入提交。
确认无误后创建提交:
1 | git commit -m "fix: reject tasks with an empty title" |
查看最近的提交历史:
1 | git log --oneline --decorate --graph -5 |
此时提交只存在于本地功能分支:
1 | main: A---B---C |
其中 D 是刚刚创建的修复提交。
一个 Pull Request 可以包含多个提交,但每个提交都应尽量完成一件清晰、完整的事情。不要使用“update”“修改一下”等无法说明目的的提交信息。
9. 第七步:推送前再次同步上游
如果开发只用了几分钟,这一步可能没有新内容;如果任务持续了几天,上游 main 很可能已经发生变化。推送前再次检查可以尽早发现冲突。
1 | git fetch upstream |
查看自己的分支与最新上游之间的提交关系:
1 | git log --oneline --graph --decorate --all -15 |
由于该功能分支目前只有自己使用,可以把本地提交变基到最新的 upstream/main:
1 | git rebase upstream/main |
如果上游没有新提交,Git 会提示当前分支已经是最新状态。如果发生冲突,需要手工编辑冲突文件,然后执行:
1 | git add path/to/conflicted-file |
想放弃本次变基并恢复到操作前:
1 | git rebase --abort |
冲突文件通常会用 <<<<<<< HEAD、======= 和 >>>>>>> 三组标记分隔两边内容。解决冲突时不能只机械选择某一边,应理解双方修改目的,组合出正确结果,并重新运行测试。
如果项目贡献指南要求使用 merge 而不是 rebase,应遵守项目规范。不要对 upstream/main 或其他人的共享分支执行变基和强制推送。
10. 第八步:Push 功能分支到个人 Fork
将本地功能分支推送到 origin:
1 | git push -u origin fix/issue-128-empty-title |
这里的每个参数都有明确含义:
1 | origin 目标是自己的 Fork |
推送完成后,GitHub 上的个人 Fork 会出现这个分支:
1 | xiaoming/task-board:fix/issue-128-empty-title |
原仓库的 main 此时仍然没有改变。
建立跟踪关系后,如果继续在这个分支提交,只需执行:
1 | git push |
可以使用下面的命令检查跟踪关系:
1 | git branch -vv |
11. 第九步:向原仓库发起 Pull Request
推送成功后,打开个人 Fork 页面:
1 | https://github.com/xiaoming/task-board |
GitHub 通常会显示 Compare & pull request 按钮。点击后,必须仔细核对 Pull Request 的方向:
| 选项 | 应选择的值 | 含义 |
|---|---|---|
| base repository | octo-org/task-board |
希望代码最终进入的原仓库 |
| base | main |
原仓库中接收修改的目标分支 |
| head repository | xiaoming/task-board |
包含修改的个人 Fork |
| compare | fix/issue-128-empty-title |
刚刚推送的功能分支 |
可以把方向理解为:
1 | xiaoming/task-board:fix/issue-128-empty-title |
如果 base 和 compare 选反,就变成请求把原仓库代码合并到自己的 Fork,这不是我们想要的结果。
11.1 编写 Pull Request 标题
标题应简洁说明最终效果,例如:
1 | fix: reject tasks with an empty title |
不要只写 Fix bug 或 Update code,否则维护者无法快速判断修改内容。
11.2 编写 Pull Request 描述
一份清楚的描述可以这样写:
1 | ## 修改原因 |
Fixes #128 可以把 Pull Request 与 Issue 关联起来,并在满足 GitHub 的关闭条件后自动关闭对应 Issue。
提交前还应检查 GitHub 展示的 Files changed,确认 Pull Request 中没有混入其他任务的文件、生成物、密钥或个人配置。
如果代码还未完成,但希望维护者提前讨论方案,可以创建 Draft Pull Request;完成后再将它标记为 Ready for review。
12. 第十步:根据 Review 继续修改
维护者可能会提出修改意见,例如要求补充边界测试。此时不需要重新 Fork、不需要创建新分支,也不需要关闭当前 Pull Request。
回到原来的本地功能分支:
1 | git switch fix/issue-128-empty-title |
修改并测试后继续提交:
1 | git add tests/TaskForm.test.js |
由于本地分支已经跟踪 origin/fix/issue-128-empty-title,新的提交会被推送到个人 Fork,并自动显示在原来的 Pull Request 中。
Pull Request 实际上持续关注的是两个分支之间的差异,而不是只保存创建瞬间的那一个提交。因此,向源分支继续推送后,不需要新建 PR。
处理完 Review 后,应在对应讨论中回复变更内容,并等待检查和审批重新完成。
13. Pull Request 期间上游发生变化怎么办
等待审查期间,其他代码可能已经合并到 upstream/main。如果 GitHub 提示分支落后或产生冲突,可以在本地整合最新上游代码。
先获取上游更新:
1 | git fetch upstream |
对于已经推送并进入审查的分支,使用 merge 不会改写现有提交:
1 | git merge upstream/main |
如果有冲突:
- 使用
git status查看冲突文件; - 手工编辑并删除冲突标记;
- 重新运行测试;
- 暂存解决后的文件;
- 完成合并提交并推送。
1 | git add path/to/conflicted-file |
更新会自动进入同一个 Pull Request。
如果项目明确要求线性历史,也可以 rebase,但 rebase 会改写功能分支的提交 ID。已经推送后需要使用:
1 | git push --force-with-lease |
--force-with-lease 比 --force 更安全,但仍会改写远程历史。只有确认分支由自己独占,并且项目允许时才能使用。多人共用分支时优先 merge,避免覆盖他人刚推送的提交。
14. Pull Request 合并后的清理
维护者批准并合并 Pull Request 后,修复已经进入:
1 | octo-org/task-board:main |
但本地 main 和个人 Fork 的 main 不会自动更新,需要再次同步。
14.1 更新本地主分支
1 | git switch main |
14.2 更新个人 Fork
1 | git push origin main |
现在原仓库、本地主分支和个人 Fork 的主分支重新一致,可以作为下一个任务的起点。
14.3 删除已经完成的功能分支
删除本地分支:
1 | git branch -d fix/issue-128-empty-title |
删除个人 Fork 上的远程分支:
1 | git push origin --delete fix/issue-128-empty-title |
如果已经在 GitHub 的 Pull Request 页面点击 Delete branch,远程分支可能已经删除,可以省略第二条命令。
如果维护者使用 Squash and merge,功能分支原来的提交不会成为 main 的直接祖先,git branch -d 可能拒绝删除。这时应先确认 Pull Request 已合并、修改已存在于最新的 upstream/main,再执行:
1 | git branch -D fix/issue-128-empty-title |
最后清理已经失效的远程跟踪引用:
1 | git fetch --prune origin |
15. 下一次贡献时从哪里开始
Fork 和 Clone 通常只需要做一次。以后参与同一个项目,不需要重复创建 Fork,也不需要重新克隆仓库。
每次开始新任务时执行:
1 | git switch main |
然后重复以下循环:
1 | 开发与测试 |
需要特别注意:每个新任务都应从最新的 main 创建新分支,不要继续复用上一次已经合并的功能分支。否则新的 Pull Request 可能混入旧提交。
16. 常见错误与排查方法
16.1 克隆了原仓库,Push 时没有权限
错误信息可能包含:
1 | Permission denied |
或者:
1 | Write access to repository not granted |
先查看 origin:
1 | git remote -v |
如果 origin 指向 octo-org/task-board,将它改为自己的 Fork,再把原仓库配置为 upstream:
1 | git remote set-url origin git@github.com:xiaoming/task-board.git |
如果已经存在 upstream,不要重复添加,使用 git remote set-url upstream ... 修改即可。
16.2 在 main 上完成了开发
如果修改尚未提交,可以直接从当前位置创建功能分支:
1 | git switch -c fix/issue-128-empty-title |
未提交的工作区修改会跟随到新分支,然后可以正常提交。
如果已经在本地 main 创建提交,但还没有推送,可以先创建功能分支保存当前提交,再将本地 main 恢复到上游位置。重置分支可能丢失工作,操作前应确认提交已经能从功能分支找到。
16.3 Pull Request 方向选反
正确方向始终是:
1 | 自己的 Fork:功能分支 → 原仓库:main |
创建前核对 base repository、base、head repository 和 compare 四个选项。
16.4 Push 到了个人 Fork 的 main
只要没有强制推送,一般不会影响原仓库,但会让个人 Fork 的主分支包含任务提交。应为贡献创建功能分支,并让个人 main 只用于跟踪上游。
不要为了快速恢复而随意执行强制推送。先使用 git log --graph --oneline --all 理解分支关系,再决定如何处理。
16.5 推送被拒绝:non-fast-forward
说明远程同名分支包含本地没有的提交。先获取并查看差异:
1 | git fetch origin |
如果分支只有自己使用,可能是另一台电脑推送了更新,应先 merge 或 rebase。不要看到错误后立即使用 git push --force。
16.6 GitHub 上没有出现功能分支
检查当前分支、最近提交、远程地址和跟踪关系:
1 | git status |
常见原因是只执行了 git commit 而没有 git push,或者把代码推送到了另一个远程或分支。
16.7 Pull Request 混入了其他提交
常见原因包括:
- 从旧功能分支创建了新分支;
- 在同一分支连续处理多个任务;
- base 分支选择错误;
- 本地
main本身包含未合并到上游的提交。
最稳妥的习惯是每次先同步 upstream/main,然后从干净的本地 main 创建一个全新功能分支。
17. 完整命令清单
首次参与项目
先在 GitHub 页面完成 Fork,然后执行:
1 | git clone git@github.com:xiaoming/task-board.git |
开始 Issue #128
1 | git switch main |
开发并提交
1 | git status |
推送到个人 Fork
1 | git fetch upstream |
随后在 GitHub 上创建:
1 | xiaoming/task-board:fix/issue-128-empty-title |
Pull Request 合并后
1 | git switch main |
18. 总结
在没有原仓库写权限的情况下,一次完整的 GitHub 协作流程是:
- 在 GitHub 上将原仓库 Fork 到自己的账号;
- Clone 自己的 Fork 到本地;
- 将个人 Fork 配置为
origin,原仓库配置为upstream; - 从最新的
upstream/main创建独立功能分支; - 在本地开发、测试并提交;
- 将功能分支 Push 到自己的
origin; - 从个人 Fork 的功能分支向原仓库的
main发起 Pull Request; - 在同一分支继续处理 Review 和冲突;
- PR 合并后同步主分支并清理功能分支。
最重要的是始终分清两个远程:
1 | upstream:原作者仓库,负责提供最新代码 |
只要围绕这条主线理解命令,Fork、Clone、Fetch、Push 和 Pull Request 就不再是互相割裂的操作,而是一条清晰的开源协作链路。
参考资料
[4] GitHub Docs:从 Fork 创建 Pull Request
[5] Git 官方文档










