Git的基本使用

git init #初始化Git仓库
git add . #添加所有未追踪文件或修改
git add filename #添加指定文件
git commit -m "commit information" # 提交
git push #推送到远程
git pull #从远程拉取
git status # 查看当前状态
git log # 查看提交日志

前一个版本:

I’m a little tired!!!

当前版本:

I’m not tired!!!

这时使用 git add 和 git commit 提交了, 但是想回退过去的操作办法如下:

  1. git中HEAD表示当前的提交版本, HEAD^表示前一个提交版本,所以想回退的操作为: git reset --hard HEAD^

    未回退命令时日志显示: 有两个版本: first 和 not tired

    使用回退命令后日志显示: 只有 first 了

  2. 当知道commit ID时, 使用 commit ID直接跳到想要的版本.

    比如现在想再返回 not tired, 操作命令为: git reset --hard commit_id

    这时再查看log结果为:

    可以看到, 使用了这个命令后, 我们的not tired 提交又回来了.

  3. 当不知道commit ID时也是有办法的

    git提供了 git reflog 命令, 这个命令的输出结果是记录你的每一次命令, 使用 git reflog 命令查看历史命令:

    可以看到第一列记录了所有的commit ID, 最后一列记录了执行的操作, 根据最后一列信息找到对应的ID号即可

  • 如何跳到某个指定commit_id的版本?
  • 假如你刚提交了一个版本, 现在后悔了想返回到前一个版本, 应该使用什么命令?
  • 假如你回退到前某个版本, 现在后悔了, 想返回到最近的某个版本, 但是 commit_id 不记得, 该怎么办?
  • HEAD 为当前版本, 使git reset --hard commit_id 可以跳到对应的版本

  • git reflog 命令可以查看所有操作的记录, 可以用于查找所需要的版本号

  • tip: HEAD表示当前版本, HEAD^表示前一个提交版本, HEAD^^为前2个提交的版本, HEAD~100 为第前100个版本

前一节所说的版本回退有个缺点,

那就是, 回退到前一个版本, 所有相关文件的最新修改均会丢失.

并且版本回退是针对已提交的内容的.

现在有个新问题: 在git管理的版本下, 撤销对某个文件的修改.

这之前要了解一下git管理的基本逻辑.

git仓库其实分为工作区(working tree), 暂存区(stage), 和 仓库()

工作区就是我们看到和使用的部分

暂存区是我们使用 git add filename 后文件所到达的部分

仓库是我们使用 git commit 后文件所在的部分

在使用 git commit 命令时, 只会将暂存区部分的内容保存到仓库

  1. 在上一次提交后, 本次我们对某个文件进行了修改, 发现修改后程序运行出错了.

现在删除对文件的修改, 应该怎么做(并未使用 git add命令)?

这时需要的命令为 git checkout -- filename.

对文件修改后查看工作区状态:

可以看到有修改, 这时使用 git checkout filename 后结果为:

再次查看文件, 修改己经没有了

  1. 假设这次我们对文件进行了修改, 且已经使用了 git add filename了该怎么办呢?

办法是使用 git reset HEAD filename 记其回到未 add 的情况.

我们将修改添加到暂存区, 并查看状态:

先看一下与前面的区别, 未add时, 字是红色的, 提示信息是"尚未暂存以备提交的变更"

使用 git add filename 后提示信息是"要提交的变更"

此时使用 git reset HEAD filename 后的状态为:

可以看到取消了暂存的变更, 这时查看状态则又到了修改未提交的状态了.

此时再使用 git checkout -- filename 命令即可以撤销修改了.

  • 有修改, 但是未添加到暂存区, 如何撤销?
  • 有修改, 已添加到暂存区, 如何撤销?
  • git checkout -- filename 撤销对工作区文件的修改
  • git reset HEAD filename 撤销对文件的暂存操作

git仓库的文件删除和正常文件夹的删除不大一样, 因为git将删除也视为了一个操作, 因此删除也需要提交. 正常删除文件后查看仓库状态:

同样, 使用 git checkout -- filename也可以取消该操作.

使用 git rm filename 删除文件后查看状态:

可以看到, 直接删除后提示信息是红色的, 且提示信息是"修改尚未加入提交".

也就是说, 这时还需要使用 git add命令才可以使用 git commit将删除操作提交到仓库.

而 git rm filename则可以直接将删除提交到仓库.

对于已添加,未添加的修改如何撤销可见上一节.

  1. 若要删除仓库中的文件应如何操作?
  2. 直接删除与使用 git rm 命令删除有何不同?
  3. 删除后如何撤销?
  1. 删除与修改一样, 都是要提交到仓库的
  2. 使用 git rm 操作相当于使用 直接删除并使用了 git add 命令
  3. 删除的撤销可根据是否添加到暂存区同修改一样操作
  1. 分支的创建与删除: git branch branch_name

    创建新分支前:

    创建新分支后: git branch newBranch (* 表示当前HEAD所在的分析, 即当前工作区的分支)

    切换分支: git checkout newBranch 可以看到 * 从main到了newBranch

    分支删除: git branch -d newBranch (不能删除当前所在分支, 因此要先切回main分支才可执行该操作)

  2. 分支的合并现有两个分支:main和bugFix 合并前:

    将HEAD放在main分支上, 并执行 git merge bugFix:

    可以看到, 合并会产生一个新的提交, 且当前节点有了两个父节点.

    这里其实可以更深入理解下git checkout这个命令, 它的真实作用是切换当前工作区在git这个仓库树中的位置.即, HEAD可以指向main或者bugFix这种具体的分支名, 也可以指向某一个commit_id(每一个commit_id即是工作区的一个版本)

假如我们现在有2个分支: master和newBranch, 在这两个分支中我们都对一个文件进行了修改. 这时, 将newBranch分支合并到master分支时就会出现冲突:

这里master分支中我们添加的内容是"Creating a new branch!!!“并提交了.

在newBranch分支中我们添加的内容是"Create a new branch!!!”.

所以在HEAD处于master分支, 并运行 git merge newBranch时会出现冲突.

我们打开提示的冲突文件, 文件内容如下:

Git用<,=,>标记出不同分支的内容,我们修改如下后保存:

这时查看仓库状态为:

此时将其添加到仓库并提交

这时查看日志树可以看到合并过程:

  1. 如何创建分支与删除分支?
  2. 如何在不同分支中进行切换?
  3. 如何合并不同的分支?
  4. 合并分支后有冲突了该怎么办?
  1. 分支创建: git branch branch_name
  2. 分支删除: git branch -d branch_name
  3. 将其他分支合并到当前分支: git merge other_branch
  4. 分支切换: git checkout branch_name
  5. 冲突解决: 修正冲突的文件并添加到暂存区, 然后提交

这是一个很小的概念, 单独说它, 就是因为它很重要.

其实git是由不同的提交组成, 每个提交都可以看成一个树中的节点.

所有的提交在一起组成了一个提交树.

而当前的工作区(也就是我们能看到并修改的那些文件), 就是HEAD所指向的.

其实HEAD并不是非要指向具体的分支, 它也可以直接指向某个提交.

比如当前的提交树为:

可以看到*在bugFix这个分支上, 这也表示HEAD指向bugFix, 我们可以直接让它指向C4 git checkout C4 (这里的C4是提交id):

同样, 也可以使用这个命令让它指向 main, 或者 C0:

其实checkout这个命令用来切换HEAD的指向并不准确, 新版的git使用switch命令

  1. 在不同的仓库进行切换时, 使用commit id十分麻烦, 因为id需要使用git log命令查找, git提供了相对引用, 即^表示上一个, ^^表示上2个, ~10 表示前10个.

    如使用 git checkout main^ 效果为:

    可以看到当HAED指向main时, HEAD^和main^都是指向C5的.

    再次使用 git checkout HEAD~3 结果为:

当本地出错要回退时, 有2个命令可用.

  1. reset可将当前分支回退到指定提交. 可看执行结果:

原本提交树是这样, 使用 git reset HEAD^ 后:

可以看到, 这个命令让分支放弃了当前节点而指向了父节点.

若对原本的提交树使用 git revert HEAD^ 命令后的提交树为:

可以看到, 现在这人分支指向了C1’这个提交, 其实C1’与C1是相同的, 但是 git 将回退作为一个操作然后提交到仓库.

这样做的好处: 在本地其实是没有什么区别的, 但是如果推送到远程, 使用git reset后若要与远程同步, 需要将远程的最新提交删除, 这个操作难以实现, 因此将回退作为一个新的提交可以方便远程与本地的同步, 方便多人合作.

  • rebase: 它的主要作用是创建线性提交记录, 方便工程修改脉路整理. 比如对于当前提交树:

    我们使用 git rebase main后提交树变化为:

    现在分析一下这个结果. feature 和 main 分支的公共父节点是C1, 我在的HEAD指向feature, 此时运行以上命令后, git 将C2,C4 提交接到了main所指向的C3上, 也就是说: 这个命令会将当前分支的所有提交接到指定的分支上

    因此 rebase 可以表象地理解为 剪枝 + 嫁接

    如果使用merge命令的话提交树会变为:

这时提交记录则不够线性, 不方便查看对工程的修改历史

  • rebase 的另一个功能: 提交记录排序与删除.

    git rebase -i HEAD~4: 这个命令会弹出一个交互式的创口, 让你调整最新4个提交节点的顺序或者删除提交节点. 下面是一个例子:

    知道这个功能即可, 不具体说明了

  • cherry-pick: 这个命令允许我们随意地移动提交树中的节点, 将指定的提交节点移动到当前的HEAD所指定的节点下.

    对以下提交树:

    使用 git cherry-pick C2 C4 C6 后结果为:

可以看到, 选中的提交节点都添加到了当前分支 main 所指向的节点下.

这样的命令有什么作用呢?

但是此时, 我们只想要收录解决问题的提交到当前的main下, 此时就可以使用 rebase -i 和 cherry-pick 这两个命令来实现.

将HEAD调整到main分支上, 再使用 git cherry-pick C4 (左) 或者使用 git rebase -i HEAD~3 后调整提交 (右) 即可获得:

下面的提交树中, 我想更新newImage提交中的信息, 且是只更新它的信息, 可使用 rebase -i 和 cherry-pick 这两种方式来实现.

口述使用 rebase -i 命令过程: 先调整C2和C3顺序,将C2提交节点放到最前, 再使用 git commit --amend 来提交列新C2中的信息, 最后使用 rebase -i 命令将C3节点放到最前.

详细说明 cherry-pick 过程 (因为不用交互, 易于博客中展示). 首先调整HEAD到main: git checkout main

其次, 将C2 拿到最前: git cherry-pick C2

然后, 修改C2中的内容并提交: git commit --amend

最后, 将C3节点放到最前: git cherry-pick C3

前文中提交节点要么是使用提交ID标识的, 要么是使用分支名子标识的.

分支名字会被移动, 且有新的提交时分支名字也会移动到新的提交节点中, 提交ID太长,难记.因此需要一个为某个提交节点进行永久命各的办法: 它就是tag. 对于以下的提交树:

为不同的提交节点添加tag, 如运行命令: git tag V1 C1, git tag V2 C2, 这时的提交树为:

同时, 可以直接利用tag名称来移动HEAD: git checkout V1

可以看到这时HEAD指向了C1. 为什么不是V1* 呢, 这是因为无支对某个tag直接提交, 所以才会发生这种分离HEAD的现象.

git describe <ref>: <ref> 为提交树中的引用, 默认为HEAD.

该命令的输出结果为: <tag>_<num>_g<hash>, tag为与该引用最近的标签, num 为 该引用与最近标签的间隔提交数, <hash>为<ref>的提交ID.

对于以下的提交树, 使用 git describe, git describe main, git describe side 的结果分别为: