likes
comments
collection
share

Git 时光机,一只调皮的猫

作者站长头像
站长
· 阅读数 6

前言

本文主要讲Git的相关知识,特别是关于rebase、revert指令的一些个人理解,欢迎大家指正。articles/2023-03-14-GIt时光机(简单小例子).md at main · slshsl/articles (github.com)

Git 与其他版本控制系统的区别

Git 直接记录快照,而非差异比较。

Git 完整性

Git 中所有的数据在存储前都计算校验和,然后以校验和引用Git 用以计算校验和的机制叫做 SHA-1 散列(hash,哈希)。

Git 如何存储数据

Git存储数据有点像 key-value storekey可以认为是datasha1哈希,value认为是数据本身。

💡value是经过ZLib压缩过的

Git 基础概念

Working Space / Working Directory工作区/工作目录,就是你平时存放项目代码的地方,本文统一使用工作区

Index / Stage暂存区/索引区,用于临时存放你的改动,本文统一使用暂存区

Local Repository本地仓库本地版本库)本文统一使用本地仓库

Stash贮藏区

Remote Repository远程仓库远程版本库)本文统一使用远程仓库

<remote_name>/<branch_name>本地远程分支引用/本地远程跟踪分支,本文统一使用本地远程跟踪分支

Git 时光机,一只调皮的猫

常用指令

git xxx -h

查看某个指令的帮助信息。

git config

# 查看git配置
$ git config -l`
# 查看系统配置
$ git config --system --list
# 查看用户配置
$ git config --global --list
# 查看项目配置
$ git config --local --list
# 设置用户名称
$ git config --global user.name xxx
# 设置用户邮箱
$ git config --global user.email xxx
# 启动的编辑器一般为 vim。 当然也可以命令设置你喜欢的编辑器。
# 我一般习惯设置成vscode
$ git config --global core.editor xxx
# 项目启用rerere
$ git config rerere.enabled true
# 设置log -1 HEAD的别名为last
$ git config --global alias.last 'log -1 HEAD'
# 使用last别名
$ git last

💡每一个级别会覆盖上一级别的配置

git init

初始化一个仓库。

git clone

# 克隆一个仓库
$ git clone <url>

💡git clone 命令做了什么?

  1. 添加一个跟踪的远程仓库remote,自动将其命名为 origin,拉取它的所有数据
  2. 创建一个origin/master本地远程跟踪分支
  3. 创建一个 master本地分支
# 克隆一个仓库
$ git clone —depth=1 <url>

通常我们直接使用 git clone 即可克隆项目,如果只是使用一个仓库项目,这是加入 –depth=1参数可以使我们只下载当前的最新提交即可。

git add

添加工作区的内容至索引区。

git status

显示当前的状态。

$ git status
On branch dev
Your branch is ahead of 'origin/dev' by 2 commits.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   a.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:   a.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        c.txt
  1. 显示当前分支与其本地远程跟踪分支(如果有)的关系 可以通过git push推到远程的commit
  2. 显示暂存区和本地仓库有差异的文件 通过运行git commit会添加到本地仓库的文件
  3. 显示工作区和暂存区有差异的文件 通过运行git add可以添加到暂存区的文件
  4. 显示工作区中不被git追踪的文件(也不被.gitignore忽略) 通过运行git add可以添加到暂存区的文件

git commit

# 提交变更到本地仓库
$ git commit -m "xxx"
# 修补提交:修补最后的提交
$ git commit --amend
# 修补提交:修补最后的提交,不修改提交信息
$ git commit --amend --no-edit

Git 时光机,一只调皮的猫

💡git commit --amend:修补最后的提交时,是完全用一个新的提交替换旧的提交。修补提交最明显的价值是可以稍微改进你最后的提交,例如一些小的修补,笔误等等;可以像下面这样操作:

# 第一次提交
$ git commit -m 'initial commit' 
# 修改笔误等
$ git add forgotten_file
# 第二次提交
$ git commit --amend

💡最终你只会有一个提交;第二次提交将代替第一次提交的结果。

💡假如你的代码已经 push 了的话,git commit --amend要慎用,因为会修改提交历史。

# 带签名的提交
git commit --signoff -m 'xxxx'

git log

# 基本用法
$ git log
# 仅显示最近的 n 条提交
$ git log -<n>
# 按补丁格式显示每个提交引入的差异
$ git log -p
# 显示每次提交的文件修改统计信息
$ git log --stat
# 在日志旁以 ASCII 图形显示分支与合并历史
$ git log --graph
# 每条日志一行显示
$ git log --pretty=oneline
# 仅显示 SHA-1 校验和所有 40 个字符中的前几个字符
$ git log --abbrev-commit
# --pretty=oneline --abbrev-commit 合用的简写。
$ git log --oneline
# 仅显示作者匹配指定字符串的提交。
$ git log --author='xxx'
# 
$ git --no-pager log
# Log files that have been moved or renamed
$ git log --name-status --follow -- file

git diff

# 比较工作区与暂存区之间的差别
$ git diff
# 比较暂存区与本地仓库之间的差别
$ git diff —cached(git diff —staged)
# 比较工作区和暂存区(即所有未提交到本地仓库的修改)与本地仓库当前的HEAD之间的差别
$ git diff HEAD
# 比较工作区和暂存区(即所有未提交到本地仓库的修改)与本地仓库dev分支最新commit之间的差别
$ git diff dev
# 比较工作区和暂存区(即所有未提交到本地仓库的修改)与本地远程跟踪dev分支最新commit之间的差别
$ git diff origin/master
# 比较两次提交之间的差别
$ git diff [commit_sha1_value] [commit_sha1_value] 

git tag

有两种tag,一种是lightweight轻量标签;一种是annotated tag附注标签

# 显示所有
$ git tag
# 创建lightweight tag(轻量标签)
# 如果没有写commit_sha1_value,则会自动把tag放到目前的这个commit上
$ git tag <tag_name> [commit_sha1_value]
# 创建annotated tag(附注标签)
# 如果没有写commit_sha1_value,则会自动把tag放到目前的这个commit上
$ git tag -a <tag_name> -m <tag_message> [commit_sha1_value]
# 删除tag
$ git tag -d <tag_name>
# 查看某个commit上的标签
$ git tag --points-at <commit>

💡轻量标签很像一个不会改变的分支——它只是某个特定提交的引用

💡附注标签是存储在 Git 数据库中的一个完整git的tag类型的objects对象, 它们是可以被校验的,其中包含打标签者的名字、电子邮件地址、日期时间, 此外还有一个标签信息。

git show

# 显示tag对应的commit所提交的内容,如果是附注标签,还是显示附注标签的信息
$ git show <tag_name>
# 显示某次commit提交的内容
$ git show <commit_sha1_value>

git branch

# 查看本地仓库分支
$ git branch
# 查看远程仓库分支
$ git branch -r
# 查看本地仓库和远程仓库分支
$ git branch -a
# 创建本地仓库分支
$ git branch <branch_name>
# 强制删除本地仓库分支
$ git branch -D <branch_name>
# 删除本地仓库分支
$ git branch -d <branch_name>
# 查看分支详细信息,包括分支最后一个commit的hash、message等
$ git branch -v
# 同git branch -v
$ git branch -vv
# 修改本地仓库分支名称,如果省略old_branch_name,则更改的是当前分支的名称
$ git branch -m <old_branch_name> <new_branch_name>
# 强制修改本地仓库分支名称
$ git branch -M <old_branch_name> <new_branch_name>
# 查看哪些分支已经合并到当前分支
$ git branch --merged
# 查看哪些分支未合并到当前分支
$ git branch --no-merged

💡git branch -d <branch_name>如果该分支还未合入master或者main,会提示报错,不让删除

💡git branch -vv同时还会显示每一个分支正在跟踪哪个远程分支

git checkout

# 切换到新分支
$ git checkout <branch_name>
# 创建并切换到新分支 
$ git checkout -b <branch_name>
# 重置分支,删除已存在的分支且重新创建,分支不存在也不会报错称
$ git checkout -B <branch_name>
# 将head指向一个commit
$ git checkout <commit_sha1_value>
# 从某个本地远程跟踪分支中检出一个新分支(并设置该分支的上游是远程跟踪分支)
$ git checkout -b <new_branch_name> <remote_name>/<old_branch_name>
# 是 git checkout -b <new_branch_name> <remote_name>/<old_branch_name>的简写
$ git checkout --track <remote_name>/<branch_name>
# 让工作区中的所有文件撤销更改
$ git checkout -- .
# 让工作区中的某些文件撤销更改
$ git checkout -- <file_name> <file_name> 

💡 git checkout -b <new_branch_name> <remote_name>/<old_branch_nameh>

💡 git checkout --track <remote_name>/<branch_name>

💡 git checkout <branch_name>

git checkout -b <new_branch_name> <remote_name>/<old_branch_nameh>的意思是从某个本地远程跟踪分支中检出一个新分支,并设置该新分支的上游是该本地远程跟踪分支; 因为git checkout -b <new_branch_name> <remote_name>/<old_branch_nameh>经常用,且检出的新分支名字和其上游分支的名字一般相同,不太会取不同的名字;所以出了一个简写命令git checkout --track <remote_name>/<branch_name>git checkout --track <remote_name>/<branch_name>该命令从某个本地远程跟踪分支中检出一个新分支,并设置该新分支的上游是该本地远程跟踪分支,同时该新分支的名字不能指定,只能是该本地远程跟踪分支的名字; git checkout --track <remote_name>/<branch_name>还是太长;所以 git checkout branch_name 切换分支时,先从本地库查找分支,在本地库没找到时,会去本地远程跟踪分支中查找,如果本地远程跟踪分支中有相同名称的分支,则也会检出分支并设置其上游为同名的本地远程跟踪分支;如果本地远程跟踪分支里也没有找到就会报错;

💡git checkout <commit_sha1_value>:这样会发生detached head,即head不再指向一个分支,而是指向一个commit; 应用场景是比如从之前的commit或者误删的commit(通过给git reflog查看);拉出一个新分支;

# 查看所有操作的日志
$ git reflog
# 检出需要检出的commit
$ git checkout <ommit_sha1_value>
# 检出一个新的分支
$ git checkout -b <new_branch_name>

💡git checkout --的撤销功能:git checkout撤销的是工作区的内容,即清除工作区 场景一、本地库中已有 user.txt在工作区修改了该文件,但是没有提交到暂存区,此时撤销更改是从本地仓库中恢复内容 场景二、本地库中已有 user.txt在工作区修改了该文件,并且之前按已提交到暂存区,此时撤销更改是从暂存区中恢复内容

git restore

替代git checkout的撤销功能。

# 放弃在工作区的修改
$ git restore <file_name> <file_name>
# 放弃所有文件在工作区的修改
$ git restore .
# 将暂存区的内容,移动工作区中,即是git add的反向操作
$ git restore  —staged <file_name> <filen_ame>

💡git restore <file_name> <file_name>替代git checkout -- <file_name> <file_name>

💡git restore .替代git checkout -- .

💡it restore —staged <file_name> <filen_ame>git add 的反向操作

git switch

替代git checkout的切换分支功能。

# 如果本地仓库或者本地远程跟踪分支有这个分支,则切成功,否则失败
$ git switch <branch_name>
# 创建并切换到新分支 
$ git switch -c <branch_name>
# 重置分支,删除已存在的分支且重新创建,分支不存在也不会报错称
$ git switch -C <branch_name>

💡git switch <branch_name>替代git checkout <branch_name*>

💡git switch -c <branch_name>替代git checkout -b <branch_name>

💡git switch -C <branch_name>替代git checkout -B <branch_name>

💡git switch -可以快速切换上一个分支,来回切换,与 cd -一样

git remote

# 显示远程仓库的名字
$ git remote
# 显示远程仓库的名字及url
$ git remote -v
# 查看本地远程跟踪分支与远程仓库中分支的同步情况
$ git remote show <remote_name>
# 可以删除(在远程仓库中被删除的分支)对应的本地远程跟踪分支
$ git remote prune <remote_name>

💡git remote show <remote_name>:一般应用场景就是在git fetch之前查看一下分支同步情况

💡git fetch —prunegit remote prune的作用一样,实际上删除(远程仓库中被删除的分支)对应的本地远程跟踪分支。

git rebase

变基

$ git rebase <upstream_name>
# 当前分支为dev
$ git rebase main
# 如果有冲突,解决冲突后继续编辑
$ git rebase —continue
# 当前分支为main
$ git merge dev

Git 时光机,一只调皮的猫

git rebase <upstream_name>做了什么? git rebase maindev)把dev分支的提交重放(重新应用)到main分支的顶部。

Git 时光机,一只调皮的猫

git rebase --onto main server client做了什么? 对于这个指令,每次看到都头大,需要自己用文字描述一下,以方便理解

Git 时光机,一只调皮的猫

# newbase_name\upstream_name\branch_name既可以是分支名,也可以是commit_sha1_value
$ git rebase [--onto <newbase_name>] [<upstream_name> | [branch_name]]

💡 如何理解上面这条指令各个参数是啥意思?列出具体指令来尝试讲解

  1. 场景及命令一: 场景:假设dev是从master检出的分支,要在dev分支上变基,当前所在分支不是dev分支 命令:git rebase <upstream_name>git rebase master

    # 切换到dev分支
    git checkout dev
    # rebase master,这个master是对应upstream_name;怎么理解这里的上游分支
    # 实际上就是dev是从master检出的,是dev的上游分支,依此来找到两个分支的交叉点
    # 把在dev分支上以这个交叉点为起点,以当前HEAD为终点(dev最新commit),把这两点之间的
    # commit在master分支顶部(最新commit),重新来一遍,得到一个新的dev分支。
    git rebase master
    
  2. 场景及命令二: 场景:假设dev是从master检出的分支,要在dev分支上变基,当前所在分支不是dev分支 命令:git rebase <upstream_name> [branch_name]git rebase master dev

    # git rebase master dev 是 git checkout dev + git rebase master 这两条命令的简写
    # 就是我先切到dev分支,再rebase master分支
    git rebase master dev
    
  3. 场景及命令三: 场景:假设当前所在分支是dev分支,其本地远程跟踪分支是origin/dev 命令:git rebase

    # git rebase 是 git rebase origin/dev 命令的简写
    git rebase
    

    💡 简写的前提是

    1. 当前不是detached headhead状态
    2. 当前分支有对应的本地远程跟踪分支(即上游分支)
  4. 场景及命令四: 场景:

    Git 时光机,一只调皮的猫
    命令:git rebase --onto main server client

    # 第一步:git checkout client
    # 第二步:找到client与其上游分支server的交叉点的之后的所有commit
    # 第三步:把这些commit基于--onto参数的值main顶部(最新的commit),重放这些commit
    # 理解分为两部分,第一部分找server client交叉之后的提交,
    # 第二部分,以--onto参数的值为基,重放这些提交
    git rebase --onto main server client
    
  5. 场景及命令五: 场景:master分支,共6个提交,每个提交都创建一个txt文件 命令:git rebase --onto HEAD~5 HEAD~3 HEAD

    # 第一步:git checkout HEAD,处于detached headhead状态
    # 第二步:找到HEAD~3与HEAD之间所有的commit
    # 第三步:把这些commit基于HEAD~5,重放这些commit
    # 实际效果就是删除第2次与第3次提交
    # 可以从当前游离的head检出一个分支替代master,或者直接git branch -D master
    git rebase --onto HEAD~5 HEAD~3 HEAD
    

    Git 时光机,一只调皮的猫

  • 应用场景
    1. 在合并分支前,不想分叉,可以先rebase目标分支,再合入目标分支
    2. 同步远程分支时,不使用git pull,使用git fetch,再使用git rebase

💡 共享分支:当一个分支会被push到远程仓库,且有可能其他人会进行pull操作时,这就是一个共享分支

💡 Do not rebase commits that exist outside your repository and that people may have based work on.

💡 永远、永远不要对一个共享分支进行变基

💡 原因就是:变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。

💡 基于变基的本质,如果是多人协作对共享分支允许变基;假如A在本地变基操作后推到了远程共享的分支,同时也丢弃了一些现有的提交;

💡 而B在拉去远程共享分支时,由于依赖了之前A丢弃的提交,所以如果再merge后推送到远程,就会造成重复的提交。

💡 解决方式:那就是永远不要对共享分支进行变基;

交互式变基

# start_commit_sha1_value不包含,end_commit_sha1_value包含
# end_commit_sha1_value可以没有,则默认为当前分支的HEAD
$ git rebase -i [start_commit_sha1_value] [end_commit_sha1_value]
# 如果你异常退出了变基选择操作的窗口,使用以下命令重新打开
$ git rebase --edit-todo
# 放弃变基
$ git rebase --abort
# 保留,啥也不动
**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 [-C | -c] <commit> = like "squash"
       but keep only the previous commit's log message,
       unless -C is used,in which case keep only this commit's message; 
       -c is same as -C but opens the editor
# 删除该提交
**d, drop <commit> = remove commit**

建议使用vscode中的GitLens插件来进行交互式变基,如下图 Git 时光机,一只调皮的猫

git fetch

会拉去数据,同时会同步所有远程仓库分支本地远程跟踪分支,对于本地远程跟踪分支,实际上就是为了区分本地仓库分支,前面加了remote_name的分支引用

💡git fetch 命令做了什么?

  1. 拉取它的所有数据
  2. 更新你的远程跟踪分支
  3. 对与远程仓库中有而本地没有的分支,只会创建远程跟踪分支,不会创建本地分支

💡 当抓取到新的远程跟踪分支时(这句话的意思是比如远程仓库新建了一个分支dev,当git fetch时就会拉取新远程分支到本地的远程跟踪分支origin/dev),但不会创建一个本地dev分支

  1. 如何只是想合并到你的分支,可以执行git merge origin/dev
  2. 如果想在本地分支的工作,可以从origin/dev检出一个本地的dev分支,执行git checkout -b dev origin/dev
# 从远程仓库中拉去所有分支数据到本地,同步所有远程仓库分支和本地远程跟踪分支,如果缺省远程仓库名,默认为origin
$ git fetch <remote_name>
# 从远程仓库中只拉取一个分支数据到本地
$ git fetch <remote_name> <remote_branch_name>
# 从远程仓库中只拉取一个分支数据到本地,并在本地仓库中创建一个分支
$ git fetch <remote_name> <remote_branch_name>:<local_branch_name>
# 首先会同步远程仓库分支与本地远程跟踪分支,同时会将本地远程跟踪分支中存在而远程仓库分支没有的分支删除。
$ git fetch —prune
# 显示fetch的详细信息
$ git fetch -v

💡git fetch —prune:比如初始状态时远程仓库分支和本地远程跟踪分支已经同步; 此时,在远程仓库中删除某个分支a,再使用git fetch时,本地远程跟踪分支中的分支a不会删除; 此场景下可以使用git fetch —prune,也可以使用git remote prune <remote_name>

  • 应用
    1. 应用一 git fetch origin master //从远程origin仓库拉取master分支的数据,同时同步本地远程跟踪分支 master git log -p master origin/master //比较本地仓库master分支和本地远程跟踪分支master的区别 git merge origin/master //将本地远程跟踪分支中的master分支合入本地仓库master分支
    2. 应用二 git fetch origin master:temp //从远程的origin仓库拉取master分支的数据,并在本地仓库中新建一个分支temp git diff temp //比较本地仓库当前分支和本地仓库temp分支的差别 git merge temp //合并本地仓库temp分支到本地仓库master分支 git branch -d temp //删除本地仓库temp分支

git merge

Git 时光机,一只调皮的猫

# 默认采用fast forward
$ git merge
# 采用fast forward
$ git merge -ff
# 强行关闭fast forward
# 个人觉得应用场景是:拉取远程分支,检出自己的分支上开发,然后push前rebase一下远程分支,提个pr,采用git merge --no--ff的方式merge到远程分支。
$ git merge --no-ff
  1. fast forward:这时候bugfix合入master是一次fast forward
  2. 3 way merge

💡 如果已经分叉了,还想实现fast-forwardmerge,可以使用git rebase

git pull

git fetch + git merge

# git fetch + git merge
$ git pull
# git pull的简写
$ git pull --merge
# git fetch + git rebase
$ git pull --rebase
# 显示详细信息
$ git pull -v

git push

将本地仓库当前分支推出送远程分支

# 完整命令,对于没有设置上游的本地分支推送至远程采用的方法
$ git push <remote_name> <local_branch_name>:<remote_branch_name>
# 如果本地分支名和远程分支名一样的情况下,可以省略:<remote_branch_name>
$ git push <remote_name> <local_branch_name>>
# 设置当前的分支的上游分支是
$ git push —set-upstream <remote_name> <remote_branch_name>
# git push -u origin是git push —set-upstream origin的简写
$ git push -u <remote_name> <remote_branch_name>
# 删除远程分支
$ git push <remote_name> -d <remote_branch_name>
# 删除远程分支
$ git push <remote_name> :<remote_branch_name>
# 推送某个标签
$ git push <remote_name> <tag_name>
# 一次性推送很多标签
$ git push origin —tags
# 删除远程标签的方式
$ git push origin --delete <tag_name>

💡git push简写命令使用前提条件是:

  1. 远程仓库有这个分支
  2. 并且通过git branch -vv查看,本地仓库当前分支与本地远程跟踪分支是关联的
  3. 并且当前只有一个originremote

💡git push <remote_name> <local_branch_name>:将本地仓库分支推送到远程仓库;这样操作,虽然会同时更新本地远程跟踪分支;并没有将本地的该分支与对应的本地远程跟踪分支进行关联(通过git branch -vv查看)

💡新建本地分支后push到远程仓库,但并没有将本地分支与对应的本地远程跟踪分支相关联,下次本地分支有新的commit后,再push到远程,依然要git push <remote_name> <local_branch_name>,不能直接用git push这样的简写命令

💡git push -u <remote_name> <remote_branch_name>:将新建的本地分支推送到远程分支,并将该分支与对应的本地远程跟踪分支相关联,下回再推送时就可以使用git push这样的简写

git revert

git revert <commit_sha1_value>
git revert HEAD
git revert HEAD^
git revert HEAD~

下图是在C5为当前HEAD,分别对C5C4C3C2C1C0进行revert Git 时光机,一只调皮的猫

💡区别:HEAD^主要是控制merge之后回退的方向;HEAD~才是回退的步数 个人理解:如果某个节点只有一个父节点,那就用~,不要用^,因为不太直观,容易混乱;只针对有多个父节点的回退操作采用^

git revert -m <commit_sha1_value>
git revert -m HEAD
git revert -m HEAD^
git revert -m HEAD~

💡revert主要分两类,一类是针对只有一个父节点的commit,一类是针对有两个父节点的commit(merge分支产生的)

💡什么时候加-m,当该节点是merge产生的节点的时候,它会有2个父节点(之前老是记不清,一个commit节点是不可能有多于2个父节点的,通过这个-m的参数才想起来,所以这个-m后面跟的数字只能是1和2,1代表我保留自己的分支,2代表保留合入的分支)

💡revert可能带来的问题如下图描述

Git 时光机,一只调皮的猫

对于此问题官网给出的解决方案是在masterrevert掉之前的revert,然后再merge

git reset

# 更改HEAD和当前分支顶部指向哪个commit,并覆盖暂存区
git reset —mixed <commit_sha1_value>
# 只更改HEAD和当前分支顶部指向哪个commit
git reset —soft <commit_sha1_value>
# 更改HEAD和当前分支顶部指向哪个commit,并覆盖暂存区和工作区
git reset —hard <commit_sha1_value>
# git reset —mixed <commit_sha1_value>简写
git reset <commit_sha1_value>

git reset —soft

Git 时光机,一只调皮的猫

git reset —mixed

Git 时光机,一只调皮的猫

git reset —hard

Git 时光机,一只调皮的猫

下图是在C5为当前HEAD,分别resetC4C3C2C1C0可以使用的命令 Git 时光机,一只调皮的猫

💡 revertreset的区别是什么 git revert是用一次新的commit来回滚之前的commit,此次提交之前的commit都会被保留;git reset是回退到某次提交,提交及之前的commit都会被保留,但是此之后的commit都会被删除;所以可以说revert是以新增commit的方式回滚某个commit;而reset回退到某个commit

💡 单从用法上来讲,应用场景可以参考以下: 找出有问题的commit,如果从HEAD一直到有问题的commit之间都不要,那就用reset;如果从HEAD一直到有问题的commit之间,只想回滚有问题的commit,中间其他的commit还要保留,那就用revert

💡 以上过于啰嗦了,简单点,就是回滚某个commitrevert回退到某个commitreset;所以对于git reset HEAD这个命令是无意义的,当前就在这个commit,要回退到这个commit岂不是无用功;对于git revert HEAD这个命令是有意义的,如果你确实想回滚当前的commit

git stash

# 保存工作区暂存区中的内容
git stash
# 保存工作区暂存区中的内容,并添加注释,不推荐使用
git stash save 'message'
# 保存工作区暂存区中的内容,并添加注释,推荐使用
git stash push -m 'message'
# 恢复后同时弹出 stash
git stash pop
# 恢复后stash内容并不删除
git stash apply
# 删除stash
git stash drop
# 清空`stash`
git stash clear
# 显示所有的stash
git stash list
# 显示最新缓存修改的统计信息
git stash show

💡 在使用git stash pop代码时,经常会碰到有冲突的情况,一旦出现冲突的话,系统会认为你的stash没有结束。导致的结果是git stash list 中的列表依然存在,实际上代码已经pop出来了。

  • 应用场景
    1. 正常dev分支上开发,紧急bug或者功能来时,可以先将在dev分支做的工作用git stash save存下,然后切换到紧急分支,修复或者完成commit之后,在切回dev分支,用git stash pop将之前的工作内容回复

Git中的其他概念及指令

本地远程跟踪分支 上游分支

从一个远程跟踪分支检出一个本地分支会自动创建所谓的跟踪分支(它跟踪的分支叫做上游分支,即跟踪的是远程仓库的分支); 跟踪分支是与远程分支有直接关系的本地分支; 如果在一个跟踪分支上输入 git pullGit 能自动地识别去哪个服务器上抓取、合并到哪个分支。

ORIG_HEAD

ORIG_HEAD 记录了reset或者merge操作之前的HEAD

应用场景:

  1. 利用ORIG_HEAD撤销resetgit reset ORIG_HEAD
  2. 利用ORIG_HEAD撤销mergegit reset --merge ORIG_HEAD

use --merge flag to preserve any uncommitted changes

FETCH_HEAD

记录了远程所有分支对应的最新的commit

git ls-files -s

查看索引区内容

git cat-file

# 查看git object内容
git cat-file -p <sha1_value>
# 查看git object类型
git cat-file -t <sha1_value>
# 查看git object大小
git cat-file -s <sha1_value>

git objects

有四种类型:blobcommittreetag

通用数据结构为:

Git 时光机,一只调皮的猫

blob

# echo -n输出的内容不换行
echo -n 'Hello, World!' | git hash-object --stdin
b45ef6fec89518d314f546fd6c3025367b721684
# echo -e转义
echo -e -n 'blob 13\0Hello, World!' | openssl sha1
(stdin)= b45ef6fec89518d314f546fd6c3025367b721684

git add .会生成blob对象

Git 时光机,一只调皮的猫

blob对象存储文件内容信息,包含sha1、字节大小、文件内容数据

tree

git commit会生成tree对象(可能有多个)

Git 时光机,一只调皮的猫

tree对象中嵌套tree以来表示文件的嵌套

💡 blob对象不存储文件的名称,文件的名称存储在tree对象中

commit

git commit会生成一个commit对象(包含)、tree对象(可有有多个),不会生成blob对象

Git 时光机,一只调皮的猫

tag

git tag -a tagname会创建一个tag对象,对象中的object会指向某一个committag对象会包含一些作者和时间的信息等等

Git 时光机,一只调皮的猫

💡 git tag <tag_name>不会创建一个tag类型的git object

git references

git中的引用实际上是指向某个commit的指针

Tags

Lightweight tags的引用是指向某个commit类型的git object对象的指针

ANNOTATED tags的引用时指向某个tag类型的git object对象的指针

Branches

Branches的引用是指向某个commit类型的git object对象的指针

HEAD

通常是指向当前分支的引用,但是它也可以指向某个commit类型的git object对象(detached HEAD

git show-ref

# 查看当前所有的tag,约等同与 git tag
git show-ref --tags
# 查看当前所有的分支,约等同与 git branch
git show-ref --heads

git gc

由于git是全量快照,每一次commit都会对应一个版本的全部数据,这样会造成仓库很大,为了解决该问题,引入了压缩算法,举例,git clone在拉取远程仓库时,就会压缩成pack后再传递;在本地执行gc后,也会把.git/objects文件夹下的对象压缩到pack文件夹下。

参考文献

本文发布内容来自个人对于Git-scm网站关于Pro Git book的阅读及实践后的理解,同时参考了Git In-depth视频,文章未经授权禁止任何形式的转载。