WXL's blog

Talk is cheap, show me your work.

0%

Git版本控制与常见操作

为什么我们要用git来提交代码?

我们平常在自己电脑上写一个简单的实验题目的时候,比如我实现了可视化功能,代码以及没有bug了,接下来想要同步数据库,但是又害怕接下来的代码会误改之前完整的代码,于是我就将当前没有Bug的代码压缩打包,命名为v1.0,然后就可以肆无忌惮的编写后面的代码了,等到后面的某一个功能实现之后,我重新将新版的代码压缩打包,命名为v1.1,继续后面的工作,如果后面某一个环节没做好导致所有代码都崩了,想回到v1.0,直接解压v1.0压缩包重新开始编写即可。

相信大家遇到一个复杂的项目的时候会有上述的操作,但是操作比较繁杂,特别是多个人来合作一个项目开发的时候,大家的代码共享、修改都会遇到很多的问题。使用git可以很好的解决上述问题,大家可以在自己的电脑上写好代码,然后上传到github 上,也可以将别人写的代码从github上下载过来,这个功能解决了开发人员之间的代码交互问题。

GitHub支持多个版本分支,如下图:

版本控制意图

上图中每一个节点表示一个版本号的代码,假设左边分支名字为dev,表示我们进行开发的分支,也就是可能有bug,正在维护的版本分支;右边分支假设是main 分支,表示我们发布的版本分支,比如我们最终可以拿出来发布的版本。

main分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到main上,在main分支发布1.0版本.团队合作的分支看起来就像这样:

复杂图

我首先在我的账号上创建了一个仓库BigData,然后拉其他人过来就可以协同完成这个项目,首先将项目从远程拉取过来:

1
git clone git@github.com:wangxl12/BigDataProject.git

注意要把SSH Key添加到GitHub,如果提示电脑没有安装ssh,就需要安装一下.

查看目前有哪些分支:

1
git branch

默认只有main分支:

1
2
$ git branch
* main

注意,要在dev分支上开发,就必须创建远程origin(远程主机名)的dev分支到本地,使用如下命令创建本地dev分支:

1
git switch -c dev origin/dev

然后就可以在dev分支上修改,并时不时将dev分支push到远程:

1
2
3
git add .
git commit -m "changed something"
git push origin dev

我一开始创建仓库的时候只有main分支,这个分支作为稳定版本发布的分支,尽量少用,而dev分支我们作为开发分支,可以上传代码到这里。可能对于本地仓库、远程仓库的概念有不理解的地方,可以参看下面这张图:

仓库图

workspace就是我们的本地工作区,即BigData文件夹,index是暂存区,也在本地,repository是本地仓库,就是BigData目录下的.git目录(这是一个隐藏目录,如果看不见的话可能隐藏了,可以设置文件夹中的隐藏项为可见就可以看到了),而Remote就是远程仓库,即Github或者Gitee。有向线段之间的单词表示操作,经过这些操作之后文件会被传到不同的仓库。

廖雪峰教程

工作区(BigData)目录下有一个隐藏目录.git,这个是Git的版本库,版本库里有很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD.

本地仓库

往Git版本库里添加的时候分为两步:

第一步是用git add将文件添加进去,实际上就是将文件中的修改添加到暂存区;

第二步是用git commit提交修改,实际上是将暂存区中的所有内容提交到当前分支;

因为我们创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以,现在,git commit就是往master分支上提交更改。可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改。

比如现在创建了一个test.txt文件:

1
touch test.txt

先加到暂存区:

1
git add test.txt

然后提交到当前分支:

1
git commit -m "我创建了一个test.txt文件"

注意,git commit只会将暂存区里的修改提交到当前的分支,如果有修改没有提交到暂存区,那么commit无法将这个修改提交到当前分支。此时,使用git status查看当前的状态的时候,可以看见类似下面的内容:

nostaged

这就说明有一个read.txt文件在暂存区内,该文件有修改没有提交到版本库。可以使用如下指令查看工作区内的这个文件和版本库(本地)之间的区别

1
git diff HEAD -- readme.txt

diff

白色是工作区和版本库之间相同的部分,红色是工作区删掉的,绿色则是工作区添加的内容。

丢弃修改

  • 假设此时修改了工作区的内容,但是还没有提交到暂存区,可以通过如下的方式丢弃修改:

丢弃工作区的修改:

1
git restore readme.txt  # 不用背,使用git status查看状态的时候有提示

修改之后查看工作区和版本库之间的区别:

nodiff

发现回到了修改工作区之前的状态。就是让这个文件回到最近一次git commitgit add时的状态。git checkout -- file命令中的--很重要,没有--,就变成了“切换到另一个分支”的命令。

  • 假设此时修改了工作区的内容,还通过add添加到了暂存区,可以通过如下的方式丢弃修改:

首先输入git restore --staged <file>将暂存区中的文件恢复到提交到暂存区之前,即在工作区修改了没有进行add操作。

然后使用git restore <file>将工作区的文件修改抛弃,即恢复到没有修改的状态。期间可以使用git status查看文件的状态,如果是绿色字体的modified,表示提交到了暂存区,没有commit,如果是红色的modified,表示修改了没有add。

  • 如果不但add交到了暂存区,还commit到了版本库,可以使用版本回退来回到上一个版本,不过这个要求你还没有将本地的版本库推送到远程版本库。

然后将本地仓库的内容上传到远程仓库:

1
git push

查看历史记录:

1
2
3
git log  # 确定要回到之前的哪个版本
# 或者
git log --pretty=oneline

版本回退:

1
2
3
git reset --hard HEAD^  # 回退一个版本
git reset --hard HEAD^^^ # 回退3个版本
git reset --hard HEAD~100 # 回退100个版本

版本前进:

1
git reset --hard 版本号  # 版本号可以通过git log查看,不需要完全输入,可以通过输入4~5个字符自动找到

查看记录的每一次命令:

1
git reflog  # 确定要回到未来的哪个版本

删除文件

将readme.txt删除:

1
rm readme.txt

然后查看状态:git status

deletestatus

根据提示,可以使用git restore <file>来恢复工作区的修改:

1
git restore readme.txt

重新查看状态:

1
git status

newstatus

但是如果真的想删掉呢?我们创建一个test.txt文件试一下

1
2
3
4
5
touch test.txt
git add .
git commit -m "commit test.txt"
rm test.txt
git status

result1

接下来将暂存区中的文件删掉,然后commit即可:

delete

commit

创建远程仓库

如果一开始只是在本地仓库中操作的,可以将本地仓库关联远程 仓库,然后将本地仓库的内容推送到GitHub仓库,在本地仓库下执行如下指令即可关联:

1
git remote add origin git@github.com:wangxl12/learngit.git

这里的origin是远程仓库,这是Git默认的叫法,也可以改成别的,但是origin这个名字一看就知道是远程库。

下一步可以将本地库中的所有内容推送到远程库上:

1
git push -u origin master

把本地库的内容推送到远程,用git push命令,实际上是把当前分支master推送到远程。

由于远程库是空的,我们第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。

从现在起,只要本地作了提交,就可以通过命令:

1
git push origin master

把本地master分支的最新修改推送至GitHub,现在,你就拥有了真正的分布式版本库!

删除远程库

如果添加的时候地址写错了,或者就是想删除远程库,可以用git remote rm <name>命令。使用前,建议先用git remote -v查看远程库信息:

1
2
3
$ git remote -v
origin git@github.com:wangxl12/BigDataProject.git (fetch)
origin git@github.com:wangxl12/BigDataProject.git (push)

然后,根据名字删除,比如删除origin

1
git remote rm origin

此处的“删除”其实是解除了本地和远程的绑定关系,并不是物理上删除了远程库。远程库本身并没有任何改动。要真正删除远程库,需要登录到GitHub,在后台页面找到删除按钮再删除。

从远程库克隆

上面将的是先创建本地库,然后关联远程库,现在是先创建远程库,然后从远程库克隆。

如果有多个人协作开发,那么每个人各自从远程克隆一份就可以了。

1
git clone git@github.com:michaelliao/gitskills.git

分支管理

创建与合并分支

强烈建议先学廖雪峰的Git教程:https://www.liaoxuefeng.com/wiki/896043488029600/900003767775424

创建分支并切换分支:

1
2
3
4
git checkout -b dev
# 相当于下面的两句:
git branch dev
git checkout dev

如果希望将分支推送到远程库可以使用如下指令:

1
git push origin dev

使用git branch查看分支:

1
git branch

然后我们可以在dev分支上正常提交,比如对readme.txt添加了一行内容:

添加了一行

绿色部分就是添加的内容,然后提交:

1
2
git add readme.txt
git commit -m "branch dev test"

通过git status也可以查看状态信息。

然后dev分支的工作完成之后,可以切回到main分支并查看添加的内容:

1
2
git checkout main
cat readme.txt

发现刚刚添加的内容不见了:

修改消失

因为刚刚提交的是在dev 分支上,而main 分支此时的提交点并没有发生改变:

状态1

现在,我们将dev分支的工作成果合并到main上:

1
git merge dev

合并

再查看readme.txt内容发现和dev分支的内容一致了:

结果2

注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。

当然,也不是每次合并都能Fast-forward,我们后面会讲其他方式的合并。

合并完成后,就可以放心地删除dev分支了:

1
2
3
git branch -d dev
# 查看分支:
git branch

因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master分支上工作效果是一样的,但过程更安全。

切换分支还可以使用switch:

1
2
3
4
# 创建并切换分支
git switch -c dev
# 切换到已有分支
git switch dev

查看分支:git branch

创建分支:git branch <name>

切换分支:git checkout <name>或者git switch <name>

创建+切换分支:git checkout -b <name>或者git switch -c <name>

合并某分支到当前分支(快速合并):git merge <name>

删除分支:git branch -d <name>

解决冲突

现在只有main分支,创建一个文件test,add、commit,然后创建一个新的分支feature1,add到暂存区,切换到这个分支下commit到本地仓库中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
touch test
git add .
# 先将test创建的这一个修改提交到本地仓库
git commit -m "create test file"
# 创建并切换到feature1分支
git switch -c feature1
# *********向test文件中添加一行内容,比如我添加branch main**************
# vi test
# i
# branch main
# :wq!
# *****************************************************************
# .表示将所有修改提交到暂存区
git add .
# 暂存区和工作区都是所有分支共享的,即修改和add操作在哪个分支下进行都可
# 但是commit需要切换到想要提交的分支,这里我想提交到main分支
git switch main
git commit -m "add a main branch line"
# 至此,main分支已经比feature1分支多一个版本节点了,下面继续让二者程并列的状态
git switch feature1
# *********向test文件中添加一行内容,比如我添加的是branch feature1*******
# vi test # 打开会发现test是空的,因为之前的修改已经提交到main分支上了
# i
# line2 branch dev
# :wq!
# *****************************************************************
git add .
git commit -m "add a feature1 branch line"
git switch main

至此,分支的状态到达了下图所示:

并行状态

这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:

1
2
3
4
$ git merge feature1
Auto-merging test
CONFLICT (content): Merge conflict in test
Automatic merge failed; fix conflicts and then commit the result.

因为我们对一个文件进行了两种修改,这两种修改谁覆盖谁都说不通,所以会发生冲突。我们查看冲突的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git status
On branch main
Your branch is ahead of 'origin/main' by 11 commits.
(use "git push" to publish your local commits)

You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)

Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: test

no changes added to commit (use "git add" and/or "git commit -a")

我们直接查看test的内容:

1
2
3
4
5
6
$ cat test
<<<<<<< HEAD
branch main
=======
line2 branch dev
>>>>>>> feature1

我们直接手动修改test文件为如下内容,从而解决冲突:

solveconflict

然后重新提交:

1
2
git add test
git commit -m "conflict fixed"
1
2
$ git commit -m "conflict fixed"
[main d7c72cc] conflict fixed

现在main和feature1分支变成了下图所示:

状态2

用带参数的git log也可以看到分支的合并情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git log --graph --pretty=oneline --abbrev-commit
* d7c72cc (HEAD -> main) conflict fixed
|\
| * 7fb2bc5 (feature1) add a branch feature1 line
* | f68248d add a main branch line
|/
* 69e37a8 Merge branch 'dev' into main
|\
| * d441a47 test
* | 4e52e8e test-main
|/
* 9bea0ff branch dev test
* 53a40b8 delete test.txt
* a66d7e5 commit test.txt
* 4b238b8 test
* b5353dc two lines changed
* 80c715c add line 2
* 87cf69b create readme.txt
* ed46945 (origin/main) delete README.md
* f650647 (origin/origin/dev, origin/dev) wxl commit readme.md

合并完成之后,我们就可以删除feature1分支了:

1
git branch -d feature1

当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。

解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。

git log --graph命令可以看到分支合并图。

上面举的例子是对同一个文件进行修改,所以无法自动合并,如果是两个分支同时创建一个文件的话,还是可以使用merge合并的,下面的内容就是举的这个例子:

然后再新建一个文件,add,切换到main分支下commit:

1
2
3
4
touch test-main
git add .
git switch main
git commit -m "test-main"

注意这里的创建文件、add在哪个分支下进行都可以,因为工作区和暂存区是所有分支共享的,但是commit需要在想提交的那个分支下进行,因为commit的本质就是同步工作区里的相应的分支。

现在可以切换到dev和main分支中查看,分别有一个test文件和test-main文件。现在各自的状态如下(将feature1分支改为dev,master分支改为main):

状态3

这种情况下如果进行快速合并的话:

1
2
git switch main
git merge dev

会调到一个类似文本文件的界面,输入:q退出。出来之后发现main分支有test文件了:

test文件又有了

分支管理策略

通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。

如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。

合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并。

切换到dev分支,修改一下test里的内容,然后add, commit提交:

1
2
3
4
git switch -c dev
vi test
git add .
git commit -m "add merge"

然后切换回main:

1
git switch main

准备合并dev分支,请注意–no-ff参数,表示禁用Fast forward:

1
2
git switch main
git merge --no-ff -m "merge with no-ff" dev
1
2
3
4
$ git merge --no-ff -m "merge with no-ff" dev  # 因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。
Merge made by the 'recursive' strategy.
test | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)

合并后,我们用git log看看分支历史:

1
2
3
4
5
6
7
$ git log --graph --pretty=oneline --abbrev-commit
* fecae9f (HEAD -> main) merge with no-ff
|\
| * 39ba997 (dev) add merge
|/
* d7c72cc (origin/main) conflict fixed

可以看到,不使用Fast forward模式,merge后就像这样:

状态4

行行好,赏一杯咖啡吧~