Skip to content
This page has been auto-translated and may contain errors.View in English

远程仓库与 GitHub ​

docs.scrimba.com

到目前为止,这本手册里出现的仓库都只待在一个地方:你自己的电脑。这在大多数情况下都没问题,直到你想要一份能在笔记本电脑损坏后保住数据的备份,或者想换一台电脑继续这个项目,或者想让别人也能参与进来。这时你需要的是一份仓库的拷贝,放在所有相关设备都能访问到的地方。这份拷贝就叫远程仓库(remote),大多数开发者会把它放在 GitHub 上。

什么是远程仓库(以及 GitHub 在其中扮演的角色) ​

远程仓库(remote) 是托管在别处(通常是 GitHub)的一份仓库拷贝,而不是存在你自己的电脑上。克隆一个仓库时,Git 已经帮你设置好了一个远程仓库,默认命名为 origin。运行 git remote -v(-v 表示同时显示名称和对应的 URL)就能看到它:

bash
$ git remote -v
origin  https://github.com/mara-chen/weather-app.git (fetch)
origin  https://github.com/mara-chen/weather-app.git (push)

Git 认识的每个远程仓库都会列出两次,一次对应拉取(fetch),一次对应推送(push),因为理论上这两者可以指向不同的地址。但实际上它们几乎总是同一个地址。origin 是 Git 在克隆时自动分配的昵称。你可以给它改名,或者再添加一个指向别处的远程仓库,但几乎每个你接触到的项目,主远程仓库都叫 origin。

这里正好可以再强调一下几乎每个新手都会踩的一个坑:Git 和 GitHub 不是一回事。Git 是运行在你机器上的版本控制工具,不管你有没有联网,它都会记录提交。GitHub 是一个网站,托管着你仓库的一份拷贝,为你提供一个可以推送和拉取的远程仓库。git remote、git push 和 git pull 就是连接这两者的命令:它们让运行在你笔记本上的 Git,和存放在 GitHub 服务器上的仓库进行对话。

一个远程仓库,说到底不过是一个映射到 URL 的名字,存在你仓库的配置里。除此之外,Git 还会维护一个 远程跟踪分支(remote-tracking branch):一个本地书签,记录着你的机器上次检查时,那个远程上的某个分支处在什么位置。origin/main 正是这样的东西。它并不是真正存在于 GitHub 上的那个 main 分支,而是你的仓库对"上次 fetch 或 pull 时 GitHub 上的 main 在哪个位置"的记忆。用 git branch -r 可以列出 Git 当前保存的远程跟踪分支:

bash
$ git branch -r
origin/main

远程仓库能做的一切,都是通过这条配置项加上这些跟踪书签来实现的。这里没有任何后台连接,也没有什么东西在监视变化。只有当你主动执行 fetch 或 pull 时,Git 才会更新它对远程仓库的认知——这正是本章接下来要讲的内容。

Juno什么是远程仓库 远程仓库是托管在别处(通常是 GitHub)的一份仓库拷贝,Git 把默认的那个叫作 origin。随时运行 git remote -v 就能看到你的仓库知道哪些远程仓库。Git 和 GitHub 不是一回事:Git 是你机器上的工具,GitHub 常常是你远程仓库所在的地方。早点把这两者分清楚,能省下不少之后的困惑。
Juno什么是远程仓库origin 只是 Git 在克隆时给远程仓库起的默认名字,所以在项目需要的时候,你可以给它改名,或者再添加更多远程仓库。git remote -v 会显示每个远程仓库用于 fetch 和 push 的 URL。记住 Git 和 GitHub 的区分:你在本地运行的那些命令属于 Git,而 GitHub 只是这些命令能到达的其中一个目的地。
Juno什么是远程仓库 远程仓库是配置文件里映射到某个 URL 的一个名字,配合像 origin/main 这样的远程跟踪分支——也就是你机器上,关于上次 fetch 或 pull 时那个分支处在什么位置的记忆。没有什么东西会自动更新;Git 只在被要求时才去检查远程仓库。GitHub 实际拥有的内容,和你的远程跟踪分支所记住的内容之间的这个差距,正是几乎所有 fetch 与 pull 相关意外的根源。

把远程仓库放在哪里:GitHub 及其他选择 ​

远程仓库能做的一切都是纯粹的 Git 操作,这就带来一个值得早点知道的结论:另一端的托管平台是可以随意更换的。对 Git 来说,每个托管平台不过是配置里的一个 URL,git push、git pull 和 git fetch 对所有平台的行为都完全一致。托管平台真正添加的东西,是更上一层的东西:代码的网页视图、审查流程、issue 跟踪和权限控制。下面把各个平台的取舍说清楚:

  • GitHub 目前规模最大,托管了绝大多数开源项目。它的优势在于网络效应:你想要参与的项目大多已经在这里,教程默认以它为准,雇主也会去这里找你的个人主页。代价是它是一个封闭平台(归 Microsoft 所有),你越依赖它在 Git 托管之外提供的那些附加功能,就越难脱离它。
  • GitLab 是最接近的完整替代品,核心功能相同,只是叫法略有不同(在这里,pull request 叫作"merge request")。它最突出的特点是提供一个可以自己搭建在自有服务器上的自托管版本,这也是那些希望把代码留在内部的公司常常选它的原因。代价是它打包了太多功能,整个平台可能会比你原本只想要的代码托管服务显得更沉重。
  • Bitbucket 是 Atlassian 出的托管平台,设计上是为了配合它的项目跟踪和文档工具,也就是 Jira 和 Confluence。如果你的团队已经在用这些工具,它能直接融入进去。离开这个生态之后,它在三大平台里社群规模最小,开源项目的存在感也不高。
  • Codeberg 是一个面向开源项目的非营利托管平台,运行在开源平台 Forgejo 之上。它的吸引力在于,没有任何公司拥有这个托管平台,而且平台本身也是开源的。代价是规模有限:集成少、社群小,而且专注于开源,而不是私有团队协作。
  • 自托管 是所有这些平台背后的共同基础:你可以自己搭建免费开源的 Forgejo 或 Gitea,或者在自己掌控的硬件上运行 GitLab 的自托管版本。你能获得完全的控制权和隐私性;代价是维护它正常运行成了你自己的工作。

这本手册的示例用 GitHub,是因为它托管了大部分开源项目,也是你最可能最先在上面协作的地方。本章的每条命令换到其他任何托管平台都完全适用:换个 URL,日常操作流程照样不变。真正的差异出现在更上一层——每个网站用于审查和合并工作的网页界面,这正是下一章拉取请求流程要讲的内容。

当选择权在你手里时,这件事很少取决于 Git 本身的功能,因为核心的那一套(托管仓库、审查、issue、CI 流水线)在哪里都有。真正起决定作用的是使用场景:想参与开源或者建立公开的个人履历,通常指向 GitHub;一家依赖 Atlassian 工具链运作的公司,通常指向 Bitbucket;代码必须留在自有基础设施上,通常指向 GitLab 自托管版或 Forgejo;而一个价值观驱动的开源项目,可能在 Codeberg 上感觉最自在。而且这个选择也不是永久的。把仓库本身迁移到另一个平台,只需要一条 git remote set-url 加一次推送;迁移真正麻烦的地方,是 Git 之上那一层——issue、wiki 和 CI 配置,这些东西不会跟着提交一起搬过去。

在决定自托管之前,值得把利弊看清楚。Forgejo 和 Gitea 在一台小型服务器上运行起来毫无压力,但运维负担是真实存在的:安全更新、对仓库和平台自身数据库的备份、账户和 SSH 密钥管理,还有可用性——因为一旦你自己搭的这个仓库平台挂了,团队里每个人的推送和拉取都会被卡住。当合规要求代码不能放到第三方基础设施上,或者处于隔离网络环境中,或者一个组织希望把工具链完全掌握在自己手里时,自托管才真正物有所值。而在网页层之下,一切都没有变化:这些托管平台说的都是同一套 Git 协议,所以你的推送和拉取根本分不出它们的区别。

Juno把远程仓库放在哪里 GitHub 是大多数项目所在的地方,也是你刚起步时最保险的默认选择,但它只是众多托管平台之一:GitLab、Bitbucket 以及非营利的 Codeberg 存的都是同样的 Git 仓库。无论远程仓库放在哪里,push 和 pull 的用法都一样,所以就算项目换到别处,你在这里学到的东西也不会白费。
Juno把远程仓库放在哪里 各个托管平台的差异出现在 Git 之上那一层:GitLab 把 pull request 叫作 merge request,并提供自托管版本;Bitbucket 能接入 Atlassian 的工具链;Codeberg 运行在开源的 Forgejo 之上。根据你的协作者和工具链已经在用什么来做选择,记住之后迁移在 Git 层面上代价很低;真正搬不走的是 issue 和 CI。
Juno把远程仓库放在哪里 既然远程仓库不过是映射到 URL 的一个名字,把项目从一个平台迁到另一个平台,只需要 git remote set-url 加一次推送。自托管 Forgejo 或 Gitea 能换来控制权和隐私,代价是打补丁、做备份和保证可用性;只有在政策要求时才做这个取舍,不是为了好玩——这是背过运维值班机的人的经验之谈。所有平台特有的东西,都停留在 Git 协议之上的网页层。

发送和获取你的工作:git push 与 git pull ​

有了远程仓库之后,两个命令就能覆盖大部分日常工作。git push 把你本地的提交发送到远程仓库。git pull 把别处产生的提交拉下来,并合并到你当前所在的分支。

bash
$ git push origin main
Enumerating objects: 5, done.
Writing objects: 100% (3/3), 312 bytes | 312.00 KiB/s, done.
To https://github.com/mara-chen/weather-app.git
   9f8e7d6..a1b2c3d  main -> main

origin 是远程仓库,main 是你要发送的分支。最后一行值得细看:它显示远程上的 main 从一个提交移动到了下一个提交。

拉取的方式一样,只是方向相反:

bash
$ git pull origin main
remote: Enumerating objects: 4, done.
Unpacking objects: 100% (4/4), done.
Updating a1b2c3d..b7c9e21
Fast-forward
 src/index.js | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

这就是和别人协作一个项目时的完整循环:开始前先把别人的改动拉下来,做自己的工作并提交,再推送回去。只要和别人共享一个分支,就要先 pull 再 push。跳过 pull 这一步,你的 push 可能会覆盖在一些你从未见过的提交上面,而 Git 会拒绝这样做,宁可拒绝也不冒着丢掉别人工作的风险。

第一次推送一个新分支时,加上 -u(--set-upstream 的简写):

bash
$ git push -u origin feature/add-forecast-icons
Enumerating objects: 5, done.
Writing objects: 100% (5/5), 412 bytes | 412.00 KiB/s, done.
To https://github.com/mara-chen/weather-app.git
 * [new branch]      feature/add-forecast-icons -> feature/add-forecast-icons
branch 'feature/add-forecast-icons' set up to track 'origin/feature/add-forecast-icons'.

这会把 feature/add-forecast-icons 记录为跟踪 origin/feature/add-forecast-icons,这个关联叫作 上游(upstream)。一旦设置好,之后每一次 push 和 pull 都可以省略远程仓库和分支名,直接在这个分支上运行简单的 git push 或 git pull。如果一个分支还没有配置上游,Git 会提示你,并打印出确切的修复命令,所以你几乎不需要死记这个参数,只要在提示出现时能认出来就行。

这个上游关联,其实就是在运行 -u 的那一刻,写进你仓库配置里的两行普通文本:branch.feature/add-forecast-icons.remote 记录远程仓库的名字,branch.feature/add-forecast-icons.merge 记录它对应的分支。在一个你之前推送过的分支上运行 git config --get branch.main.remote,你就会看到正是这个值。git status 用来告诉你"你的分支比 origin/main 领先几个提交"或者"两者已经分叉"的信息,读的正是这两行配置:这个比较完全是拿你的远程跟踪分支来做的,是一次没有从 GitHub 抓取任何东西的本地检查。只要设置一次上游,之后 Git 给你显示的每一个"领先/落后"计数,都是从同一对配置行里读出来的。

Juno发送和获取你的工作git push 把你的提交发送到远程仓库,git pull 把别处产生的提交拉下来并合并到你的分支。开始工作前先 pull,做完之后再 push,你就能和项目里的其他人保持同步。跳过 pull 是导致 push 被拒绝最常见的原因。
Juno发送和获取你的工作git push origin main 发送提交,git pull origin main 拉取并合并提交,一旦某个分支用 git push -u 设置好了上游,这两个命令之后就不用再指定远程仓库和分支名了。在共享分支上,要先 pull 再 push,否则你的 push 会因为远程已经领先于你的本地历史而被拒绝。
Juno发送和获取你的工作 你用 git push -u 设置的上游,说到底就是两行配置:branch.<name>.remote 和 branch.<name>.merge,在这个参数执行的那一刻写入。git status 读取同样这两行,告诉你你的分支比 origin/main 领先或落后多少——这是一次完全基于你本地远程跟踪分支的比较。每个分支只需设置一次,之后所有简写的 push、pull,以及所有领先/落后计数,都是免费得到的。

git fetch 与 git pull 的区别 ​

git pull 一步做了两件事:下载远程上发生的任何变化,然后立刻把这些变化合并到你当前所在的分支。git fetch 只做前一半。它只下载新的提交,从不把它们合并到你的分支里。你当前的分支,以及你的工作文件,都会原样保持不变,直到你告诉 Git 去合并或拉取。

这是几乎每个人至少踩过一次的坑:运行 git fetch,看到哪里都没有明显变化,就以为远程上什么都没发生。其实发生了:只是 fetch 还没碰到你的工作分支。要看看下载了什么,可以查看本章前面提到的远程跟踪分支:git log origin/main 会显示那些已经到了远程、但还没到达你本地 main 的提交。等你准备好把它们拉进来时,git merge origin/main 就能做到,或者直接用 git pull,它会一次帮你运行 fetch 加上那次合并。

当你想先看看再决定是否合并时,就单独用 fetch。比方说同事王芳提到她推送了一些东西到 main:先 git fetch,再用 git log origin/main 或 git diff main origin/main,你就能在改动落到你的工作分支之前,确切知道改了什么。确认没问题之后,git merge origin/main 把它合并进来。日常情况下,大多数人会直接用 git pull,因为一步看到并完成合并正是他们想要的。而在共享分支上,当你更愿意先读一读进来的改动,或者还没决定现在是不是合并的好时机时,fetch 就派上用场了。

fetch 实际更新的内容,由一个 refspec 来决定:它是一个存储在你远程仓库配置里的映射规则,告诉 Git 该把远程上的哪些分支拉下来,以及用什么本地名字来保存它们。普通克隆下默认的 refspec 大致是 +refs/heads/*:refs/remotes/origin/*,意思是"把远程 heads 下的每个分支都拿过来,镜像到我本地的远程跟踪分支里,让它们保持一致,即便这意味着覆盖它们原来指向的位置"。这就是 origin/main 保持最新状态背后的全部机制:fetch 读取远程的分支,重写你的远程跟踪分支使其保持一致,然后就此停止。你自己的 main 不会有任何变化,除非有另一个单独的 merge、rebase 或 pull 动作,去处理 fetch 拉下来的内容。

Junogit fetch 与 git pull 的区别git pull 一步下载新提交并合并到你的分支。git fetch 只下载它们,同时更新 Git 对远程当前状态的记忆;你的分支会保持原样,直到你去合并。fetch 之后你的文件里什么变化都没看到,这是正常的,新的提交正在远程跟踪分支上等着呢。
Junogit fetch 与 git pull 的区别git fetch 只下载不合并,git pull 一步下载并合并。当你想在改动落到你的分支之前,先用 git log origin/main 看看进来的提交时,就用单纯的 fetch;等你准备好立刻合并时,就用 pull。把这两者搞混,是团队里最常见的 Git 混淆之一。
Junogit fetch 与 git pull 的区别 一个 refspec 精确决定了 fetch 会拉取哪些内容,以及更新哪些远程跟踪分支,包括 origin/main,而默认的那条规则会镜像远程 heads 下的每个分支。fetch 只会移动你的远程跟踪分支,从不触碰你当前签出的分支,所以合并或拉取始终是一个独立的、需要你主动执行的步骤。

到这里,你已经掌握了什么 ​

push、pull 和 fetch 让你的提交能送到共享的远程仓库,也能再拉回来,这已经涵盖了日常 Git 协作中的大部分工作。这一切都建立在提交循环之上:你依然要先在本地 stage、commit,远程仓库只是改变了这些提交能被送到哪里。接下来要讲的,是如何通过 GitHub 本身来完成这种协作——提出一次改动,让它在合并之前经过审查。拉取请求流程正是从这里接着讲下去的。如果本章里有哪个术语你还觉得没完全吃透,比如远程跟踪分支、refspec、上游,术语表里都有简明的解释,而什么是 Git也值得再看一遍,专门讲清楚 Git 和 GitHub 的区别。