远程仓库基础

本地仓库可以和服务器上的远程仓库建立联系,通常将架设在服务器上的仓库用于备份或者开源,有如下特点:

  • 本地和远程仓库的网络通信通常基于ssh或https协议,两者的区别在于权限,例如是否需要每一次推送操作输入密码确认等
  • 可以有多个远程仓库,虽然个人使用时通常只需要一个
  • origin 是习惯上默认的远程仓库名称,当然也可以改成其他名称,有些命令在缺省时会尝试对名为origin的远程仓库进行操作
  • 远程仓库和本地仓库在地位上是不等价的
    • 单纯的添加操作对远程仓库没有任何影响
    • 拉取和推送都只能由本地仓库向远程仓库发起,远程仓库并不能主动向本地仓库发送信息
    • 本地仓库向远程仓库发起推送或拉取可能会受到权限限制,尤其是在多人合作的项目中

由于本地仓库和远程仓库都存在HEAD和分支,在逻辑管理上更加复杂,从实现的角度来说:

  • 本地的HEAD和分支是可修改的,具体实现在.git/refs/heads
  • 远程仓库的HEAD和分支是只读的,只是一个影子或书签。对于每一个远程仓库,HEAD和分支的具体实现在.git/refs/remotes/<remote_name>/中,例如对于名为origin的远程仓库,有HEAD和分支保存在.git/refs/remotes/origin/
    • 远程仓库的HEAD,名称为origin/HEAD
    • 远程仓库的分支,名称格式为<remote>/<branch>,例如origin/main

还具有如下的特点:

  • 远程仓库的HEAD和远程分支都只是远程仓库留在本地的只读的影子,这个影子是上一次和远程仓库进行网络通信后留下的缓存信息,而不是远程仓库目前的真正状态,并且影子很可能是过时的信息!
  • 通常的Git本地命令不会进行网络通信,例如git loggit status,虽然它们也会显示远程仓库的HEAD和分支信息,但是只是基于缓存的信息
  • 有限的几个远程命令会主动通过网络获取信息,例如使用git ls-remote加上远程仓库的名称,可以通过网络通讯查看对应仓库所有的远程引用列表的真实状态,包括名称和哈希值,使用git remote show会先调用git ls-remote命令,因此获取的状态信息也是最新的

remote命令

git remote以及它附带的命令是管理远程仓库的主要命令,用法比较复杂,这里从命令的角度来梳理一下

命令原型如下

1
2
3
4
5
6
7
8
9
10
11
12
13
git remote [-v | --verbose]
git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
git remote rename [--[no-]progress] <old> <new>
git remote remove <name>
git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
git remote set-branches [--add] <name> <branch>…​
git remote get-url [--push] [--all] <name>
git remote set-url [--push] <name> <newurl> [<oldurl>]
git remote set-url --add [--push] <name> <newurl>
git remote set-url --delete [--push] <name> <URL>
git remote [-v | --verbose] show [-n] <name>…​
git remote prune [-n | --dry-run] <name>…​
git remote [-v | --verbose] update [-p | --prune] [(<group> | <remote>)…​]

下面对主要使用的子命令依次介绍。

直接使用(remote)

直接使用git remote会列出当前的远程仓库名称

1
git remote

显示的信息只有远程仓库名称,例如origin

加上-v--verbose)选项会显示更多一点信息,例如远程仓库的URL

1
git remote -v

显示的信息例如

1
2
origin  git@xxx:xxx.git (fetch)
origin git@xxx:xxx.git (push)

添加(remote add)

使用 git remote add 可以添加一个远程仓库,需要指定名称(习惯上为origin)和URL

1
git remote add <remote_name> <remote_url>

添加远程仓库的操作只是更新本地配置,并不会自动获取远程仓库的信息,后续还需要手动获取,例如

1
git fetch <remote_name>

然后就会通过网络获取远程仓库的数据,并存储到本地的数据库中。

或者使用-f选项,相当于在添加远程仓库之后立刻进行拉取

1
2
3
4
5
git remote add -f <remote_name> <remote_url>

# i.e.
git remote add <remote_name> <remote_url>
git fetch <remote_name>

例如刚刚添加了远程仓库之后,远程仓库状态可能为

1
2
3
4
5
6
* remote origin
Fetch URL: git@xxx:xxx.git
Push URL: git@xxx:xxx.git
HEAD branch: main
Remote branch:
main new (next fetch will store in remotes/origin)

执行一次拉取 git fetch 之后,远程仓库状态变为

1
2
3
4
5
6
* remote origin
Fetch URL: git@xxx.git
Push URL: git@xxx.git
HEAD branch: main
Remote branch:
main tracked

重命名(remote rename)

使用 git remote rename 可以重命名远程仓库

1
git remote rename <remote_name_old> <remote_name_new>

注意这个名称只是在本地仓库的配置文件中,远程仓库的一个代号,并不会对远程仓库有什么影响。

移除(remote remove)

使用 git remote remove 可以移除远程仓库

1
git remote remove <remote_name>

查询(remote show)

如果需要获取指定的远程仓库的详细信息,可以使用如下命令

1
git remote show <remote_name>

Git会通过网络查询并显示指定远程仓库的具体信息,显示的信息类似于

1
2
3
4
5
6
7
8
9
10
* remote origin
Fetch URL: git@xxx:xxx.git
Push URL: git@xxx:xxx.git
HEAD branch: main
Remote branch:
main tracked
Local branch configured for 'git pull':
main merges with remote main
Local ref configured for 'git push':
main pushes to main (up to date)

如果使用 git clone 克隆得到的本地仓库,那么这就是克隆完成的默认结果,将本地的 main 分支和 origin/main 绑定。

git remote show会通过网络通信来获取远程仓库的最新信息:

  • 如果远程有更新的改动,会显示(local out of date)
  • 否则会显示(up to date)

而其它的本地命令通常不会使用网络通信,即使git remote show选项也可以加上-n选项,要求在查询时只使用本地缓存信息,否则默认会使用git ls-remote进行网络查询。

补充

使用git remote get-url可以查询远程仓库的URL

1
git remote get-url <remote_name>

使用git remote set-url可以修改远程仓库的URL

1
git remote set-url <remote_name> <new_url>

远程仓库操作

获取(fetch)

最基础的获取远程仓库信息的命令是 git fetch ,它的作用是进行网络通信,将远程仓库的分支下载到本地,并且下载在分支的提交历史中所涉及的所有快照数据。

命令原型如下

1
2
3
4
git fetch [<options>] [<repository> [<refspec>…​]]
git fetch [<options>] <group>
git fetch --multiple [<options>] [(<repository> | <group>)…​]
git fetch --all [<options>]

例如指定从origin仓库获取信息

1
git fetch origin

在仓库名称缺省时,如果当前的本地分支绑定了一个远程分支,则获取对应远程仓库的更新,否则默认尝试获取名为 origin 的远程仓库

1
git fetch

使用 --all 选项可以获取所有远程仓库

1
git fetch --all

使用--dry-run选项在从远程仓库获取信息之后,并不会修改版本链表,只是进行一次预演

注:git fetch 得到的更新结果会在版本链表中体现,但是并不会主动与当前分支合并,还需要手动操作,将当前的本地分支与指定的远程分支合并或变基

1
git merge origin/main

克隆(clone)

使用 git clone 可以克隆一个现成的远程仓库到本地,注意当前本地仓库是不存在的

命令原型如下

1
2
3
4
5
6
7
8
9
git clone [--template=<template-directory>]
[-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]
[-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>]
[--dissociate] [--separate-git-dir <git-dir>]
[--depth <depth>] [--[no-]single-branch] [--no-tags]
[--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules]
[--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow]
[--filter=<filter> [--also-filter-submodules]] [--] <repository>
[<directory>]

基本用法如下

1
git clone url

这里的URL就是服务器上的xxx/.git对应地址,如果是裸仓库的话直接就是xxx.git,这里URL也可以是本地文件系统中的路径。

git clone 相当于依次调用了如下命令:

  1. 在当前位置创建一个新的子目录用于存储本地仓库,子目录的名称xxx默认会通过解析URL获得, 此时本地仓库会创建在当前的xxx/子目录下,也可以指定本地仓库所处的目录,例如下面的命令会把本地仓库创建在当前的test/子目录下

    1
    git clone url test

  2. git init初始化得到空的 Git 仓库

  3. 根据 URL 添加一个远程仓库,远程仓库的名称默认为origin,相当于git remote add origin URL,也可以使用-o选项指定远程仓库名称,例如

    1
    git clone -o <remote_name> url

  4. 对远程仓库执行git fetch获取远程仓库的内容,包括远程仓库的HEAD以及所有的远程分支,例如origin/HEAD和origin/main

  5. 假设origin/HEAD当前指向远程分支origin/main,那么将会进行如下的本地操作:

    1. 创建同名本地分支main,并将HEAD指向本地分支main
    2. 将本地分支main绑定到远程分支origin/main
    3. 执行git checkout将HEAD指向的提交结点检出到本地的工作目录

克隆之后会得到一个本地的Git仓库,它添加了URL对应的远程仓库,克隆之后的远程仓库状态通常为

1
2
3
4
5
6
7
8
9
10
* remote origin
Fetch URL: git@xxx:xxx.git
Push URL: git@xxx:xxx.git
HEAD branch: main
Remote branch:
main tracked
Local branch configured for 'git pull':
main merges with remote main
Local ref configured for 'git push':
main pushes to main (up to date)

git clone有如下的额外选项:

  • -o:指定远程仓库的名称,缺省时默认名称为origin
  • -n--no-checkout):要求在克隆的最后不检出到工作目录
  • -l--local):如果克隆的URL位于本地文件系统中,可以加上这个选项,此时会绕过关于网络的一些处理,注意即使没有这个选项,克隆也可以正常工作
  • --bare:克隆得到一个裸仓库,此时也不会执行检出操作,因为裸仓库没有工作目录,例如将本地仓库克隆创建为一个裸仓库
    1
    git clone --bare -l /home/user/project/.git /pub/project.git

远程分支操作

分支绑定

我们需要将本地分支和远程分支绑定,建立一一对应关系,此时称本地分支为跟踪分支,称远程分支为上游分支

最基础的设置是git branch加上--set-upstream-to-u)选项,例如将本地分支main绑定远程分支origin/next

1
2
git branch --set-upstream-to master origin/next
git branch -u master origin/next

很多操作都可以顺便设置上游分支,例如git push,使用的同样也是--set-upstream-to-u)选项

1
2
git push --set-upstream-to origin main
git push -u origin main

有很多操作会自动进行分支绑定,例如从一个远程分支检出(这相当于是克隆仓库时的一个子过程),就会自动创建同名的本地分支,并将两者相互绑定。 甚至于git checkout <name>如果没有匹配到本地分支name,但恰好找到一个名称匹配的远程分支,也会自动执行这样的过程。

注:

  • git fetch以及git pull也可以设置,但是只支持--set-upstream-to不支持简写-u,并且不常见到,主要是通过git push绑定远程分支
  • 如果当前HEAD指向的本地分支已经绑定了远程分支,则git fetchgit pullgit push三个远程操作命令可以省略很多参数,见下文
  • 使用git branch -vv可以查看本地分支以及它绑定的远程分支的详细信息,包括跟踪分支相比于上游分支的比较是否落后等,但是这个命令似乎不会主动通过网络通讯,因此远程仓库的信息仍然只是过时的缓存信息

拉取(pull)

除了git fetch,还有 git pull 命令可以在git fetch获取远程仓库的基础上,自动尝试将远程分支合并到当前本地分支(或者将本地分支变基到远程分支),git pull命令通常需要三个信息:本地分支,远程仓库,以及远程分支

命令原型如下

1
2
3
4
git pull <remote_repo> <remote_branch>:<local_branch>
git pull <remote_repo> <remote_branch>
git pull <remote_repo>
git pull

最完整的形式如下,考虑将origin仓库的远程分支origin/test合并到本地分支main

1
git pull origin test:main

这相当于

1
2
git fetch origin
git merge origin/test

可以省略本地分支名称,此时默认合并到当前HEAD指向的本地分支

1
git pull origin test

还可以进一步省略远程仓库的分支名称,此时的尝试逻辑如下:

  • 如果HEAD指向的本地分支绑定了指定的远程仓库中的上游分支,会优先选择该上游分支
  • 否则会默认拉取远程仓库的HEAD目前指向的远程分支
1
git pull origin

甚至可以省略三个信息,不带任何选项直接使用git push,此时的尝试逻辑为: 如果HEAD指向的本地分支有且仅有一个上游分支,则会选择这个远程仓库以及对应的远程分支进行拉取,否则命令报错。

1
git pull

注意:

  • 默认git pull在获取之后的行为是merge,也可以通过选项或者设置将其更改为 rebase,例如直接使用-r--rebase)选项

  • 对于git pull如果总是希望执行变基而非合并,可以使用如下设置

    1
    git config --global branch.autosetuprebase always

  • 如果当前状态下,本地未提交的修改和从远程仓库新获取的修改有冲突,则在获取之后会放弃合并/变基

推送(push)

推送到远程仓库的命令是 git push ,它的作用是进行网络通信,将本地仓库的分支推送到远程仓库,并且推送在分支的提交历史中所涉及的所有快照数据。 推送的命令需要明确三个信息:本地分支,远程仓库,以及远程分支名称

命令原型如下

1
git push <remote_repo> <local_branch>:<remote_branch>

最完整的形式如下,将本地分支main推送到origin仓库的远程分支origin/test

1
git push origin main:test

可以省略远程分支名称,包括两个示例:

  • 将本地分支test推送到远程仓库的同名分支origin/test

    1
    git push origin test

  • 本地的HEAD指向的本地分支推送到远程仓库的同名分支

    1
    git push origin HEAD

可以进一步省略远程仓库中的分支名称,包括两个示例:

  • 使用:会将所有与该远程仓库建立绑定关系的本地分支推送到各自的上游分支

    1
    git push origin :

  • 不使用:会尝试将HEAD指向的当前本地分支推送到远程仓库的同名分支

    1
    git push origin

甚至可以省略三个信息,不带任何选项直接使用git push,此时的尝试逻辑如下:

  • 推送的源选择当前HEAD指向的本地分支
  • 关于推送的目的地选择:
    • 如果HEAD指向的本地分支有且仅有一个上游分支,则会选择这个远程仓库以及对应的远程分支进行推送
    • 如果没有绑定上游分支,则会尝试推送到名为origin的远程仓库
1
git push

注意:

  • 如果远程仓库目前没有指定名称的远程分支,那么将直接在远程仓库中创建分支

  • 如果要求删除远程仓库的指定名称的分支,可以使用如下命令

    1
    2
    git push <remote_repo> --delete <remote_branch>
    git push origin --delete master

  • 可以使用 --set-upstream-to 或者简称 -u参数设置上游分支,例如指定当前本地分支的上游分支为origin/main,此时会首先将当前的本地分支的上游分支设置为 origin/main ,然后再进行推送。(下次再执行 git push 就不再需要指定,可以自动进行推送)

    1
    2
    git push --set-upstream-to origin main
    git push -u origin main

小结

git pullgit push的参数设置是有规律的。完整形式下,首先需要单独提供远程仓库名,然后是通过:连接的两个对应分支

  • 如果是pull行为,格式为<remote_branch>:<local_branch>,只提供一个时,视作:前面的远程分支
  • 如果是push行为,格式为<local_branch>:<remote_branch>,只提供一个时,视作:前面的本地分支

我们可以完全省略两个对应分支的名称,只提供远程仓库名称,Git将根据远程仓库的配置和分支名称自动推测具体的行为。 我们还可以省略所有参数,如果已经配置好了唯一的远程仓库以及追踪分支,那么行为就是我们想要的,否则Git会推测对应的默认行为或报错。 如果不清楚Git的具体行为,可以在命令最后加上-vv参数,这会显示更多信息。

补充

远程仓库信息

现在我们可以解释git remote show显示的所有详细信息

1
git remote show origin

它显示的信息类似于

1
2
3
4
5
6
7
8
9
10
* remote origin
Fetch URL: git@xxx:xxx.git
Push URL: git@xxx:xxx.git
HEAD branch: main
Remote branch:
main tracked
Local branch configured for 'git pull':
main merges with remote main
Local ref configured for 'git push':
main pushes to main (up to date)

这些详细信息依次为:

  • 远程仓库的名称和URL,从URL也可以看出,只有fetch和push两个最基本的命令,而pull只是对fetch的封装
  • 远程仓库的HEAD指向的远程分支为origin/main
  • 远程仓库的分支的状态,本地分支main绑定到远程分支origin/main
  • git pull的默认行为:本地分支main会合并远程分支origin/main
  • git push的默认行为:本地分支main会推送到远程分支origin/main(并且已经同步到最新)

值得注意的是,git pushgit pull的默认行为是可以不同的,例如可以让git push自动推送,但是git pull不自动拉取。

远程标签操作

在前文中我有意识地忽略了涉及远程仓库的标签操作,这里补充一下。

从远程仓库单独获取标签的更新(默认git fetch origin也会顺便获取标签的更新)

1
git fetch origin --tags

查看远程仓库的所有标签

1
git ls-remote --tags

远程仓库的标签被删除后,本地仓库无法通过命令直接获取到删除的信息,只能通过git ls-remote获取到远程仓库的标签,然后与本地仓库的标签比对,手动删除本地标签。

在远程推送时并不会附带标签信息,如果想要推送标签到远程仓库,需要额外的处理,例如

1
2
3
4
5
6
7
8
# 推送单个标签的添加
git push origin v1.0

# 推送所有标签的添加
git push origin --tags

# 推送单个标签的删除
git push origin --delete v1.0

多个远程仓库

假设我们有两个远程仓库:

  • 一个仓库在私人的服务器上:git@XXX:Demo.git,命名为origin
  • 另一个是Github仓库:git@github:Demo.git,命名为github

为了避免分支操作的麻烦,始终保持一个本地分支main,两个远程仓库也只有main分支(其它情况下的分支都是临时操作), 那么本地main分支可以绑定origin/main,然后推送操作如下

1
2
git push
git push github

分别推送到两个远程仓库。

实践发现,可以做到git push同时推送到两个仓库,只需要对两个仓库进行配置。

拉取操作对于非绑定的仓库则必须加上远程分支名称

1
2
git pull
git pull github main

如果在本地维护两个分支分别追踪两个远程仓库,实践中发现对应的操作非常繁琐,某个本地分支必然和远程分支(xxx/main)不同名,git push/pull必须要加上更多的参数。

SSH和HTTPS协议的URL

Github同时提供基于HTTPS和SSH协议的远程仓库URL,例如

1
2
https://github.com/user/repo.git
git@github.com:user/repo.git

对于两种URL的使用有很多区别:

  • 使用SSH协议URL的前提是我们在本地和Github已经成功配置了所需的SSH密钥对
  • 使用HTTPS协议URL不需要在本地配置,对于公开和私有仓库,对应格式的HTTPS协议URL都是有效的
  • 对于HTTPS协议URL,在远程操作时可能需要进行身份验证,通常要求我们输入远程仓库所对应的用户名和密码
    • 对于公开仓库,git clonegit fetchgit pull不需要身份验证,git push需要身份验证
    • 对于私有仓库,git clonegit fetchgit pullgit push都需要身份验证

由于HTTPS协议URL在每次操作都需要进行身份验证,我们可以将身份验证信息保存在本地,在远程操作时自动使用。

我们有很多做法来达到这个目的,例如最简单的方法有:

  • 将身份验证信息在内存中缓存,当前会话结束会失效
  • 将身份验证信息明文保存在文件中,默认存储在~/.git-credentials文件中(注意不是存储在当前仓库中)

这两种做法都支持对不同仓库存储或缓存不同的身份验证信息。

这两种做法对应的配置命令为

1
2
git config credential.helper cache
git config credential.helper store

这里的配置只是当前仓库的,我们也可以加上--global使得对应配置全局生效。

注意:

  • 对于保存在本地文件中的凭证,如果在自动使用时出现授权失败,那么本地的记录也会自动删除;
  • 上文中的两种做法都是相当不安全的,操作系统中通常都提供了更安全的凭证管理方式,这里不作讨论;
  • Github在安全方面的要求很高,它不允许直接输入用户名和密码进行身份验证,需要在Github上获取专门的token替代密码。