Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

天格计划技术培训

git 培训

git 是什么

Git 是一款分布式版本控制软件。版本控制是一种随着时间保存更改而不覆盖之前版本的方法;分布式意味着每个使用 Git 仓库的开发者都拥有整个仓库的副本——每一次提交、每个分支、每一个文件。

git 的存储方式

Git 把数据看作是对小型文件系统的一系列快照。在 Git 中,每当你提交更新或保存项目状态时,它基本上就会对当时的全部文件创建一个快照并保存这个快照的索引。为了效率,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。

git快照示意图

Git 中所有的数据在存储前都计算校验和,然后以校验和来引用,这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。 Git 用以计算校验和的机制叫做 SHA-1 散列,这是一个由 40 个十六进制字符(0-9 和 a-f)组成的字符串,基于 Git 中文件的内容或目录结构计算出来, SHA-1 哈希看起来是这样:

24b9da6552252987aa493b52f8696cd6d3b00373

文件的状态与仓库

Git 有三种状态,你的文件可能处于其中之一:

  • 已修改 (modified) 表示修改了文件,但还没保存到数据库中。
  • 已暂存 (staged) 表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。
  • 已提交 (committed) 表示数据已经安全地保存在本地数据库中。

这会让我们的 Git 项目拥有三个阶段:

  • 工作区是对项目的某个版本独立提取出来的内容。这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。
  • 暂存区是一个文件,保存了下次将要提交的文件列表信息,一般在 Git 仓库目录中。
  • Git 仓库目录是 Git 用来保存项目的元数据和对象数据库的地方。这是 Git 中最重要的部分,从其它计算机克隆仓库时,复制的就是这里的数据。

工作目录、暂存区域以及 Git 仓库

基本的 Git 工作流程如下:

  1. 在工作区中新建或修改文件。
  2. 将你想要下次提交的更改选择性地暂存,这样只会将更改的部分添加到暂存区。
  3. 提交更新,找到暂存区的文件,将快照永久性存储到 Git 目录。

文件的状态变化周期

参考资料

git 怎么用

git 安装

可进入 git 官网查找不同操作系统的安装链接或命令。

  • 对于 windows(x64) 用户,可以点击此链接下载 git (2.52.0) ,之后安装即可;
  • 对于 Debian/Ubuntu 用户,可以使用以下命令安装 git :
sudo apt update
sudo apt install git

git 用户信息配置与 ssh 配置

git 用户信息配置

在初次运行 git 前,你应该完成一些配置,最基本的是要设置用户信息,即你的用户名和邮件地址,每一个 Git 提交都会使用这些信息,它们会写入到你的每一次提交中,不可更改。

你可以使用如下命令配置(当你想针对特定项目使用不同的用户名称与邮件地址时,可以在那个项目目录下运行没有 --global 选项的命令来配置)

git config --global user.name "你的用户名"
git config --global user.email "你的邮箱"

你可以使用如下命令查看你的所有 git 配置信息

git config --list --global  # 全局的 git 配置信息
git config --list  # 当前仓库的 git 配置信息

ssh 密钥对生成

为了方便与远程仓库之间的通信,我们可以使用 ssh ,其基本原理是生成一个非对称密钥对,一个公钥(一般以 .pub 为后缀),可以上传到 git 平台或服务器上,一个私钥(与公钥名称相同,没有 .pub 后缀),自己保密。平台用公钥加密,你可以用对应的私钥解密,反之亦然,这样就能建立安全的通讯。

你可以使用如下命令生成一个 ssh 密钥对:

ssh-keygen -t ed25519

该命令可以在后面使用 -C 参数添加注释,譬如你的邮箱信息,也可以不加。输入完成后,会提示你选择密钥保存位置,默认为 ~/.ssh/id_ed25519 ,之接按回车选择默认位置即可;然后会让你设置一个密码(每次使用密钥时需要输入),一般情况下直接留空就行,因为别人一般拿不到你的私钥,所以推荐直接按两次回车;之后你的 ssh 密钥对就被成功生成了,你可以使用如下命令查看你的公钥:

cat ~/.ssh/id_ed25519.pub

本地操作

参考资料

基本命令

  • 初始化一个 git 仓库:
git init
  • 查看 git 状态:
git status
  • 比对更改内容:
git diff  # 比对已修改但未暂存的
git diff --staged  # 比对已暂存但未提交的
  • 查看 git 历史:
git log

也可以使用 tig

# 安装 (debian/ubuntu)
sudo apt update
sudo apt install tig
# 使用
tig
  • 暂存文件:
git add [file]  # 暂存某一文件
git add .  # 暂存所有文件
  • 提交:
git commit -m "[commit message]"
  • 撤回提交:
git revert HEAD  # 撤回最近的提交
git revert [commit]  # 撤回某个 commit , 但要注意如果不是按顺序的, 可能会引起冲突
参考资料

git 特殊文件

  • .gitignore:告诉 Git 哪些文件或目录不应该被跟踪,常用于忽略编译产物、临时文件、本地配置等。其基本特征与语法为:
    • .gitignore 文件按目录层级生效:仓库根的规则对整个仓库有效,子目录下的 .gitignore 只影响其子树。
    • 路径层级越深的 .gitignore 优先级越高, .gitignore 文件中越靠后优先级越高。
    • 每行一个规则,表示忽略的文件或目录,以 # 开头为注释。
    • / 开头表示 Git 仓库的根目录(而非系统的根目录),以非 / 开头表示相对于 .gitignore 所在目录的路径。
    • * 通配符匹配任意字符,不包括目录分隔符 /
    • ** 通配符匹配任意层级的目录。
    • ! 开头表示否定规则,用来重新追踪被忽略的路径。
    • / 结尾表示这是一个目录。
  • .gitkeep:这是一个约定俗称的文件,目的是让 Git 能够跟踪一个空目录。因为 Git 不跟踪空目录,放一个(通常空的).gitkeep 文件到目录中,便能使目录被提交到仓库,从而保留该目录结构。
  • .gitattributes:可以针对特定的路径配置某些设置项,这样 Git 就只对特定的子目录或子文件集运用它们。详见 Git 属性
  • .gitmodules:记录每个子模块的路径和 URL。详见 Git 子模块

分支管理

几乎所有的版本控制系统都以某种形式支持分支。使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线。

一个分支管理的例子

一些常用的命令如下:

  • 查看所有的分支:
git branch
  • 创建一个新分支:
git branch [branch-name]
# 也可以使用以下命令, 它们会同时切换到新创建的分支
git checkout -b [branch-name]
git switch -c [branch-name]
  • 切换分支:
git checkout [branch-name]
git switch [branch-name]
  • 删除分支:
git branch -d [branch-name]
  • 合并分支:
    • merge 命令会把两个分支的最新快照以及二者最近的共同祖先进行三方合并,合并的结果是生成一个新的快照并提交。
    • rebase 命令会将提交到某一分支上的所有修改都移至另一分支上,这样可以保持提交记录为一条直线(见下图)。
git merge [branch-name]
git rebase [branch-name] 

变基示意

参考资料

远程仓库

为了能在任意 Git 项目上协作,你需要知道如何管理自己的远程仓库。远程仓库是指托管在因特网或其他网络中的你的项目的版本库。你可以有好几个远程仓库,通常有些仓库对你只读,有些则可以读写。与他人协作涉及管理远程仓库以及根据需要推送或拉取数据。

一些常用的命令如下:

  • 克隆一个远程仓库:
git clone [url] [path]
  • 查看当前的远程仓库:
git remote
git remote -v  # 可以显示 fetch/push url
  • 添加远程仓库:
git remote add [alias] [url]
  • 删除远程仓库
git remote remove [alias]
git remote rm [alias]
  • 修改远程仓库名字
git remote rename [old-alias] [new-alias]
  • 修改远程仓库的 url :
git remote set-url [alias] [url]
  • 获取远程的所有分支、标签(不会修改当前工作区):
git fetch [alias]
  • 拉取远程仓库的修改:
git pull [alias] [branch]
# 该命令等价于以下两条命令的组合
git fetch [alias]
git merge [alias]/[branch]
  • 向远程仓库推送修改(有时可能需要删除或覆盖掉远程仓库的一些记录,此时可能会要用到 -f--force ,但一般情况下尤其是多人协作的情况下不要使用强推!):
git push [alias] [branch]
# 若远程还没有该分支, 或是未建立绑定关系, 可以使用以下任一命令
git push -u [alias] [branch]
git push --set-upstream [alias] [branch]
参考资料

git 实操练习

在开始下述内容之前,请确保已完成 git 安装git 用户信息配置与 ssh 配置

本地操作

第一次提交

创建一个新文件夹并进入,使用如下命令初始化一个 git 仓库:

git init

创建 .gitignore 文件与 README.md

touch .gitignore
touch README.md  # 你可以写入一些基本信息

把这两个文件暂存并提交

git add .
git commit -m "Initial commit"

使用 .gitignore 取消对某些文件的追踪

创建一个(形式上的) python 虚拟环境:

# 如果你已经准备好了 python
python3 -m venv .venv
# 如果你还没有准备好 python , 可以创建一个形式上的
mkdir .venv
touch .venv/.gitkeep

.gitignore 中,添加以下内容:

.venv/

完成提交:

git add .
git commit -m "chore: 添加了 python 虚拟环境, 并被 .gitignore 忽略."

接下来,我们将体验一下如何取消对某些已追踪文件的追踪。

模拟两个文件,一个是确实有效的文件,一个是测试文件:

touch main.py
touch test.py

不小心忘记在 .gitignore 中添加 test.py ,直接提交

git add .
git commit -m "perf: 完成了主程序."

如果想取消对 test.py 的追踪,你需要先在 .gitignore 中添加以下内容

test.py

但你可以发现 test.py 仍然在仓库中被追踪,这时你需要在 git 缓存中删除它(如果你在本地也不需要保留,可以不加 --cache

git rm --cache test.py

然后完成提交

git add .
git commit -m "chore: 取消对 test.py 的追踪."

撤回提交

模拟创造一个有 bug 的文件并完成提交:

touch bug.py
git add .
git commit -m "feat: 添加一个绝妙的功能."

使用如下命令撤回本次提交

git revert HEAD

分支管理

创建一个分支并切换到该分支上:

git checkout -b new
# 你也可以使用以下命令的组合
git branch new
git checkout new

在该分支开发并提交:

touch new.py
git add .
git commit -m "feat: 添加一个新功能 new.py ."

回到主分支,进行开发并提交:

git checkout master  # 如果你的主分支是 main , 请修改
touch new_m.py
git add .
git commit -m "feat: 添加一个新功能 new_m.py ."

使用 merge 完成分支合并:

git merge new

接下来我们将手动创造一些冲突,并实现冲突处理:

main.py 中写入:

print("hello world")

然后提交:

git add .
git commit -m "feat: 实现了 hello world ."

开一个新分支:

git checkout -b conflict

在该分支下,把 main.py 的内容修改为:

print("Hello world")

然后提交:

git add .
git commit -m "fix: 把 hello world 的首字母大写."

之后回到主分支:

git checkout master  # 如果你的主分支是 main , 请修改

在该分支下,把 main.py 的内容修改为:

print("hello world.")

然后提交:

git add .
git commit -m "fix: 在 hello world 后添加句点."

接着使用 merge 执行合并操作:

git merge conflict

会出现报错,例如:

Auto-merging main.py
CONFLICT (content): Merge conflict in main.py
Automatic merge failed; fix conflicts and then commit the result.

这是因为两个分支下都对 main.py 的同一行进行了修改, Git 不能自动选择该接受哪一个,这时你必须自行处理,打开 main.py ,把里面的内容改为(一个综合考虑两个分支修改的例子,你也可以选择采用某一分支的更改):

print("Hello world.")

然后手动进行合并操作:

git add .
git commit -m "Merge branch 'conflict'"

远程仓库

在 git 平台中添加 SSH Keys 并验证

首先进入天格计划的 GitLab,点击左侧边栏右上角的头像,点 编辑个人资料(Edit Profile) ,在左侧点击 SSH Keys ,点 添加新秘钥(Add new key) ,然后把你的公钥粘贴进去。

我们可以尝试 clone 一个仓库,来看一下你的 ssh key 能否正常使用,进入一个新的文件夹,输入:

git clone ssh://git@git.grid.science:30022/grid-training/git-example.git

如果能够 clone 下来,就说明你的 ssh key 可以正常使用;否则可能会报错,例如:

Cloning into 'git-example'...
git@git.grid.science: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

建立自己的远程仓库并使用

点击左侧边栏靠上方位置的 搜索或转到...(Search or go to...) ,点 您的工作(Your work) ,在左侧点 项目(Projects) ,然后点击蓝色的 新建项目(New Project) 按钮,选择 创建空白项目(Create blank project) 。之后输入你的 项目名称 (Project name) (自取),在 项目 URL (Project URL)选择群组或命名空间(Pick a group or namespace) 中,选择 用户 (Users) 下你的 id ,对于仓库的 可见性级别 (Visibility Level) ,你可以自行选择, 私有(Private) 意味着除本仓库的成员外其他人不可见,内部(Internal) 意味着只有能登陆天格计划 GitLab 的成员可见(相当于半公开),最后取消掉下面的 使用自述文件初始化仓库(Initialize repository with a README) (如果选择了,仓库会自动生成一个 README.md ,该 README.md 中一般只包含一个类似于教程的操作信息,为避免冲突,建议不勾选) ,点击蓝色的 新建项目(Create project) 按钮 ,即完成了你的仓库创建。

在你的仓库中,点击蓝色的 代码(Code) 按钮,点第一个复制按钮来复制 使用 SSH 克隆 (Clone with SSH) 下的链接,下文称为 <url>

回到我们刚才一通操作的那个仓库,为它添加远程仓库:

git remote add origin <url>

然后把本地仓库发布到远程:

git push -u origin master
# 如果你想把 master 以外的分支也 push 上去
git push -u origin new
git push -u origin conflict

检查你的远程仓库,可以发现里面已经有你本地仓库的内容了。

接下来,我们将模拟多人协作的场景,来制造一些冲突并解决。

首先在远程仓库中,点击蓝色的 代码(Code) 按钮,点 Web IDE ,这将会打开一个网页端的 VSCode ,修改 main.py

print("A: Hello world.")

接下来,你可以使用图形化界面,点击左侧的 git 图标,在框中写入 commit message ,例如 "feat: 让 A 说 Hello world." ,然后点击 Commit and push to 'master' ,点 Continue 。回到你的远程仓库后,你可以发现完成了修改。但是你的本地仓库还没有同步,我们来手动制造一点冲突,在本地把 main.py 修改为:

print("B: Hello world.")

然后提交:

git add .
git commit -m "feat: 让 B 说 Hello world."

接下来,我们来完成一下同步,输入:

git pull origin master

这时由于本地和远程都有更新,会出现分叉的分支,所以会报错,例如:

hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint: 
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint: 
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.

上述提示中给出了三种解决方案,分别是 merge, rebase, fast-forward only ,如果使用了它提供的 git config 命令,会给仓库设置默认的合并策略,我们先不设置,而是指定本次使用 rebase 策略进行 pull

git pull --rebase origin master

然后会发现,出现了和之前类似的冲突报错,例如:

Auto-merging main.py
CONFLICT (content): Merge conflict in main.py
error: could not apply 6e65887... feat: 让 B 说 Hello world.
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 6e65887... feat: 让 B 说 Hello world.

然后手动解决冲突,譬如选择接受让 B 说,然后输入:

git add .
git commit -m "feat: 让 B 说 Hello world."
git rebase --continue

然后推送到远程仓库:

git push origin master