前言

最近因为软件工程这门课,要我使用git来记录我所做的工作,啊这……
我之前是有学过那么一下,但是,很快就忘了,因为没有用过,所以现在就再学一遍。

不过,我也只是刚刚开始学习,本文尝试模拟种种场景来记录使用git,水平不高,难免会有错漏,若有不妥希望大家评论评论指正一下~

文章会一直更新,因为我发现git真的是太强大了!我会一直使用它,并且完善这遍文章!

本文章借鉴了廖雪峰的Git『Git』知道这些就够了这两个教程,以实用为主,不作深究……

——总算是写完了,啊啊啊啊啊太难写了!2020年11月13日 21:35:16

介绍

前些天,毛概课要求写个调研计划书,我作为小组长,布置完任务之后,再让组员们发上群,收集起来再改改,再收集,结果……

image.png

啊这,我反正已经分不清哪个文件里面有什么内容,几次版本间又作了什么修改了。


所以怎么处理这种情况呢?

这个时候,Git隆重登场!它是全世界最先进的分布式版本控制系统

什么叫做版本控制?
以上图的毛概计划书为例,光选题背景就已经修改了四次,我怎么知道这四次都做了什么修改?使用git,就可以清晰的记录每次修改了什么地方、是谁修改的等信息。若对某个版本不满意,还可以退回到某个版本进行修改。
而分布式又是什么意思?
对于多人协作的项目,每个人手上都有一份版本库,记录着当前版本和历史版本的信息,每个人都可以在自己的电脑上进行新版本的工作,互不干扰。万一发生某些意外丢失了版本库,那也没关系,因为每个人都有一份完整的。另外,还可以把项目放到一个统一的服务器上面,方便备份,那就是github仓库。


还有一种叫做集中式版本控制系统,它使用"中央服务器"来保存所有文件的修订版本,各成员则可以取出这最新的文件或者提交文件。它最大的缺点就是要联网才能工作,而且万一中央服务器出现故障,那么每个人都不能进行工作。

在本地工作

一次提交

假设,我现在要开始做一个项目了!
项目的路径就选择在E:\learning\project

接下来,我们要在这个文件夹里创建git仓库

输入git init

image.png

创建成功!

然后我用Java写了一个hello world并编译运行,此时工作区中都有什么呢?

image.png

有git相关的隐藏文件.git,还有两个Hello文件

我们把它加入到版本库中的暂存区

输入git add Hello.class Hello.java
把这两个文件都加入到暂存区中,命令格式为git add <filename>

若想偷个懒,也可以直接git add .,它表示把所有文件都加进去。

image.png

然后提交

git commit -m "hello world!

它的意思是,把文件提交到仓库,并备注提交信息:hello world!

image.png

可以看看里面有什么信息,比如 2 files changed,两个文件有改变;5 insertions(+),插入了五行,下面也展示了创建文件的名字,等等信息,多看看就知道了。


相关概念

还是要了解一下工作区和版本库相关的东西,如图所示。

image.png

按照我现在的理解,工作区就是我们电脑上面某个文件夹,是我们能看到的;然后版本库就是git帮我们管理的一块区域,里面有一个叫做stage暂存区,每次add的时候就是把文件放进去暂存区,然后提交到对应分支上,比如这里的默认的分支master。

撤回及查看相关

我在文件里面写了一个功能A。

public class Hello{
    public static void main(String[] args) {
        System.out.println("hello world!");
        System.out.println("function A");
    }
}

然后我们可以通过这条命令来查看一下当前的状态。

git status

它会显示:
image.png

看看英文,特别是红字那里,它说Hello.java发生了改变,第二行还说我们变更未提交,所以下面我们提交一下。

git add .

这里我们再看一下状态如何:
image.png

它绿了!看到没有,变更将要提交,改变的文件是Hello.java,这里我们就可以去commit了。

git commit -m "function A"

依照上面的方法,然后我再写功能B,提交。

public class Hello{
    public static void main(String[] args) {
        System.out.println("hello world!");
        System.out.println("function A");
	    System.out.println("function B");
    }
}

提交了这么多次,有没有什么方法可以查看一下提交记录呢?有!
这条命令专门用于查看提交记录。
git log

image.png

从上往下看,上面的记录是最新的。
里面清晰地写道时间、人物、说明(就是commit -m后面的信息)和commitID,那一串长长的东西,可以认为它是这次commit的身份证,非常重要。不过看着太长了,可以只取前七位,也能确定一个commit了。

PS:用这句似乎更好看

git log --graph --pretty=oneline --abbrev-commit

然后,我觉得功能B写得不好,想回到只有功能A的版本,可以这样:

git reset <commitID>

看看上面的截图(每个人的不一样),知道提交功能A那次的commitID是6c04da7,输入即可

image.png

最后查看一下文件变成啥样的

public class Hello{
    public static void main(String[] args) {
        System.out.println("hello world!");
        System.out.println("function A");
    }
}

回到了写功能A的时候,针不戳。

但,我突然又想改变主意了, 又想回到功能B的时候,怎么办呢?

再像上面看看日志?

git log

image.png

发现功能B那次提交没有了,我们换另一条命令,可以查看所有的提交记录,包括被删除的。

git reflog

image.png
(注:我这里撤回了两次,是我按错了,reset的记录应该只有一条。)


关于后悔药reset

补充:reset还可以将暂存区(staged)中的文件撤回到工作区(working area),commit的时候就不会提交这个文件。

git reset <filename>

话说,重置到某个commit时的状态,它有三种模式。

  1. --mixed
  2. --hard
  3. --soft

假设我们现在在3.0版本上进行工作,计划回退到2.0版本。

第一种情况--mixed默认的情况,即回退至2.0版本。但是它会把原版本3.2和新版本2.0之间的差异放到工作区,还清空了暂存区,把里面的内容也放到工作区中,mixed在一起

第二种情况--hard,不会保留任意变更,直接回到该commitID时刻的状态!你的工作区暂存区*将会变到回退版本的时候,简单说,就是啥也没有了

第三种情况--soft,它会保留工作区暂存区的内容,回退后,会把当前版本3.0的内容(即与回退版本2.0之间有差异的文件)放到暂存区中,随时可以commit。

现在对它们的理解更深了一点,但是还得结合实际去运用。我查了查,发现它们各自应用的情形不一样。

hard可以用git reset --hard HEAD来放弃本地的所有修改(可能改烦了or写错了?),还能抛弃某版本之后的所有commit,确实非常hard。

soft主要是合并结点,我之前想的问题终于算是有种解决方案了。要是我经常commit,比如我写几行就提交一次,那么一定会有很多次提交记录。为了让记录好看一点(不太冗余),可以通过soft来合并一些无意义的commit,把差异放到staged,那就再commit一下就好。

mixed的功能也很丰富,毕竟是默认的选项,一是像soft一样,回退某个版本,但是是回到工作区,而soft是回到暂存区,都差不多,只是多一次add而已。二是把暂存区的文件回退到工作区,不想提交commit了,就用git reset HEAD。三似乎是修改错误commit,回退到该版本,然后在工作区修改,修改完后再提交commit。

分支

首先要知道,我们创建完git仓库后,有一个默认分支就是master
查看分支
git branch

image.png

分支功能很强大,git鼓励多使用分支
那么问题来了,怎么创建分支呢?

git checkout -b <name> <template>

这个命令就是,以某分支为模版,创建并切换到分支上。
当最后一个参数为空则默认为当前分支。

什么叫做以xx分支为模版呢?

比如以

image.png

就是bugFix分支相当于是master的分身,处处和master相同,但又不是master。对bugFix进行处理,不会对master有任何影响。

虽然作业中会产生多个分支,但是最后还是要合并的,所以最后看一个合并分支的操作。

创建完bugFix分支后,在bugFix分支上对Hello.java进行了修改。

public class Hello{
    public static void main(String[] args) {
        System.out.println("hello world!");
        System.out.println("func A");
        System.out.println("fix a bug")
    }
}

同时,master分支上面也做了一些改动。

public class Hello{
    public static void main(String[] args) {
        System.out.println("hello world!");
        System.out.println("function A");
        System.out.println("some changes")
    }
}

最后若要合并,则使用这条命令:
git merge <branchName>
将branchName分支合并到当前分支上。

现在,将bugFix合并到master分支上。
但想想,能有这么顺利吗?如果master没有变更,那直接将bugFix的变化并到master那里就行了,但是master也发生了变动。
所以在这些变动之间发生了冲突,要解决冲突,还是看看这样合并之后会怎样吧。

我用的VSCODE来编辑的,发生冲突是怎样的,要怎么处理都会提示,非常智能。

image.png

image.png

当我们手动解决冲突之后,再重新提交,即可合并成功(别忘了把分支删除噢)。


疑问:
其实我发现,它发生冲突之后,我居然要一个一个地手动解决冲突,这个可编辑的java文件还好,我用IDEA的时候还有一些奇奇怪怪的文件发生了变化(删除之后它会自动再编译出来,影响不大),但就很……暂时不知道怎么处理这个。希望到时找一些智能一点的图形化工具,提高效率。

临时存储小功能

还是想补充一下这个命令,感觉非常实用。
比如说,某天在开发,突然有一个紧急的bug要修复,但你手头上的工作还没完成。

此时可以使用

git stash

保存当前的工作现场,现在工作区、暂存区都“干净”了,可以用git status查看。

但是保存的东西在哪呢?可以用

git stash list

来看看存的东西都到哪了。

要恢复的话,有两种方式。
一是直接 git stash pop 像栈一样把它弹出来,此时再查查,git stash list发现里面的东西是空的。
二是用 git stash apply 恢复出来,但是东西还是存着,没有消失。

远程仓库

介绍几条命令就好,因为大部分操作都是在本地完成,远程仓库只是简单的push, pull。

添加远程仓库
(也有起别名一说,即origin就是代表这个链接,以后就不用写这么长)
git remote add origin <githubLink>

上传自己的工作
git push

但若是第一次上传(换句话说,本地分支是新建的分支)那么就要设置一个上游分支。你不说它也会提醒你的

git push --set-upstream origin <name>

然后就能push了。

若是取东西,则是
git pull

其实,这是一种简便的方式,它等于git fetch + merge,若有区别,它会自动合并到最新版本。


疑问:

为什么每次git pull都是直接。。。拉下来,不用合并,它这自动合并也太,我想自己确定我想要的,但就是不行,每次都自动搞定了,啊这真是不懂

看了看别人说的,没有冲突就会自动合并,但我明明手动制造了冲突,它却自动把所有都给我合在一起了

变♂基 rebase

让你的提交记录变得简洁。

一、把多个记录整合成一个记录

完成一个任务可能需要很长时间,并且那些提交记录可能。。。对自己来说很有用,但对别人来说可能是没意义的。
所以我们要把“无意义”的记录给处理掉,让它变简洁。

git rebase -i HEAD~n

这表示把当前版本到前n个版本之间的版本进行合并。

也可以写成到某个版本的commitID,这样是指对当前版本到该commitID的记录之间进行处理。

注意:尽量合并没有提交到版本库的版本。万一它push了,那就会变得很麻烦,因为本地和远程的版本不一样,到时合并了会更乱。。。

下面来实际演练一下。

我提交了四次记录,每次都新建了一个文件,1234各自对应各个提交记录。

image.png

然后,我想把2~4都合并成一个记录,使用命令
(包括自己的本版本的三个记录作变基处理)

image.png
然后,我们按i,对它作这样的修改

image.png

s代表的是,把当前版本合并到上一版本,这样就是把4合并到3,然后再把3合并到2

里面也有很多参数,以后再细细推敲。

然后我们输入:wq进行保存。

它又跳转到另一页面,如图

image.png

这里的意思是,让你确定合并后,显示的信息(就是commit -m ""里面的信息),自己修改一下就好了,我把它改成"2 & 3 & 4",然后保存。

image.png

果然,提交记录变整洁了。

写到这里,我突然又想起,万一它有冲突怎么处理?先留着一个小疑问,以后实践碰到了再补充。

二、解决分支的情况

刚刚一中的情况是单分支上的简化,但是若是有多个分支的开发会怎样呢?

下面设定一个背景,接着上面的情况继续开发。
创建新分支dev,在该分支上我们提交一个dev branch,然后回到master分支再创建一个master分支,提交一个master branch。
完成工作后,我们再把dev合并回master分支

image.png

结果如图:

image.png

我们就可以发现,因为分支而导致这种提交有点乱,图上面那里又岔出来。
上面的合并方法是以前传统的做法,下面来展示一下用rebase怎么合并。

image.png

同样的,在dev分支上再提交一个dev1 commit,在master分支上提交一个master1 commit。

先在dev分支上采用变基操作
git rebase master

(大致如下)
image.png

image.png

然后回到master分支上,把它们给merge回来。

image.png

这样操作可以使提交记录变简洁,但是也缺失了具体的记录,这要看各自的取舍了。

三、远程拉取的情况

有这么种情况:拉取远程仓库的内容,并且它与本地的仓库有冲突。一般都是使用pull直接拉取,但是这样做,解决冲突之后会有一条分叉,不好,所以要考虑一下如何使用变基。

(这是一次有分叉的合并,看看里面的内容就知道大概了)
image.png

git fetch origin master
git rebase origin/dev

注:git fetch所取回的更新,在本地主机上要用”远程主机名/分支名”的形式读取。比如origin主机的master分支,就可以用origin/master读取。

rebase冲突处理

它会提醒你解决冲突,按照提示就行。
但是解决完之后呢,要输入这个

git rebase --continue

继续变基。

总的来说,跟着提示走就没有错,嘿嘿。