Git 入门
在了解了命令行的基本操作后,我们来尝试使用 Git。这部分的内容是命令和原理结合的,你可以在这里学到 Git 的基本操作和 Git 的工作原理。
获得一个 Git 仓库的方法有两种,一种是“将一个文件夹变成 Git 仓库”,一种是从远程克隆一个 Git 仓库。
将一个文件夹变成一个 Git 仓库
Git 仓库的本质是一个文件夹。所以,我们可以将一个文件夹变成一个 Git 仓库。
你可以输入以下命令,把当前文件夹变成一个 Git 仓库:
git init
或者,你可以输入以下命令,把指定文件夹变成一个 Git 仓库:
git init path/to/folder
Git 仓库和普通文件夹的区别在于,Git 仓库中有一个隐藏的.git
文件夹,这个文件夹中存储了 Git 的所有信息,包括所有的git object,ref等。在任何情况下,你都不要应该修改.git
文件夹中任何的内容。唯一的例外是.git/config
文件,这个文件存储了当前仓库的配置信息,例如远程仓库的地址等。但是你也应该谨慎修改这个文件或者使用git config
命令修改配置。
如果你不小心把一个文件夹变成了 Git 仓库1,直接删除.git
文件夹就可以了。使用以下命令即可:
rm -rf .git
Git 在操作 Git 仓库时,会递归的从当前文件夹向上查找.git
文件夹,直到找到第一个.git
文件夹或者到达文件系统的根目录。所以,你可以在任何一个 Git 仓库的子文件夹中使用 Git 命令。当 Git 仓库嵌套时,Git 会使用离当前文件夹最近的.git
文件夹。
克隆远程仓库
要从远程克隆仓库首先要确保你具有权限,对于 GitHub,GitLab, Bitbucket等代码托管平台的公开仓库,任何人都具有权限,因此可以直接克隆。对于私有仓库,你需要一个具有读取权限的平台账号,然后将平台账户和你的 SSH 密钥绑定。
具有权限后,就能够克隆仓库了,使用以下命令即可:
git clone https://github.com/NEUQ-CS/manual.git
记得将仓库地址更换成你想克隆的仓库的地址。克隆结束后,在当前目录下会产生一个与仓库名一致的文件夹,在本例中,是manual
。
这个仓库就是你克隆的 Git 仓库。
clone
命令还有一些其他参数可以使用
克隆到指定文件夹
在地址后面加上指定路径即可:
git clone https://github.com/NEUQ-CS/manual.git path/to/repo
注意,仓库里的文件会直接存在你指定的文件夹里面,而不是在你指定的目录里放仓库文件夹。
克隆指定分支
加上参数-b <分支名>
即可,例如:
git clone https://github.com/NEUQ-CS/manual.git -b master
这将指定需要克隆master分支.
需要说明的是,即使你选择了一个分支,整个仓库的所有分支都实质上会被克隆下来,只是克隆完成后位于指定分支。
指定克隆深度
使用--depth <深度>
来指定克隆深度:
git clone https://github.com/NEUQ-CS/manual.git --depth=1
仅克隆主分支上包含最新一个提交的完整仓库,克隆的仓库不含有任何历史记录。不推荐使用,除非你只是为了临时下载代码并且不需要历史记录和其他分支。
查看 Git 仓库的状态
在一个 Git 仓库中,你可以使用以下命令查看当前仓库的状态:
git status
你会看到以下输出:
On branch master
Your branch is up to date with 'origin/use-git'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: src/use-git/configure.md
Untracked files:
(use "git add <file>..." to include in what will be committed)
src/use-git/try-git.md
当前在分支master
上,你的分支是最新的。Changes to be committed
中列出了你修改的文件,Untracked files
中列出了你新建的文件。
Git 工作原理
在继续尝试前,我们先来了解一下 Git 的工作原理,你可能已经对上面的输出感到迷惑了。
在一个 Git 仓库的文件夹中,一个文件可能存在以下三种状态的其中一种:
- 未跟踪 Untracked
- 已跟踪 Tracked
- 暂存中 Staged
未跟踪
当你使用 git init
初始化一个仓库时,原有的所有文件都是未跟踪。Git 不会管理任何未跟踪的文件,但是这个文件仍然存在于这个文件夹中。不会管理的意思在于,当你通过 git 操作仓库的状态时,未跟踪的文件不会被修改或删除。如果一定会造成修改2,git 会阻止这次操作,直到你解决完冲突。
已跟踪
这应该是仓库中大部分文件存在的状态。当你刚远程克隆完一个仓库时,所有文件都处于已跟踪状态。这是显然的,因为只有被跟踪的文件才会被提交到远程服务器。
跟踪未跟踪的文件
要把一个未跟踪的文件变为已跟踪状态的方法是,先添加到暂存区,然后再commit。以下是命令行教程
# 添加a_new_file到暂存区
git add a_new_file
# commit在暂存区的所有文件
git commit -m "add `a_new_file` to this repo"
将已跟踪的文件删除
你可以使用git rm <文件>
来删除,这会直接把删除操作直接添加到暂存区,文件系统上的文件也会被删除。它等价于以下操作:
rm an_old_file
git add an_old_file
要取消跟踪一个文件,但希望在文件系统上保留这个文件,使用git rm --cached file
暂存中
准确地说,暂存的不是文件,是操作。通常来说,可以暂存
- 修改 Modify/Update
- 添加 Add
- 删除 delete
- 移动 Move
这几种操作。
当你在文件系统上修改一个文件,然后用git add
的时候会暂存一个修改
操作到暂存区。
当你在文件系统上添加一个文件,然后用git add
的时候会暂存一个添加
操作到暂存区。
当你在文件系统上删除一个文件,然后用git add
的时候会暂存一个删除
操作到暂存区。
当你在文件系统上移动或重命名一个文件,然后用git add
的时候会暂存一个移动
操作到暂存区。
将操作从暂存区撤出,使用以下命令:
git restore --staged file
对于这些状态的理解,我推荐使用Visual studio code的内置Git功能进行观察,他是Git的一个前端。可以代替手动的命令行,但是,就我个人而言,更会倾向使用命令行,因为这些前端不支持较为复杂的操作。
现在我们再来看这段输出:
On branch master
Your branch is up to date with 'origin/use-git'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: src/use-git/configure.md
Untracked files:
(use "git add <file>..." to include in what will be committed)
src/use-git/try-git.md
Changes to be committed部分就是存在于暂存区的文件,当我们使用git commit
的时候,就会跟踪并把这些文件添加到git仓库。
Untracked files是前文说过的未跟踪文件,这是因为这个文件才创建,当使用git add
来添加时,会在暂存区添加一个添加
操作。
代码提交
代码提交使用git commit
命令,提交的时候需要一段简短的话描述你的这次修改。同时,你的姓名,邮箱也会被记录在这条commit记录中。
关于如何编写commit信息,我会在后面讲解良好的git使用习惯时讲到。
git commit
命令常用的形式有以下两种:
- 将信息作为参数直接提交
git commit -m "<提交信息>"
- 使用代码编辑器编写提交信息
git commit -a
这会打开你在安装时选择的代码编辑器,信息编写完成后,保存关闭即可。
有时候,你觉得提交信息写的不好,打算重写编写提交信息时,可以采用以下命令:
git commit --amend -m "<提交信息>"
或者使用编辑器
git commit --amend -a
这会完全重新提交一次commit并获得不同的commit哈希3,以及时间戳。
使用git commit --amend
的操作等价于以下操作:
# 保留修改回到上一次提交
git reset --soft "HEAD^"
# 重新提交
git commit -m "<提交信息>"
我个人更喜欢这种方法,因为更为灵活,你可以在重新提交过程中做一些修改。
查看提交历史
使用git log
命令可以查看提交历史,这会列出所有的提交记录,包括提交哈希,提交信息,提交时间,提交者等信息。
推送到远程仓库
尽管我还没有教你配置远程仓库,但是我还是会告诉你如何推送到远程仓库。
使用git push
命令可以将本地仓库的修改推送到远程仓库。这个命令的形式是:
git push <远程仓库名> <本地分支>:<远程分支>
通常来说,远程仓库名是origin
。并且,大多数时候,本地分支和远程分支是一样的,所以可以省略远程分支:
git push origin <分支>
这会将本地分支推送到远程分支,如果远程分支不存在,会自动创建。
有时候,远程分支上包含了你不希望存在的修改,这时候你想用本地分支覆盖远程分支,可以使用-f
参数:
git push -f origin <分支>
-f
选项会强制推送,这会覆盖远程分支上的所有修改,所以请谨慎使用。如果你把别人的代码或者作业给覆盖了,小心被打。
在推送之前,你需要先确保你具有远程仓库的所有历史记录,如果本地仓库不包含远程仓库存在的一些更改,你就需要先拉取远程仓库的更改。
拉取远程仓库的修改
拉取远程仓库的修改使用git pull
命令,这会将远程仓库的修改拉取到本地仓库。这个命令的形式是:
git pull <远程仓库名> <远程分支>:<本地分支>
同样,大多数时候,远程仓库名是origin
,远程分支和本地分支是一样的,所以可以省略远程分支:
git pull origin <分支>
这会将远程分支拉取到本地分支,如果本地分支不存在,会自动创建。
git pull
命令实际上是git fetch
和git merge
的组合,git fetch
会将远程仓库的修改拉取到本地仓库,但不会应用更改,而是会在一个<远程名>/<分支名>
的分支上保存,git merge
会将这个特殊分支合并到本地分支。
分支
前面提到了那么多关于分支的内容,但是我还没有告诉你什么是分支。
分支是 Git 的一个重要概念,它是一个指向一个提交的指针。在一个 Git 仓库中,你可以有多个分支,每个分支指向一个提交。默认情况下,你会有一个master
1分支。
分支的意义在于它和其他分支可以并行开发,你可以在一个分支上开发一个新的功能,而不影响其他分支。当你开发完成后,你可以将这个分支合并到主分支上。合并会将当前分支上所有领先于基础分支的patch应用到基础分支上。这就是使用 Git 进行协作开发的原理。
了解了什么是分支,现在我来教你如何管理分支。
查看分支
使用git branch
命令可以查看当前本地仓库的所有分支。加上-a
参数可以查看所有分支,包括远程分支。
样例输出:
caiyi@archlinux ~/r/manual (use-git)> git branch
fix-ci
fix-format
main
master
* use-git
caiyi@archlinux ~/r/manual (use-git)> git branch -a
fix-ci
fix-format
main
master
* use-git
remotes/origin/HEAD -> origin/main
remotes/origin/fix-ci
remotes/origin/fix-format
remotes/origin/main
remotes/origin/master
remotes/origin/use-git
上面的输出表明,我们当前在use-git
分支上,本地仓库中有fix-ci
, fix-format
, main
, master
, use-git
五个分支,远程仓库中有fix-ci
, fix-format
, main
, master
, use-git
五个分支。
创建分支
创建分支是创建当前分支的拷贝,因此要先切换到你想要拷贝的分支上,然后使用命令。
创建分支有两种命令:
git checkout -b <分支名>
git branch -m <分支名>
这会创建一个新的分支,并切换到这个分支上。
要创建一个分支但不切换到这个分支上,可以使用以下命令:
git branch <分支名>
切换分支
当两个分支里的文件不一样时,切换分支会修改当前文件系统上的文件,因为切换分支意味着你要切换到另一个分支的文件系统状态。
切换分支使用git checkout
命令,这会将当前分支切换到指定分支上:
git checkout <分支名>
删除分支
删除分支使用git branch -d
命令,这会删除指定分支:
git branch -d <分支名>
如果分支上有未合并的修改,Git 会拒绝删除这个分支。如果你确定要删除这个分支,可以使用-D
参数:
git branch -D <分支名>
合并分支
合并分支有三种方法:
- 合并 merge
- 变基 rebase
- 快进 fast-forward
通常来说,使用merge
即可,但是在一些特殊情况下,你可能需要使用rebase
。快进是一种特殊的合并,当你的分支是基于基础分支的最新提交时,Git 会直接将基础分支指针指向当前分支指针,这就是快进。
合并分支使用git merge
命令,这会将指定分支合并到当前分支上:
git merge <另一个分支>
合并分支会产生一个新的提交commit,这个提交包含了两个分支的所有修改。
变基
变基是一个相对复杂的概念,相对于merge
来说,它能够让提交记录更加线性。这是因为,在合并后,分支继承了基础分支,也同时继承了合并分支,同时具有两个父节点。这会使得提交记录变得复杂,也就是更加不线性。
变基与合并不同的地方在于,它不会让分支同时继承两个父节点,而是将合并分支的提交记录放在基础分支的最新提交之后。提供更改的分支不会被继承,因而不会出现两个父节点。
变基使用git rebase
命令,这会将指定分支变基到当前分支上:
git rebase <另一个分支>
变基会产生一个新的提交commit,这个提交包含了两个分支的所有修改。
回退到某一个提交
有时候,你可能需要回退到某一个提交,这时候你可以使用git reset
命令。命令的格式为
git reset [option] <提交哈希>
其中option
有以下几种:
--soft
仅仅回退HEAD指针,不会修改暂存区和工作区--mixed
回退HEAD指针和暂存区,不会修改工作区--hard
回退HEAD指针,暂存区和工作区
通常情况下,建议显式指定选项,以避免错误使用命令。
提交哈希可以使用git log
查看。当你想从当前版本向前回退几个版本时,可以使用HEAD~n
,其中n
是你想回退的版本数。
例如:
# 回退到上一个版本
git reset --hard HEAD~
# 回退到上上一个版本
git reset --hard HEAD~2
常见误区
Git 命令行 一般情况下仅在本地管理仓库,不会实时修改远程仓库。当且仅当执行以下命令时会通过网络与远程仓库交互:
git fetch
和 git pull
执行这两个命令时,仅会读取远程仓库的信息,不会修改远程仓库的信息。git fetch
会将远程仓库的信息拉取到本地仓库,git pull
会将远程仓库的信息拉取到本地仓库并合并到当前分支。
git push
执行这个命令时,会将本地仓库的信息推送到远程仓库,这会修改远程仓库的信息。
总结
本节课我们学习了 Git 的基本操作,包括将一个文件夹变成 Git 仓库,克隆远程仓库,查看 Git 仓库的状态,提交代码,查看提交历史,推送到远程仓库,拉取远程仓库的修改,分支的创建,切换,删除,合并,变基等操作。
这些操作需要你多加练习,只有多加练习,你才能熟练掌握这些操作。
要练习这些操作,我推荐使用这个网站:Learn Git Branching。使用这个网站时,你只需要先掌握基本操作,以及操作远程仓库的操作,就可以了。对于更加复杂的操作,等你对 Git 有了更深的理解再去尝试。
但更重要的是,你应该多使用 Git 来管理你的代码,这样你才能更好地理解 Git 的工作原理。
下一节课开始,我们将学习如何使用代码托管平台,本教程以 GitHub 为例。