在实习当中经常用到Git,有碰到一些问题在此记录一下,具体Git入门教程可以参考廖雪峰写的教程,这里提及一下我觉得挺好用的两个命令git stashgit rebase。再说这两个命令之前,先来回顾一下Git的各个工作区域。

Git的工作区域和文件状态

Workspace: 工作区,就是你平时存放项目代码的地方

Index / Stage: 暂存区,用于临时存放你的改动,事实上它只是一个文件,保存即将提交到文件列表信息

Repository: 仓库区(或版本库),就是安全存放数据的位置,这里面有你提交到所有版本的数据。其中HEAD指向最新放入仓库的版本

Remote: 远程仓库,托管代码的服务器,可以简单的认为是你项目组中的一台电脑用于远程数据交换

Untracked: 未跟踪, 此文件在文件夹中, 但并没有加入到git库, 不参与版本控制. 通过git add 状态变为Staged.

Unmodify: 文件已经入库, 未修改, 即版本库中的文件快照内容与文件夹中完全一致. 这种类型的文件有两种去处, 如果它被修改, 而变为Modified. 如果使用git rm移出版本库, 则成为Untracked文件

Modified: 文件已修改, 仅仅是修改, 并没有进行其他的操作. 这个文件也有两个去处, 通过git add可进入暂存staged状态, 使用git checkout 则丢弃修改过, 返回到unmodify状态, 这个git checkout即从库中取出文件, 覆盖当前修改

Staged: 暂存状态. 执行git commit则将修改同步到库中, 这时库中的文件和本地文件又变为一致, 文件为Unmodify状态. 执行git reset HEAD filename取消暂存, 文件状态为Modified

以上内容copy自这篇文章。这里主要要清楚的是,workspace并不指的是你当前文件夹的所有文件,而是tracked的文件,如果一份新建的文件处于Untracked的状态,那么该文件是不参与版本控制的,后续的git stash操作也自然对他不会起作用。

Git Stash

假设你现在正在开发一套代码,你也已经有了好几个commit或者是有了好几个版本,这个时候你的上司突然叫你切回原来的稳定版本给别人看一下稳定版本的效果。当然你可以直接git commit当前的工作区,提交一个commit,但是你当前的工作区并不是一个阶段性的小成果,你也不想提交这个从而产生一个没有太多参考价值的trash commit,那么这时git stash就是给你的良药。他的作用是将你当前的工作区(workspace)和暂存区(Index)打包存好

现在我们开始实验,首先新建个文件夹learnGit,然后用以下指令初始化

1
git init

接着我们创建我们的第一个文件夹test1.txt,并往里面写入一些东西

1
2
touch test1.txt
echo "This is test1.txt" >> test1.txt

我们先提交这个作为第一个commit

1
2
git add .
git commit -m "first commit"

我们按照相同方式再次提交第二个commit

1
2
3
echo "Line 2 for test1.txt" >> test1.txt
git add .
git commit -m "second commit"

这时候你接着工作,给test1.txt增加了第三行,并且将更改放到了暂存区;给test1.txt增加了第四行,还没有放到暂存区;新建了一个新文件叫test2.txt,并且给它写了第一行,还没有track这个文件。以上活动为以下指令

1
2
3
4
5
6
7
8
9
10
#给test1.txt增加第三行,并放到暂存区
echo "Line 3 for test1.txt" >> test1.txt
git add .

#给test1.txt增加第四行,还没有放到暂存区
echo "Line 4 for test1.txt" >> test1.txt

#新建test2.txt,并写入第一行
touch test2.txt
echo "This is test2.txt" >> test2.txt

这个时候使用git status查看一下当前git状态

1
2
3
4
5
6
7
8
9
10
11
12
13
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: test1.txt

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: test1.txt

Untracked files:
(use "git add <file>..." to include in what will be committed)
test2.txt

上面三个操作,分别对应着三种文件状态,第一种是已经被放到了暂存区,但还未提交;第二种是更改了还没有放进暂存区,但是文件本身是被track的;第三种就是文件都还未加入到git管理。这三种也典型地代表了几种平时会遇到的几种文件状态。这时候你的上司让你回到第一个commit,想看看那个稳定版本的效果,但是你不想提交当前的,因为什么都还没改完,我们就可以用git stash储存当前的更改

1
git stash save "first stash"

输出为

1
Saved working directory and index state On master: first stash

其实看到这行的时候,你就应该意识到问题就出现了,你的test2.txt并没有被track,所以理论上它是没有被存进去的。使用git status查看一下当前的状态

1
2
3
4
5
6
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
test2.txt

nothing added to commit but untracked files present (use "git add" to track)

确实如你所担心的那样,当前的状态并不干净,test2.txt还在这里,而对test1.txt的两个更改已经消失了(被存进去了),你还是不能这时候回到以前的commit。所以我们先把刚刚储存的弹出,tracktest2.txt文件,再存进去。先用git stash list看看存了哪些

1
stash@{0}: On master: first stash

如果你之前没有存其他的话,前一个的存储标号应为0,我们用git stash pop stash@{NUM}将其弹出,NUM填写你要弹出的stash

1
git stash pop stash@{0}

现在的工作区就是你存之前的状态了

stash的使用方法,除了用git stash pop之外还可以用git stash apply,使用方法类似,正如名字所描述的那样,pop直接会将你的stash弹出,而apply则使用当前stash之后还会保留存的stash。

这时候我们用git status查看一下当前的状态

1
2
3
4
5
6
7
8
9
10
11
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: test1.txt

Untracked files:
(use "git add <file>..." to include in what will be committed)
test2.txt

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

可以看到不一样的在于,之前放到暂存区的test1.txt的第三行,现在也在工作区(workspace)了。

接下来,我们还来制作stash,将所有的更改放到暂存区,然后再存储stash

1
2
git add .
git stash save "first stash"

git status查看一下当前状态

1
2
On branch master
nothing to commit, working tree clean

搞定,就像你什么更改都没做一样,回到了之前的状态,这时候你就可以开心地checkout到其他的提交或者分支了

Git Rebase

在实习的过程中,碰到了一个很尴尬的问题,我用的是前同事的电脑,在提交修改的时候还没把本机的作者名和邮箱改过来,用的还是前同事的,但是他已经离职了,也就导致那一份提交push不到远程仓库里。这时候就要用git rebase修改以前的提交了。

还是用前面的Git工作区,用git log查看提交过的commit

1
2
3
4
5
6
7
8
9
10
11
commit fff69d75f45e1e1689c19d0c12bda7c93584a92f (HEAD -> master)
Author: Ziyun Ge <ziyunge1999@gmail.com>
Date: Sat Oct 3 18:04:35 2020 +0800

second commit

commit 91496e19a37101cc5ff7e3920c4add1bd926e57b
Author: Ziyun Ge <ziyunge1999@gmail.com>
Date: Sat Oct 3 18:02:09 2020 +0800

first commit

假设我们要更改第二个提交,也就是second commit这一次,将它的作者改为geziyun,邮箱改为ziyun_ge@foxmail.com

先使用git rebase -i <COMMIT_ID>,这里的<COMMIT_ID>需要选第一次提交的,它显示的其实是<COMMIT_ID>之后的提交。可以看到以下交互窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pick fff69d7 second commit

# Rebase 91496e1..fff69d7 onto 91496e1 (1 command)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
...

可以看到第二次提交的的Commit ID,我们需要将第一行的pick改为edit,修改方式就跟vim用法一致,接着保存退出。

接着来更改作者和邮箱

1
git commit --amend --author="geziyun <ziyun_ge@foxmail.com>"

这里我们先暂停一下,看一下这一步做了什么操作。git commit肯定是提交了一个commit,但是是哪里提交呢,可以用git log查看一下。

1
2
3
4
5
6
7
8
9
10
11
commit 90b922390a3fb62a9bc8e5af54e8a7a83f9a08a4 (HEAD)
Author: geziyun <ziyun_ge@foxmail.com>
Date: Sat Oct 3 18:04:35 2020 +0800

second commit

commit 91496e19a37101cc5ff7e3920c4add1bd926e57b
Author: Ziyun Ge <ziyunge1999@gmail.com>
Date: Sat Oct 3 18:02:09 2020 +0800

first commit

可以有趣地发现,第一次提交还是一样的,但是第二次提交的作者和邮箱都变了,最重要的是第二次提交的Commit ID也变了,也就是说这并不是真正意义上的“第二次提交”。为了更好的阐述这个问题,如果你使用的是VSCode的话,可以下载安装一个插件Git Graph。

可以看到我们当前的这次commit其实是在first commit上的另一个分支,原来的second commit在另一个分支(还可以看到之前我们保存的stash)

接着我们使用以下指令

1
git rebase --continue

可以看到master指针已经指向了你新的提交,注意这里还可以看到老的second commit,这是因为之前的stash是在老的second commit上修改的。你可以使用git stash pop弹出之前储存的stash(没有指定哪个stash的话就默认会弹出最近的一个stash),由于你只改了作者和邮箱,所以你还是不用解冲突。然后你可以惊喜地发现老的second commit不见了,是因为它没有在哪个分支,所以就不显示出来了。如下图,这样我们的作者和邮箱就顺利修改完毕了,可以愉快地push到远程仓库了。

根据上述的描述,其实你大致也可以猜出,git rebase实际上新建一个暂时的分支,然后再那上面修改一些东西(不仅仅可以修改commit的作者,你还可以修改代码细节),最后再和之前的master分支进行合并,当然如果必要的话你还需要解冲突使得合并顺利。