Git:git版本控制知识整理和使用说明

关键词:Git

所有内容来自于《Git从入门到精通》此书的内容提要,作为学习笔记

Git概述

Git是一种分布式版本控制系统,记录项目代码不同进度或版本的内容,可以方便地查看不同版本的内容改动,Git是共享的,可以查看每个文件是什么时候加入进来的,什么时候被修改或者删除。
Git的优点

  • 免费开源
  • 速度快,文件体积小:不同版本的内容不需要复制备份,Git记录文件内容的快照,可以非常快速地切换版本
  • 分布式系统:SVN等集中式控制系统需要有中央服务器,存在单点故障的问题,且必须联网才能操作,Git是没有中央服务器,开发者有本地仓库,在没有网络的情况下也可以进行版控

Git和Github的区别:Git是版本控制软件,Github是商业网站,其本体是一个Git服务器


Linux下安装Git

使用如下命令

$ sudo apt-get install git

查看git的路径和版本

$ which git
/usr/bin/git
$ git version
git version 2.25.1

设置Git

首先要设置Git的用户名和邮箱,使用git config在终端输入以下命令

$ git config --global user.name "xxx"
$ git config --global user.email "[email protected]"

通过命令查看配置

$ git config --list
user.name=xxx
[email protected]

Git的设置参数保存位置在该用户的根目录下/home/username.gitconfig中,可以直接vim该文件修改

$ cat .gitconfig
[user]
    name = xxx
    email = [email protected]

可以以不同作者的身份参与不同项目,只需要将--global改为--local即可


使用Git

(1)新建仓库

在ubuntu终端新建一个文件夹,并且使用git初始化它即可

$ cd /tmp
$ mkdir git-practice
$ cd git-practice
$ git init

此时在项目目录下生成一个.git目录

/tmp/git-practice$ ll
total 32
drwxrwxr-x  4 xxx xxx  4096 8月  12 17:58 ./
drwxrwxrwt 23 root     root     12288 8月  12 22:03 ../
drwxrwxr-x  9 xxx xxx  4096 8月  12 22:04 .git/

如果不想让Git管控这个文件夹,直接删除.git文件即可,整个文件夹中除了.git文件删除了都能被找回来,如果.git删除了就不能找回了


(2)Git管控文件夹

使用git status查看哪些文件和目录已经被Git追踪,哪些还没有被追踪

/tmp/git-practice$ git status
On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)

该项目目录下没有任何文件时没有内容可以提交,因为还没有任何内容被add到暂存区。新建一个文件再次查看git status

/tmp/git-practice$ touch a.txt
/tmp/git-practice$ git status
On branch master

No commits yet

Untracked files:
  (use "git add ..." to include in what will be committed)
    a.txt

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

显示找到一个没有被追踪的文件,没有文件被add到暂存区但是确实有文件存在。下一步将该文件使用git add加入到暂存区,再查看status

/tmp/git-practice$ git add a.txt 
/tmp/git-practice$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached ..." to unstage)
    new file:   a.txt

显示在暂存区有心文件准备要被提交。add可以接多个文件或者通配符,例如

$ git add b.txt c.txt
$ git add *.txt

注意add只会作用于新增改动过删除的文件加入暂存区,已经存在且没有修改的不会处理,例如本地删除某个文件,该文件处于暂存区

/tmp/git-practice$ rm -rf e.txt 
/tmp/git-practice$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached ..." to unstage)
    new file:   a.txt
    new file:   b.txt
    new file:   c.txt
    new file:   d.txt
    new file:   e.txt

Changes not staged for commit:
  (use "git add/rm ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
    deleted:    e.txt

此时显示暂存区存在一个已经本地删除的文件e.txt,虽然本地已经删除,还是可以使用git add操作

/tmp/git-practice$ git add e.txt 
/tmp/git-practice$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached ..." to unstage)
    new file:   a.txt
    new file:   b.txt
    new file:   c.txt
    new file:   d.txt

add之后暂存区也没有了e.txt。
如果想将目录想的所有文件都提交到暂存区,可以只用git add .或者git add --all。在Git 2.x之后的版本,如果都位于项目根目录下使用,两者没有区别,都是对新增,改动,删除的内容要处理,区别是git add .只会对当前目录以及子目录下的文件处理,上层目录不管,而git add --all不管在哪个目录,整个项目有变化的文件一起提交到暂存区。
在项目根目录下新建一个目录test,test下新建aa.txt,在根目录下新建一个文件e.txt

/tmp/git-practice$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached ..." to unstage)
    new file:   a.txt
    new file:   b.txt
    new file:   c.txt
    new file:   d.txt

Untracked files:
  (use "git add ..." to include in what will be committed)
        e.txt
    test/

在test目录下使用git add .

/tmp/git-practice$ cd test/
/tmp/git-practice/test$ git add .
/tmp/git-practice/test$ cd ..
/tmp/git-practice$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached ..." to unstage)
    new file:   a.txt
    new file:   b.txt
    new file:   c.txt
    new file:   d.txt
    new file:   test/aa.txt

Untracked files:
  (use "git add ..." to include in what will be committed)
    e.txt

根目录下的e.txt没有被add到,再试一下add --all

/tmp/git-practice/test$ git add --all
/tmp/git-practice/test$ cd ..
/tmp/git-practice$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached ..." to unstage)
    new file:   a.txt
    new file:   b.txt
    new file:   c.txt
    new file:   d.txt
    new file:   e.txt
    new file:   test/aa.txt

全部add成功


(3)把暂存区文件提交到本地仓库

如果想把暂存区的内容永久保存下载使用git commit命令

/tmp/git-practice$ git commit -m "init commit"
[master (root-commit) c6e5309] init commit
 6 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 a.txt
 create mode 100644 b.txt
 create mode 100644 c.txt
 create mode 100644 d.txt
 create mode 100644 e.txt
 create mode 100644 test/aa.txt

-m后面的内容表示本次提交做了什么,中英文都可以,简短说明即可。
只有暂存区有文件才能commit,如果为空可以使用--allow-empty提交,例如

/tmp/git-practice$ git commit --allow-empty -m "空文件"
[master 966f3e3] 空文件

(4)工作区,暂存区,存储区
  • git add将文件从工作目录移动至暂存区
  • git commit将暂存区的内容移动至存储区
工作区,暂存区,存储区

如图使用二段操作先add后commit,这样的好处是先将文件放入暂存区,积累到一定数量之后再统一提交,避免commit太零碎,减少commit次数,导致代码不完整不方便代码review。也可以使用在commit中使用-a参数直接提交,但是仅对存储区中已经有的文件生效,例如创建一个f.txt,修改e.txt

/tmp/git-practice$ git status
On branch master
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
    modified:   e.txt

Untracked files:
  (use "git add ..." to include in what will be committed)
    f.txt

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

此时不使用二段式使用-a直接提交

/tmp/git-practice$ git commit -a -m "test"
[master 13a5a9e] test
 1 file changed, 1 insertion(+)
/tmp/git-practice$ git status
On branch master
Untracked files:
  (use "git add ..." to include in what will be committed)
    f.txt

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

已经修改的e.txt已经提交,但是新建的f.txt还是没有被git追踪,需要add。


(5)查看整个工作项目的提交记录(log)

使用git log命令可以查看提交记录,在项目根目录下操作

/tmp/git-practice$ git log
commit 13a5a9e45fbc3ced0c9b05cfb8a008d948f4d7cb (HEAD -> master)
Author: xxx 
Date:   Sat Aug 13 07:16:46 2022 +0800

    test

commit 966f3e362e0778318f5dbc8d7c9c9f32c67b101f
Author: xxx 
Date:   Sat Aug 13 07:08:46 2022 +0800

    空文件

commit c6e53097ec92cab959507b8eafab96b11f9ca18b
Author: xxx 
Date:   Fri Aug 12 22:58:18 2022 +0800

    init commit

可以看到提交了3次,越新的提交越在上面。每次有commit号作者提交时间commit内容,其中commit号是使用SHA-1计算出来的重复率极低的文本。可以再加额外参数,比如

/tmp/git-practice$ git log --oneline
13a5a9e (HEAD -> master) test
966f3e3 空文件
c6e5309 init commit

这样更清楚只显示一行文本简洁内容。或者--pretty=oneline显示完整sha1码

$ git log --pretty=oneline
db14c2561ea537d769c4581a7b113a222b811324 (HEAD -> master) fourth commit
c6468b5c9b3da548ea3eee62aae1a4c2cd068819 thrid commit
8cbf52670ce848582dfd76c4be419fcbf8e55265 seconf commit
3dfd0b8adf67a961bf2282ea0e0f057ecd4e0838 first commit

进一步可以添加参数进行搜索,例如所有某个指定作者的提交

/tmp/git-practice$ git log --oneline --author="xxx"

搜索提交内容带有关键字的

/tmp/git-practice$ git log --oneline --grep="init"
c6e5309 init commit

(6)使用git删除文件

在git中删除文件相当于一次改动,可以使用linux的rm直接删除,此时相当于工作区该文件删除,同时该文件状态改为delete

/tmp/git-practice$ rm -rf a.txt 
/tmp/git-practice$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
    deleted:    a.txt

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

a.txt状态改为delete

/tmp/git-practice$ ls
b.txt  c.txt  d.txt  e.txt  f.txt  test

本地也没了,如果再add就将该文件放到暂存区

/tmp/git-practice$ git add
/tmp/git-practice$ git status
On branch master
Changes to be committed:
  (use "git restore --staged ..." to unstage)
    deleted:    a.txt

第二种是使用git的rm进行删除,相当于本地和仓库rm,同时直接加入暂存区

/tmp/git-practice$ git rm c.txt
rm 'c.txt'
/tmp/git-practice$ git status
On branch master
Changes to be committed:
  (use "git restore --staged ..." to unstage)
    deleted:    b.txt
    deleted:    c.txt

/tmp/git-practice$ ls
d.txt  e.txt  f.txt  test

注意git rm的文件必须已经commit,如果仅是未追踪或者仅是add没有提交git rm会报错

fatal: pathspec 'aaa.txt' did not match any files  # 没有add报错找不到文件
error: the following file has changes staged in the index:  # 没有commit报错和仓库不一致

还有一种情况是将文件取消git控制,但是本地保留,在git rm的时候增加--cached参数

/tmp/git-practice$ git rm d.txt --cached
rm 'd.txt'
/tmp/git-practice$ git status
On branch master
Changes to be committed:
  (use "git restore --staged ..." to unstage)
    deleted:    b.txt
    deleted:    c.txt
    deleted:    d.txt

Untracked files:
  (use "git add ..." to include in what will be committed)
    d.txt

/tmp/git-practice$ ls
d.txt  e.txt  f.txt  test

此时d.txt状态改为未追踪,本地工作目录还是存在,相当于再暂存区删除,回到最开始的未追踪状态。


(7)使用git修改文件名

如果使用linux的rm,相当于在仓库将某文件改变状态为delete,并且本地多了一个未追踪的新命名的文件

/tmp/git-practice$ mv e.txt ee.txt
/tmp/git-practice$ git status
On branch master
Changes to be committed:
  (use "git restore --staged ..." to unstage)
    deleted:    b.txt
    deleted:    c.txt
    deleted:    d.txt

Changes not staged for commit:
  (use "git add/rm ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
    deleted:    e.txt

Untracked files:
  (use "git add ..." to include in what will be committed)
    d.txt
    ee.txt

此时出现三种状态

  • Changes to be committed:已经在暂存区生成了最新的快照,等待被提交。由于之前删除的三个文件都被手动add或者git rm自动add,已经加入到暂存区
  • Changes not staged for commit:暂存区生成过快照,但是有了新的修改,没有生成新的快照,linux mv不会自动将改动加入到暂存区
  • Untracked files:未跟踪的状态,暂存区中还没有生成快照

需要手动add加入到暂存区

/tmp/git-practice$ git add .
/tmp/git-practice$ git status
On branch master
Changes to be committed:
  (use "git restore --staged ..." to unstage)
    deleted:    b.txt
    deleted:    c.txt
    renamed:    e.txt -> ee.txt

此时在暂存区中已经加入rename操作。
也可以直接使用git mv

/tmp/git-practice$ git mv ee.txt eee.txt
/tmp/git-practice$ git status
On branch master
Changes to be committed:
  (use "git restore --staged ..." to unstage)
    deleted:    b.txt
    deleted:    c.txt
    renamed:    e.txt -> eee.txt

此操作一步到位,暂存区中加入改名操作。
文件的名称对git来说不重要,git基于文件的内容使用sha-1算法计算值生成blob文件在.git/objects目录下,只更改文件名不会使得git重新生成blob对象


(8)修改最后一次commit记录(--amend)

使用git commit --amend可以修改最后一次commit记录,包括修改commit信息,修改文件等,例如修改最后一次commit信息

/tmp/git-practice$ git commit --amend -m "alter commit"
[master 25bf556] alter commit
 Date: Sat Aug 13 12:33:30 2022 +0800
 1 file changed, 1 insertion(+)
/tmp/git-practice$ git log --oneline
25bf556 (HEAD -> master) alter commit
acaf3eb new commit
13a5a9e test
966f3e3 空文件
c6e5309 init commit

新生成了一个commit对象覆盖了原来的。
同样可以修改文件,比如追加一个文件到最后一次commit

/tmp/git-practice$ touch amend.txt
/tmp/git-practice$ git status
/tmp/git-practice$ git add .
/tmp/git-practice$ git commit --amend --no-edit
[master 6b74d34] alter commit
 Date: Sat Aug 13 12:33:30 2022 +0800
 2 files changed, 1 insertion(+)
 create mode 100644 amend.txt
/tmp/git-practice$ git log --oneline
6b74d34 (HEAD -> master) alter commit
acaf3eb new commit
13a5a9e test
966f3e3 空文件
c6e5309 init commit

(9)忽略文件(.gitignore)

工作目录下某些文件比如生产数据库密码配置,缓存文件等,不需要提交和git控制,使用.gitignore文件可以忽略,.gitignore只对在其创建之后才有的文件有效

/tmp/git-practice$ touch .gitignore
/tmp/git-practice$ vim .gitignore 
# vim .gitignore 输入
config.yml  # 忽略根目录下的config.yml文件
# 每一行输入一个文件
config/config.yml  # 忽略config目录下的config.yml

将.gitignore add到暂存区,同时本地新建一个config.yml文件

/tmp/git-practice$ git add .
/tmp/git-practice$ touch config.yml

此时查看status,没有显示config.yml任何信息

/tmp/git-practice$ git status
On branch master
Changes to be committed:
  (use "git restore --staged ..." to unstage)
    new file:   .gitignore

如果在add的增加-f就可以忽略.gitignore,也会把config.yml加入暂存区

/tmp/git-practice$ git add -f config.yml

可以使用git clean -fX删除本地工作目录忽略的文件

/tmp/git-practice$ git clean -fX
Removing config.yml

(10)查看某个文件的提交记录(log)

在git log后面加入文件名可以查看某个文件的提交记录,先修改d.txt,增加一些文本,在add和提交

/tmp/git-practice$ vim d.txt 
/tmp/git-practice$ git add .
/tmp/git-practice$ git commit -m "new commit"

此时使用git log --oneline d.txt查看

/tmp/git-practice$ git log --oneline d.txt
acaf3eb (HEAD -> master) new commit
c6e5309 init commit

有两条提交记录,使用-p参数可以查看具体变更内容

/tmp/git-practice$ git log --oneline -p  d.txt
acaf3eb (HEAD -> master) new commit
diff --git a/d.txt b/d.txt
index e69de29..e61ef7b 100644
--- a/d.txt
+++ b/d.txt
@@ -0,0 +1 @@
+aa
c6e5309 init commit
diff --git a/d.txt b/d.txt
new file mode 100644
index 0000000..e69de29

大致看出new commit那次增加了aa一行文本,+代表增加的内容,-表示删除的内容


(11)追踪提交代码内容的作者(blame)

当某个已经提交的代码文件需要排查某一行的作者,通过git blame可以查看

/tmp/git-practice$ git blame model.py
c5f030e7 (xxx 2022-08-13 19:21:59 +0800  1) import numpy as np
c5f030e7 (xxx 2022-08-13 19:21:59 +0800  2) import tensorflow as tf
c5f030e7 (xxx 2022-08-13 19:21:59 +0800  3) 
c5f030e7 (xxx 2022-08-13 19:21:59 +0800  4) 
c5f030e7 (xxx 2022-08-13 19:21:59 +0800  5) def glorot(shape, name=None):
54f067ef (xxx 2022-08-13 19:25:50 +0800  6)     init_range = np.sqrt(9.0 / (shape[0] + shape[1]))
c5f030e7 (xxx 2022-08-13 19:21:59 +0800  7)     initial = tf.random_uniform(shape, minval=-init_range, maxval=init_range, dtype=tf.float32)
c5f030e7 (xxx 2022-08-13 19:21:59 +0800  8)     return tf.Variable(initial, name=name)

可以看到没一行代码的作者,提交时间,commit号,可见有c5f030e7和c5f030e7两次提交,该窗口可以使用linux less中类似的跳到指定行的方法。


(12)恢复已经删除或者修改的文件或目录(checkout,回滚单个文件)

使用git checkout方法指定一个已经被删除的文件就可以恢复到工作目录

/tmp/git-practice$ rm -rf f.txt 
/tmp/git-practice$ git checkout f.txt

也可以直接加.恢复所有删除的文件

/tmp/git-practice$ git checkout .

git checkout + 文件名实际上是将要恢复的文件从.git目录中复制一份到当前的工作目录,在没删除之前这些文件被add到暂存区,就是在.git/objects下创建了对应的文件。
注意

  • 以上恢复仅对类似linux rm删除的方法可以生效,如果使用git rm或者普通rm删除之后+add,无法使用git checkout恢复删除的文件会报错
error: pathspec 'xxx' did not match any file(s) known to git
  • git checkout恢复文件仅需要文件add即可,没有commit的文件也能被恢复

如果仅有一个或者多个脚本需要回滚到指定版本,而不是整个版本回滚,可以使用checkout

git checkout xxxxx(commit id) XXXX(文件名或者路径)

例如将PredictSeq.java回滚到711c8b45这个版本的时候的样子,覆盖现有工作目录下的PredictSeq.java

git checkout 711c8b45 PredictSeq.java

在回滚之后文件只在工作目录,不再暂存区,需要add。


(13)commit拆掉重做/回滚(reset)

使用git reset使当前的commit切换到之前某个commit状态,例如因为本次commit要重做,要前往前一个commit的状态

/tmp/git-practice$ git log --oneline
54f067e (HEAD -> master) 123
c5f030e gnn
6b74d34 alter commit
/tmp/git-practice$ git reset master^
Unstaged changes after reset:
M   model.py
/tmp/git-practice$ git log --oneline
c5f030e (HEAD -> master) gnn
6b74d34 alter commit

54f067e这个commit已经删除回退到c5f030e,其中master/HEAD代表当前的commit,^代表前一次,另一种是直接指定要前往的commit号

/tmp/git-practice$ git reset 6b74d34
/tmp/git-practice$ git log --oneline
6b74d34 (HEAD -> master) alter commit

reset有三种模式

  • --mixed(默认),该种模式拆除的文件会留在工作目录,但会从暂存区删除
  • --soft,该种模式暂存区和工作目录都不会被删除,仅是HEAD移动
  • --hard,该种模式不论是暂存区还是工作目录都会被删除

reset常用于本地仓库代码回滚,测试一下

# 修改追加一行print("回滚测试") 到hello.py,新建一个huigun.txt
# commit新增一条commit记录
$ git commit -m "这是一个回滚测试版本"
[master 5ffc52a] 这是一个回滚测试版本
 2 files changed, 4 insertions(+)
 create mode 100644 huigun.txt

$ git log --oneline
5ffc52a (HEAD -> master) 这是一个回滚测试版本
db14c25 fourth commit
c6468b5 thrid commit
8cbf526 seconf commit
3dfd0b8 first commit

先测试--hard回滚,本地仓库直接恢复到一个旧的版本,且从它之后的版本在树中消失

$ git reset --hard db14c25
HEAD is now at db14c25 fourth commit

$ git log --oneline
db14c25 (HEAD -> master) fourth commit
c6468b5 thrid commit
8cbf526 seconf commit
3dfd0b8 first commit

回滚后本地工作目录和上一次提交时一样,文件修改的恢复原样,新增的也被删除。实际上5ffc52a 这个commit还在本地仓库中,可以再切过去,使用git reflog查看出历史所有对commit的操作

$ git reflog --oneline
db14c25 (HEAD -> master) HEAD@{0}: reset: moving to db14c25
5ffc52a HEAD@{1}: commit: 这是一个回滚测试版本
db14c25 (HEAD -> master) HEAD@{2}: commit: fourth commit
c6468b5 HEAD@{3}: commit: thrid commit
8cbf526 HEAD@{4}: commit: seconf commit
3dfd0b8 HEAD@{5}: commit (initial): first commit

再reset过去又恢复了

$ git reset --hard 5ffc52a
HEAD is now at 5ffc52a 这是一个回滚测试版本

$ ls
hello.py  huigun.txt  README.md  test/  two.txt  world..py

$ cat hello.py
print("hello")
print(1)
print("回滚测试")

$ cat huigun.txt
回滚测试

git log消失的commit也被恢复

$ git log --oneline
5ffc52a (HEAD -> master) 这是一个回滚测试版本
db14c25 fourth commit
c6468b5 thrid commit
8cbf526 seconf commit
3dfd0b8 first commit

从smartgit可视化来看树图


工作区回滚与恢复

也可以使用^符号回滚到指定版本的之前一个版本,一个^代表一个版本,两个就^^,再多可以使用~nn是前多少个版本

$ git log --oneline
db14c25 fourth commit
c6468b5 thrid commit
8cbf526 seconf commit
3dfd0b8 first commit

$ git reset --hard HEAD^
HEAD is now at db14c25 fourth commit

$ git log --oneline
db14c25 (HEAD -> master) fourth commit
c6468b5 thrid commit
8cbf526 seconf commit
3dfd0b8 first commit

$ git reset --hard HEAD~2
HEAD is now at 8cbf526 seconf commit

$ git log --oneline
8cbf526 (HEAD -> master) seconf commit
3dfd0b8 first commit

如果在HEAD下改动了还没有commit,想直接回滚重写,可以直接回滚HEAD

$ git log --hard HEAD

再测试一下默认的mixed模式,现在修改一个文件huigun.txt,新增一个文件new.py

$ echo "这是一个mixed测试" > huigun.txt

$ touch new.py

$ git add .
warning: LF will be replaced by CRLF in huigun.txt.
The file will have its original line endings in your working directory

$ git commit -m "这是一个mixed测试"
[master f3481aa] 这是一个mixed测试
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 new.py

$ git log --oneline
f3481aa (HEAD -> master) 这是一个mixed测试
5ffc52a 这是一个回滚测试版本
db14c25 fourth commit
c6468b5 thrid commit
8cbf526 seconf commit
3dfd0b8 first commit

使用默认的mixed回滚到上一个版本,回滚后结果如下

$ git status
On branch master
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
        modified:   huigun.txt

Untracked files:
  (use "git add ..." to include in what will be committed)
        new.py

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

结果就是HEAD已经回到上个版本,但是本地工作目录还是存在代码改动,修改的脚本还是被修改了改为未add状态,新增的脚本还是被新增了改为未追踪状态。再测试一下soft模式

$ git reset --soft HEAD^

$ git log --oneline
5ffc52a (HEAD -> master) 这是一个回滚测试版本
db14c25 fourth commit
c6468b5 thrid commit
8cbf526 seconf commit
3dfd0b8 first commit

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged ..." to unstage)
        modified:   huigun.txt
        new file:   new.py

结果仅仅是HEAD移动到上一个commit,但是工作目录和暂存区都还保留了改动。总结一下hard,soft,mixed都会把HEAD移动到想要回滚的commit,区别就是在工作目录和暂存区,hard会把整个工作目录文件变动全部还原和回滚之前一样,mixed工作目录变动保留,但是变动的文件在暂存区移除需要重新add,soft什么变动都保留,仅仅是HEAD移动


(14)git diff 比较文件差异

使用git diff可以查看文件差异,具体可以查看工作区和缓存区的差异,以及缓存区和仓库(上一次提交)的差异。
git diff不加任何参数查看项目下,工作目录和暂存区的差异

$ git diff
warning: LF will be replaced by CRLF in README.md.
The file will have its original line endings in your working directory
diff --git a/README.md b/README.md
index e69de29..7898192 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1 @@
+a
diff --git a/hello.py b/hello.py
index 4b74a69..0e56f06 100644
--- a/hello.py
+++ b/hello.py
@@ -1,3 +1,6 @@
 print("hello")
 print("hello world")
-print("hello world world")
\ No newline at end of file
+print("hello world world")
+
+
+print(1)

加上--stat可以只输出摘要而非具体改动内容

$ git diff --stat
warning: LF will be replaced by CRLF in README.md.
The file will have its original line endings in your working directory
 README.md | 1 +
 hello.py  | 5 ++++-
 2 files changed, 5 insertions(+), 1 deletion(-)

摘要就是READ.md加了一行, hello.py加了4行删了1行,一共动了5行。如果一行代码局部出现改动,git按照删除该行又新增一行处理
git diff最后加上指定文件可以只看该文件的改动而不是整个项目

$ git diff --stat hello.py
 hello.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

git diff加上--cached可以比较已经add的暂存区和仓库的文件差异

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged ..." to unstage)
        modified:   world..py

Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
        modified:   README.md
        modified:   hello.py

修改过的world..py已经再暂存区了,使用git diff --cached查看他和上一次提交到仓库的差异

$ git diff --cached
diff --git a/world..py b/world..py
index 72943a1..97811de 100644
--- a/world..py
+++ b/world..py
@@ -1 +1,7 @@
-aaa
+aac
+
+
+
+ccd
+
+cd

参考上面的git rm --cached将文件从暂存区移除回到未追踪,--cached参数就是将git命令操作再暂存区的意思。
另外git diff HEAD能查看所有已经再暂存区和还没有到暂存区的文件和上一次提交的文件差异,因为HEAD总是指向你最近的一次提交记录

$ git diff HEAD --stat
warning: LF will be replaced by CRLF in README.md.
The file will have its original line endings in your working directory
 README.md | 1 +
 hello.py  | 5 ++++-
 world..py | 1 -
 3 files changed, 5 insertions(+), 2 deletions(-)

(15)HEAD是什么

HEAD是一个指标指向一个分支,代表当前所在分支,在啊.git/HEAD中记录了HEAD内容,HEAD总是指向你最近的一次提交记录

/tmp/git-practice/.git$ cat HEAD 
ref: refs/heads/master

记录了HEAD内容的目录追踪进去看

/tmp/git-practice/.git$ cd refs/
/tmp/git-practice/.git/refs$ cd heads/
/tmp/git-practice/.git/refs/heads$ cat master 
6b74d34e6ed26227a9a017006c8574933134b7f8

实际上记录一个commit号

/tmp/git-practice$ git log --oneline
6b74d34 (HEAD -> master) alter commit
acaf3eb new commit
13a5a9e test
966f3e3 空文件
c6e5309 init commit

该commit号和最新一次提交的commit号等同。


(16).git目录简述

工作目录下的.git目录主要包含Tree对象,commit对象,Blob对象,Tag对象,在使用git各种命令的时候实际上在不断修改和写入这个.git下的文件
当创建一个文件时处于非追踪状态,此时add到暂存区就会在.git/objects下根据文件的内容创建一个blob对象。git先根据sha-1计算出blob的的名字,例如

/tmp/git-practice$ touch testa.txt 
/tmp/git-practice$ vim testa.txt 
#插入abc
abc
/tmp/git-practice$ git add .

使用git命令查看文件的sha-1值

/tmp/git-practice$ echo "abc" | git hash-object --stdin
8baef1b4abc478178b004d62031cf7fe6db6f903

生成了一个40长度的字符串,其中前2位作为文件夹的名字存在.git/objects下,后38位作为文件名,查看.git/objects

/tmp/git-practice/.git/objects/8b$ ls
aef1b4abc478178b004d62031cf7fe6db6f903
/tmp/git-practice/.git/objects/8b$ pwd
/tmp/git-practice/.git/objects/8b

这个aef1b4abc478178b004d62031cf7fe6db6f903文件是被压缩的,文本编辑器查看不了,使用git cat-file来查看

/tmp/git-practice$ git cat-file -t 8baef1b4abc478178b004d62031cf7fe6db6f903
blob
/tmp/git-practice$ git cat-file -p 8baef1b4abc478178b004d62031cf7fe6db6f903
abc

查看时候必须是完整的40长度,其中-t表示查看对象的类型,-p表示查看内容,这样就可以从压缩的文件查看原本的内容。总结Blob的文件名是SHA-1决定的,Blob的内容由压缩算法决定的。git采用前两位分文件夹也是避免一个目录下文件太多降低效率。
创建一个空的文件夹不会被git感知到,git只对文件有感应,空的目录无法被加入到git中。此时在空文件夹中创建一个空文件

/tmp/git-practice$ mkdir doc
/tmp/git-practice$ cd doc/
/tmp/git-practice/doc$ touch doc1.txt
/tmp/git-practice$ git status
On branch master
Changes to be committed:
  (use "git restore --staged ..." to unstage)
    new file:   testa.txt

Untracked files:
  (use "git add ..." to include in what will be committed)
    doc/

此时在status中就能感应得到,虽然创建的文件为空内容,但是也是有内容的(空的内容),此时git根据sha-1和压缩算法创建blob对象,如果项目已经有过空脚本的add则不会再在.git/objects下再创建因为内容一样blob只关系所有文件的内容是否一样,一样就不重复创建,不关系文件的名称以及来自的文件夹,文件夹和文件名称是Tree对象的范畴
Tree对象也是生成在.git/objects下,在commit之后生成

/tmp/git-practice$ git status
On branch master
Changes to be committed:
  (use "git restore --staged ..." to unstage)
    new file:   doc/doc1.txt
    new file:   testa.txt

目前有两个文件在暂存区已经创建最新快照,此时commit之后查看.git/objects下的26/26a0e8ab4a7de23e8578d64234f3d4ff509881a0

/tmp/git-practice/.git/objects/26$ git cat-file -p 26a0e8ab4a7de23e8578d64234f3d4ff509881a0
100644 blob 039f8c8423f854784f8eafd0b968e80a4dfd0a83    .gitignore
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    amend.txt
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    config.yml
100644 blob 6ad46738804ca6670e653cc8593f134e252f4ac1    d.txt
040000 tree 3aa0d85a4ec5bf18f43bc7e2bf8ff8e08942e57d    doc
100644 blob e61ef7b965e17c62ca23b6ff5f0aaf09586e10e9    eee.txt
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    f.txt
040000 tree f13f51556efabe074d5b255eabcdd3ec33520c55    test
100644 blob 8baef1b4abc478178b004d62031cf7fe6db6f903    testa.txt

该对象自身是一个tree,他下面又指向很多tree和blob,其中3aa0d85a4ec5bf18f43bc7e2bf8ff8e08942e57d这个tree对象就是doc目录,跟踪进去

/tmp/git-practice/.git/objects/26$ git cat-file -p 3aa0d85a4ec5bf18f43bc7e2bf8ff8e08942e57d
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    doc1.txt

doc下面是一个doc1.txt压缩之后的blob对象,由此可见这个tree->(blob,tree)的DAG有向无环图把 目录结构文件夹名称文件名都串起来了,首先整个工程是一个tree,然后他指向其他子tree或者blob。git想当于用Tree对象单独记录了目录结构和文件/文件夹名称,blob单独记录文件内容,通过40位的blob号进行关联文件名和内容的关系
然后看另一个对象

/tmp/git-practice/.git/objects/ad$ git cat-file -t ad1acc43daf0d86d5ebec5b69f1d87220d564088
commit

此乃一个从哦秘密他对象,看内容

/tmp/git-practice/.git/objects/ad$ git cat-file -p ad1acc43daf0d86d5ebec5b69f1d87220d564088
tree 26a0e8ab4a7de23e8578d64234f3d4ff509881a0
parent 6b74d34e6ed26227a9a017006c8574933134b7f8
author xxx  1660395552 +0800
committer xxx  1660395552 +0800

my commit

一切明了了,这个commit对象包含一个tree对象,上一次的commit号,作者,提交者,提交信息,相当于commit是以一个tree对象为根基,附带上一次commit号,作者,提交者,提交日期,提交信息的对象。其中tree就是刚才那个tree对象,对象号一样,观察一下上次提交号6b74d34e6ed26227a9a017006c8574933134b7f8

/tmp/git-practice/.git/objects/ad$ git cat-file -p 6b74d34e6ed26227a9a017006c8574933134b7f8
tree 12df76f32d24b906ac7274391f7c35d0cb7db5df
parent acaf3eb290728cc2cd2f5a2a75da79cdf6dff72a
author xxx  1660365210 +0800
committer xxx  1660389419 +0800

alter commit

上一次的提交信息是alter commit,看一下上一次的目录结构DAG

/tmp/git-practice/.git/objects/ad$ git cat-file -p 12df76f32d24b906ac7274391f7c35d0cb7db5df
100644 blob 039f8c8423f854784f8eafd0b968e80a4dfd0a83    .gitignore
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    amend.txt
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    config.yml
100644 blob 6ad46738804ca6670e653cc8593f134e252f4ac1    d.txt
100644 blob e61ef7b965e17c62ca23b6ff5f0aaf09586e10e9    eee.txt
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    f.txt
040000 tree f13f51556efabe074d5b255eabcdd3ec33520c55    test

在上次提交的DAG中没有testa.txt和doc目录,符合之前的操作事实。因此Tree,blob,commit三个对象关系如图


Tree,Blob,Commit关系

由此可见git是以一个commit为根,拉起对应的tree,以及下面的各个blob和tree,就如同拎葡萄一样,作者给了一个图很形象


从一个commit对象拉起整个DAG

对于历史commit可以使用git checkout+commit号重新拎起来历史提交版本,例如获取上一次提交的整个tree

/tmp/git-practice$ git checkout 6b74d34e6ed26227a9a017006c8574933134b7f8
Note: switching to '6b74d34e6ed26227a9a017006c8574933134b7f8'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c 

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 6b74d34 alter commit

此时再看工作目录,testa.txt和doc目录已经被删除

/tmp/git-practice$ ls
amend.txt  config.yml  d.txt  eee.txt  f.txt  test

使用分支

在多人开发的时候,想以现有的版本之上增加新的功能或者修改Bug,或者尝试新的做法可以另外做一个分支,鞥确认没问题再合并回来。

分支简单使用
(1)查看分支和默认分支master

使用git branch来查看现有的分支

$ git branch
* master

Git会默认设置一个叫做master的分支,前面的*代表现在正在这个分支上。


(2)创建新分支

如果要新增分支,需要再git branch后面加上分支名称

$ git branch cat
$ git branch
  cat
* master

(3)分支修改名称

git branch -m 原名称 现名称来改名

$ git branch -m cat dog
$ git branch
  dog
* master

master分支也可以改名字

$ git branch -m master new_master
$ git branch
  dog
* new_master

(4)删除分支

删除分支加上-d参数

$ git branch -d dog
Deleted branch dog (was ad5f2e2).

$ git branch
* new_master

当前所在分支不能删除,只能删除其他分支

$ git branch cat
$ git branch
  cat  # 创建了新分支cat
* new_master

$ git branch -d new_master
error: Cannot delete branch 'new_master' checked out at 'D:/git-practice'  # 直接删除当前分支报错

$ git checkout cat
Switched to branch 'cat'  # 切换到cat分支

$ git branch
* cat
  new_master

$ git branch -d new_master
Deleted branch new_master (was ad5f2e2).

$ git branch  # new_master分支已经被删除
* cat
(5)切换分支

使用git checkout+分支名

# 切换到cat分支
$ git checkout cat

(6)切换分支的理解

先理解一下master和HEAD,master是git的默认分支仅此而已没有什么其他特殊。在项目目录下第一次commit之后git默认分配一个master分支,使用git log查看提交记录,查看commit id

$ git log --oneline
787dad4 (HEAD -> master) first commit

787dad4 是commit id,HEAD表示当前分支,master是一个分支名称指向787dad4 这次commit,三者关系如图


commid id,分支,当前分支

master分支指向commit id,HEAD指向master分支表示master分支是当前所在的分支。此时再在当前分支下再提交一次commit,比如修改了a.txt

$ echo "1" > a.txt

$ git add .
warning: LF will be replaced by CRLF in a.txt.
The file will have its original line endings in your working directory

$ git commit -m "second commit"
[master b46bb64] second commit
 1 file changed, 1 insertion(+)

$ git log --oneline
b46bb64 (HEAD -> master) second commit
787dad4 first commit

master指向了新提交的b46bb64 commit,HEAD也跟着master再走,如图


同分支下新一次commit

相当于master是一个标签,指向最新一次提交的commit id,HEAD也是一个标签,标记哪一个分支是当下的分支,当下项目只有一个master分支因此肯定指向master,因此master和HEAD是两个不同的作用,master就是一个分支名称,一个分支可以叫任意名称,只不过master是git的默认分支名,HEAD的作用是标记哪一个分支是当下分支。master和HEAD就像贴纸一样贴在某个commit id上。
引入多个分支理解一下,新建一个分支cat,并且修改a.txt再提交一次

$ git branch cat

$ git branch
  cat
* master

新建一个一个叫cat的分支,HEAD还是再master,现在切换分支

$ git checkout cat
Switched to branch 'cat'

$ git branch
* cat
  master

*号走到cat,当前分支为cat,看一下此时的提交日志

$ git log --oneline
b46bb64 (HEAD -> cat, master) second commit
787dad4 first commit

此时有两个分支指向最新的提交commit号,分别是cat和master,此时的关系图如下


新建分支并且checkout切换

此时修改一下a.txt,以cat分支的身份再提交一次

$ git ls-files -s
100644 d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 0       a.txt

没修改之前a.txt内容是1,生成的blob号是d00491fd7e5bb6fa28c517a0bb32b8b506539d4d ,此时修改输入2

$ echo "2" > a.txt
$ git ls-files -s
100644 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 0       a.txt

由于内容修改因此重新生成一个blob,老的blob也存在不覆盖,两个都可以查看

$ git cat-file -p 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f
2

$ git cat-file -p d00491fd7e5bb6fa28c517a0bb32b8b506539d4d
1

印证了之前说的只要内容有任何改动,git就会新生成一个blob。此时以cat身份提交一次

$ git commit -m "cat commit"
[cat 3e92641] cat commit
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git log --oneline
3e92641 (HEAD -> cat) cat commit
b46bb64 (master) second commit
787dad4 first commit

此时cat跟着最新的commit走了,HEAD继续跟着cat,并且脱离master,示意图如下


cat分支新提交一次

从图上来看cat分支和master分支并不是独立的,而是同一个树结构,新的分支不复制原有分支,而是再原始树上的子节点长出新的节点,已经分支标签和HEAD标签的移动,切换不同分支就是选择不同的树路径,看一下如果切回master分支

$ git checkout master
Switched to branch 'master'

$ ls
a.txt

$ cat a.txt
1

此时a.txt又恢复为1了,原因是,master指向的是b46bb64,b46bb64指向的tree对象下的blob文件是d00491fd7e5bb6fa28c517a0bb32b8b506539d4d

(7)切换分支发生了什么

切换分支做了两件事情

  • (1)更性暂存区和工作目录:切换后会使用那个分支指向的commit的内容来更新暂存区和工作目录,commit的内容就是以commit为根拉起他所指向的tree和blob对象。
  • (2)更改HEAD的位置:切换之后HEAD跟着新切换的分支走,底层会修改./git/HEAD文件,实际上就是修改refs/heads/master的值为该分支指向的commit

另外如果切换到分支A后修改的内容没有commit,然后直接又切换了分支B,则修改的内容会随着切换一起走,修改的内容会到B下,就是说未commit的文件会在切换时被携带,直到在哪个分支下commit位置才固定下来,也就是说可以在A分支下修改内容,但是在B分支下提交算做B分支的改动内容

(8)合并分支

新建分支修改和提交经过验证无误之后,需要合并回master分支,切换到master分支下,使用git merge+ 需要合并的分支名称,一个例子如下

/tmp/git-practice$ git branch cat
/tmp/git-practice$ git checkout cat
Switched to branch 'cat'
/tmp/git-practice$ vim a.txt
/tmp/git-practice$
/tmp/git-practice$ touch b.txt
/tmp/git-practice$ git add .
/tmp/git-practice$ git commit -m "cat commit"
[cat 4fe449c] cat commit
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 b.txt

以上内容是在分支cat下新建了b.txt和修改a.txt,然后切回master合并

/tmp/git-practice$ git merge cat
Updating 65d44e4..4fe449c
Fast-forward
 a.txt | 2 +-
 b.txt | 0
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 b.txt
/tmp/git-practice$ ls
a.txt  b.txt
/tmp/git-practice$ cat a.txt
b

合并之后b.txt同样存在在工作目录下,且a.txt内容也同步了在cat下的改动,再看一下提交日志

/tmp/git-practice$ git log --oneline
4fe449c (HEAD -> master, cat) cat commit
65d44e4 firse commit

此时master跟上来了,本来master在65d44e4

(9)master合并快转模式与非快转模式

在刚才合并时打印了日志

/tmp/git-practice$ git merge cat
Updating 65d44e4..4fe449c
Fast-forward
 a.txt | 2 +-
 b.txt | 0
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 b.txt

其中Fast-forward表示使用master合并其他分支时用了快转模式,这是git默认的模式,他是说把master标签从老commit拿下来直接跟到cat指向的commit,并没有创建一个额外的commit对象,可以使用--no-ff选择非快转模式,非快转模式会做出一个额外的commit对象指向master的commit和擦头的commit

/tmp/git-practice$ git merge cat --no-ff -m "merge cat to master"
Merge made by the 'recursive' strategy.
 a.txt | 2 +-
 b.txt | 0
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 b.txt

看一下提交日志

/tmp/git-practice$ git log --oneline
ae5c2e1 (HEAD -> master) merge cat to master
ab157bc (cat) cat commit
fc32b71 first commit

新生成了一个commit,master跟上,cat保持在原地,再看一下可视化工具smartgit是如何展示的


--no-ff合并

smartgit绕了一一个圈,这种非快转模式可以使得master分支更加干净,快进式合并会把cat的提交历史混入到master中,搅乱master的提交历史。举个例子这是原始模式在心分支下进行了两次新提交

          A---B---C cat
         /
D---E---F master

这是使用快转模式合并的结果,cat的两次历史提交都也进入master路径

          A---B---C cat
         /         master
D---E---F

这是使用非快转模式的提交结果,他新建了一个commit G,master不包含A,B两次提交,更加干净

          A---B---C cat
         /         \
D---E---F-----------G master
(10)删除分支和恢复删除的分支

删除分支使用git branch -d + 分支名,如果一个分支还没有被master合并会需要使用git branch -D强制删除

/tmp/git-practice$ git branch -d cat
Deleted branch cat (was ab157bc).
/tmp/git-practice$ git log --oneline
ae5c2e1 (HEAD -> master) merge cat to master
ab157bc cat commit
fc32b71 first commit
/tmp/git-practice$ git branch
* master

删除之后git branch查看分支仅有master分支,但是查看log还是有cat指向的commit,删除分支仅仅是产出commit的分支标签,commit文件依旧存在,类似图示效果

删除分支标签

smartgit里面分支绿色阴影也消失,只有commit的提交信息


删除分支后的smartgit线图

因此恢复分支就是给一个无头苍蝇的commit继续打上一个分支标签,使用如下语句

/tmp/git-practice$ git branch new_cat ab157bc
/tmp/git-practice$ git branch
* master
  new_cat
/tmp/git-practice$ git log --oneline
ae5c2e1 (HEAD -> master) merge cat to master
ab157bc (new_cat) cat commit
fc32b71 first commit

这下又有了,同样smartgit也更新


恢复分支

你可能感兴趣的:(Git:git版本控制知识整理和使用说明)