目录

常用Git命令行总结

forward

众所周知,Git 是程序员们重度使用的工具,以下对开发过程中常用命令做个总结备忘,也会谈一些简单的技巧使Git更好用。

以下所有命令行不区分平台,windows建议使用GitBash,Linux/MacOS建议使用原生Terminal或第三方终端(item2)。

Git不同版本之间,某些指令支持可能会有差异,尽量用新版本吧。本文撰写时使用的版本为 windows-gitbash的 git-2.31.1 ,和 mac-terminal 的 git-2.32.0

git config

使用git config命令尽心git的配置,常用命令如下:

命令 作用
git config --list 打印出当前环境的git配置信息,
--global可以打印全局配置
git config user.name benjamin git配置username为benjamin,可后接--global
git config user.email xxx@gxx.com git配置email为xxx@gxx.com,可后接--global
git config --global core.autocrlf false windows总提示CR/LF的警告,设置后可去除
git config --global credential.helper store 终端环境保存https的密钥,避免每次都要密码
git config --global core.quotepath false 用来解决git status 显示中文乱码的问题
git-bash需要鼠标右键,
找到选项->文本->本地Locale
设置为zh_CN,而旁边的字符集选框选为UTF-8

当然,除了命令行配置,还可以直接编辑文件~/.gitconfig文件,但是请清楚修改的具体含义,避免出现git后续使用的不必要的错误。

git init

如果不依赖任何远端仓库,而是自行本地新建仓库并初始化,使用git init即可。

示例如下,创建一个名为 MyGitRepo的仓库:

1
2
3
4
cd ~/Documents
mkdir MyGitRepo
cd MyGitRepo
git init

git init 执行后,执行ls -a可以看到多了一个 .git 文件夹,这就是git的主干文件了。

接下来就可以新建文件,然后添加了,以及后续上传到远端。

git clone

然而大多数时候,我们并不需要自己新建,而是需要从远端Git仓库获取已经有的仓库。

不论是Github,还是Gitlab,拿到一个远程git仓库地址后,第一件事就是克隆(下载)到本地主机,git clone命令的作用正在于此,假如现在想克隆Facebook的C++开源库 folly ,仓库地址为:https://github.com/facebook/folly.git,在一个本地目录打开命令行,执行:

1
2
cd ~/Downloads
git clone https://github.com/facebook/folly.git

命令执行完成后即可(如果因为网络失败,则可能需要git加速,或者代理加速了)。

By the way

如果因为网络的原因,clone的速度太慢,或者报错。有2种方法可以尝试:

  • Github的镜像站进行加速

    只需要把https://github.com替换成https://hub.fastgit.org即可。

    上述命令变为:

    1
    
    git clone https://hub.fastgit.org/facebook/folly.git
    
  • Git加速工具 Dev-SideCar工具,Gitee地址:Gitee dev-sidecar

git add/restore

Git操作一个新文件的流程为:工作区(也就是本地文件)–> 暂存区(staged) –> 仓库,如下图所示:

直接基于已有的仓库操作,上文的Folly仓库,新增一个文件,假如是我们学习Folly的笔记。
1
2
3
4
5
6
7
8
9
cd cd ~/Downloads/folly
touch Learn.md
echo "# MultiThread Content" >> Learn.md                #手动写点内容
git status        #Git提示:Learn.md 是"Untracked files",颜色是红色
git add Learn.md  #add到暂寸区
git status        #Git提示:Learn.md 是"Changes to be committed",颜色是绿色
git commit  -m  "new lesson note"  # Git提示:1 file changed, 1 insertion(+),进入仓库
git status        #没有文件提示,会说明当前分支和远端分支的关系
git log -3        #可以看到刚才的一笔提交,处于最前面

有时候,git add想要排除一些文件,除了add之后restore,还可以在添加的时候就排除。示例: git add --all ':!filename1' ':!*.txt'

git add . ':!submoduleNamexx'

说了添加,接下来说下几种程度的撤回,以前都是用checkout,但是后来引入了新词 restore,更能准确体现含义。

命令 作用
git restore <file/dir> 回退掉还没有add的untracked file;
后接文件或目录,一个点表示当前目录所有,慎用
git restore --staged <file/dir> 回退掉已经add,但还没有commit的tracked file;
后接文件或目录,一个点表示当前目录所有

git reset

git reset承担着节点回退的功能,命令示范如下:

命令 命令
git rest HEAD~n 回退掉当前n个节点,回到没有add 的untracked状态(红色);
注意,其实是隐藏了--mixed的参数了,默认就是它;
n一般取1,HEAD~n也可以换成一个具体的commit-ID
git rest --soft HEAD~n 和–mixed的区别是,是回到已经add,但是没有commit的状态(绿色)
git rest --hard HEAD~n 回退的最彻底,连untracked的本地修改也没了,慎用;
如果误用,可以使用git reflog show出历史,然后找回

注意:回退的过程最容易丢代码,虽然 reflog 是一大利器,但是不是永久保存,只有90天的记录。

git branch/switch

git branch,git checkout,以及git switch承担着分支管理的一部分作用,命令示范如下:

命令 作用
git branch 查看本地所有分支,(当前所处分支会绿色高亮)
git branch -va 查看仓库所有分支(包括本地和远端)
git branch -vv 查看当前分支关联的远程分支
git branch --set-upstream=origin/main 设置当前分支与远端的origin/main分支关联
git branch my1.1 V1.1 基于名为V1.1的tag创建一个本地分支my1.1
git branch -D my1.0 删掉名为my1.0的分支
git checkout -b tmpMain origin/main 基于origin/main,创建一个名为tmp的本地分支
origin/main可以不写,则默认基于本地当前分支
git switch feature2 切换到名为feature2 的分支

git checkout

git switch

git fetch/pull

git fetch为获取,目的为拿到远端的改动,存在仓库,不对本地文件和分支做任何改动。

git pull为拉取,实际动作为git fetch + git merge,会拉取到远端改动并合并到当前本地分支。如果有冲突,需要解决冲突。

git commit

git commit即为提交,作用为将已经add到暂存区的文件进行提交,使用方式为git commit -m "commit信息描述",这里要说的是commit的其他参数:

命令 作用
git commit --amend 针对最新的这笔commit,追加内容,形成新的commit
git commit --author="user<email-addr>" 这个是针对某笔commit,可以修改作者;
多个账号提交错误时可以挽救
git commit -am "commit信息描述" 针对还没add的文件,相当于 add+commit 一步到位

git diff

git diff 的命令参数实在是太丰富了,可以用来对比查看本地没暂存(add)的工作区、暂存区的、仓库区的、分支之间的,节点之间的…等等修改.

官方help 信息如下:

1
2
3
4
5
6
usage: git diff [<options>] [<commit>] [--] [<path>...]
   or: git diff [<options>] --cached [<commit>] [--] [<path>...]
   or: git diff [<options>] <commit> [--merge-base] [<commit>...] <commit> [--] [<path>...]
   or: git diff [<options>] <commit>...<commit>] [--] [<path>...]
   or: git diff [<options>] <blob> <blob>]
   or: git diff [<options>] --no-index [--] <path> <path>]

根据个人在日常工作中最常用的命令简单汇总如下:

命令 作用
git diff . 查看当前本地修改的所有文件diff
git diff <指定某个路径文件名> 查看当前本地修改的某个文件diff
git diff --staged <指定某个路径文件名> 查看本地修改了且add了的某个文件diff
git diff SHA1值 SHA2值 --stat 查看2个节点之间的文件差距,文件列表形式给出
git diff SHA1值 SHA2值 <指定某个路径文件名> 查看2个节点之间的某个文件的具体修改点
git diff SHA1值~1 SHA1值 <指定某个路径文件名> 查看SHA值这个commit节点的某个文件的具体修改
举例git diff HEAD~1 HEAD CMakeLists.txt
git diff 分支名a 分支名b --stat 用法类似2个commit节点,只是对比2个分支的HEAD差异

在使用 git diff 这个命令时,通常会有各种教程教配置界面化的 diff 比较工具,但是注意,git diff 本身是 CLI 形式的,所以配置成带UI的是可能会出问题的,带UI的配置命令应该是 git difftool ,而不是 git diff。如果要看左右 compare 形式的 diff 效果,推荐用 vimdiff ,速度快,而且可以配置主题美化,推荐用 github 的 vim 主题。

git restore

git restore 的作用主要是回撤对文件的修改,有点像git checkout 恢复文件的修改,但区别如下:

1
2
3
4
5
6
7
8
# 回撤本地工作区中没有add的文件修改,包括对内容的修改,以及对文件本身的删除、新增
# git checkout -- <文件名> 只能回撤修改,不能回撤对文件的增删
git restore <文件名> 

# 回撤已经add但还没commit的文件修改, -S 是 --staged 的简略形式
git restore -S <文件名> 

# PS: 如果都commit了还想撤回到 added-but-not-commited 或 not-added 状态,参考 git reset

git log

git log的参数实在是多,这里简单的列出常用的几条:

命令 作用
git log -n <branch_name> 查看指定分支log,最新n条修改
不接分支名,都是默认当前本地分支
git log -n --oneline 每条commit只打印一行信息,方便查看,查看前n条
git log -n --author=<author_name> 查看指定作者, 查看满足条件的前n条
git log -n -- <path_or_file> 查看某个指定的文件/文件夹, 查看前n条
git log -n -i --grep="myKeywords" 搜索指定的commit关键词(忽略大小写), 满足条件的前n条
git log -n --graph --abbrev-commit 查看最新n条提交,以树形图列出,且commit-id为前几位
git log --name-status 每次修改的文件列表, 显示状态(新增为A,修改为M)
git log --name-only 只显示每次修改的文件列表,不显示修改状态
git log --stat 每次修改的文件列表, 及文件修改的统计(修改了多少行)

git log 还有其他的很多参数,可以将常用的写作系统alias,或者用git的alias,我习惯用前者,在 ~/.bash_profile 最后追加如下内容:

1
2
alias gitlo='git log --graph --oneline --decorate  --pretty=format:"%C(yellow)%h %Cgreen%ad%Cred %an %Creset%s"  --date=format:"%Y-%m-%d %H:%M:%S" '
alias gitlh='git log --graph --abbrev-commit '

使用的时候直接执行gitlh -3即可。

如果使用git的alias,应该是执行git config --global alias.lh 'log --graph --abbrev-commit',使用的时候执行git lh -3(注意alias定义时单引号紧贴命令,而使用时git和lh之间有空格)

git cherry-pick

git cherry-pick可以摘取其他分支的一个commit,假设commit-ID为 845fead232 ,则执行 git cherry-pick 845fead232即可,如果有文件冲突,那么需要执行git mergetool调用解决冲突的工具,比如Meld,BeyondCompare之类的带UI的工具手动解决,解决冲突后git cherry-pick --continue,至此,已经完成一次成功的commit节点的摘取。

注意 :如果需要cherry-pick多个,建议要按照commit从老到新的顺序摘取,如果commit之间毫无依赖,顺序才可以交换。如果可以压缩,建议压缩后再摘取。

如果要一次cherry-pick多个commit,也是可以的:

1
2
3
4
5
* f939bad 2022-07-19 08:41:09 kissingfire123 add file: effective_mordern_cmake
* 34afa92 2022-07-07 14:32:15 kissingfire123 添加effective-c++系列list提示
* 8069d71 2022-07-06 23:26:20 kissingfire123 <published> 发布effective-c++(4)
* a30651a 2022-07-05 23:09:52 kissingfire123 effective-c++(3) Rule42
* e1345a3 2022-06-26 09:56:27 kissingfire123 <published> 完善友链描述

举例某个我的博客仓库的feature分支如上log,现在如果要一次pick上述 中间3个修改节点 到master分支去,执行下面3种方式都可以:

1
2
3
4
5
6
7
8
# A..B 的形式,不包括左节点A,左开右闭区间
git cherry-pick e1345a3..34afa92
# A~..B 的形式,包括左节点A,左闭右闭区间
git cherry-pick a30651a~..34afa92
# 也可以单独指定多个commit A B C 的形式
git cherry-pick a30651a 8069d71  34afa92
# ⚠️ 上述说的多个节点,顺序必须是老节点写在前面,避免造成无谓的代码冲突
# 上述指令测试的git版本为 2.32.0,更老的版本(特指1.7之前的)可能不支持

git push

git push origin可以推送当前分支的commit修改到同名的远端分支;

git push origin newBranchxx:newBranchxx 可以推送本地一个新建分支到远端,并在远端新建该同名分支origin/newBranchxx;

git push -f origin 可以强行推送本地分支,覆盖远端,一般用于个人随意支配的feature分支,慎用

git push origin --delete branchXX可以删除远程分支origin/branchXX,慎用

git rebase

git rebase的英文描述是 “Reapply commits on top of another base tip”,可以理解为"将分支的基础从一个提交改为另一个提交",和merge相比最大的特点就是可以让分支流更简洁(其实缺点也是这个,丢掉了当初的分支历史);另外,如果团队中有code-review的开发规范,rebase也是必不可少的,减少代码reviewer的负担。

适用场景:从mater分支某个节点A分出了feature分支,然后各自都向前走了几个commit节点,现在feature想要获取master的新修改,这时候可以merge,也可以rebase。

1
2
3
  D --- E  feature
 /
A --- B ---C master

如果是merge,则是如下效果(其中F是merge节点):

1
2
3
  D --- E --- F --- G (feature/master)
 /           /
A --- B --- C 

如果是rebase,则是如下效果:

1
2
A --- B --- C --- D' --- E'
          master       feature

上述D’,E’是表示rebase后commit-ID会变,如果有冲突还得解决。

可以rebase到别的分支(可以是本地或远端,最常用master),比如执行git rebase origin/master ,或者git rebase commit-ID-xx选择rebase到某个分支的某个commit节点。

解决冲突时,执行git mergetool 调用工具手动解决,解决后git rebase --continue,至此,就完成了一次成功的commit。如果中途想放弃rebase,git rebase --abort即可。

  • 常见问题1:rebase 有时候还会带着 submodule 一起,结果出现下述问题

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    Fast-forwarding submodule moduleAxx
    Auto-merging moduleAxx
    error: add_cacheinfo failed to refresh for path 'moduleAxx'; merge aborting.
    hint: Could not execute the todo command
    hint: 
    hint:     pick 4ec7e2de3b00bfc5a258e002b9e5b0efe8756345 update moduleAxx
    hint: 
    hint: It has been rescheduled; To edit the command before continuing, please
    hint: edit the todo list first:
    hint: 
    hint:     git rebase --edit-todo
    hint:     git rebase --continue
    

    正确做法,执行 git rebase --edit-todo ,使用 # 号注释掉 子模块 的pick选项,先处理其他代码的 confilict , 最后再来一起看看子模块的更新,一步到位更新。

git rebase -i(compress)

有时候开发一个功能,需要分步骤多次commit临时修改,最后压缩在一起。

git rebase -i HEAD~n,其中n为数字可以用于压缩最新的n个提交为1个提交,其中HEAD~n可以替换为那个节点具体的commit值。

下方例子为step1,2,3这3个节点压缩成一个。执行 git log -5 --oneline,查看最新5条修改,想合并最新3条。

1
2
3
4
5
6
$ git log -5 --oneline
476a993ee (HEAD -> master) Step3: add const static
2aa0cd976 Step2: add marco
0db8784df step1: new Function.cpp
d6610d607 (origin/master, origin/HEAD) import or backport reinterpret_pointer_cast
1583ff067 Regen github actions (#1614)

执行git rebase -i HEAD~3,则自动跳出vim编辑界面,内容如下(大量#号内容都是注释,不影响commit-msg):

1
2
3
4
5
pick 0db8784df step1: new Function.cpp
pick 2aa0cd976 Step2: add marco
pick 476a993ee Step3: add const static

# Rebase d6610d607..476a993ee onto d6610d607 (3 commands)

vim的操作不多说,第一个pick不要改,把后面的2个pick改成字母's',表示squash:

1
2
3
pick 0db8784df step1: new Function.cpp
s 2aa0cd976 Step2: add marco
s 476a993ee Step3: add const static

vim保存后:wq退出,会跳出第二个vim界面,需要编辑commit-msg信息,默认是3条commit-msg都在,#号注释的就不会算在最终commit-msg中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# This is the 1st commit message:

step1: new Function.cpp

# This is the commit message #2:

Step2: add marco

# This is the commit message #3:

Step3: add const static

注释掉多余的,留下想要的msg,:wq再次退出后即可,此时3条commit压缩成了一个,commit-msg也只有一条。

1
2
3
4
$ git log -3 --oneline
13a004b52 (HEAD -> master) add Function.cpp and support marco,const static
d6610d607 (origin/master, origin/HEAD) import or backport reinterpret_pointer_cast
1583ff067 Regen github actions (#1614)

至此,已压缩成功。

git tag

在版本向前开发推进时,除了维护长期分支master(以及develop分支),对一些重要的Release或Feature节点需要打标签,其实就是对特定的这个commit打了一个锚点,方便问题回溯。

git tag的使用示意如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# -a 0.1.3是增加 名为0.1.3的标签;  -m 后面跟着的是标签的注释
git tag -a 0.1.3 -m “Release version 0.1.3″
# 给某个指定的commit-id (比如 34afa92 )打上tag,也可酌情加 tag-message
git tag -a v1.5.12  34afa92
# 推送本地指定的tag到远端 (建议先推当时的代码节点,再推tag)
git push origin  v1.5.12 
# 推送 本地所有的 标签到远程服务器上,在git push origin代码之后推送
git push origin --tags
# 查看已有tag列表
git tag --list 
# 查看tag以及其message
git tag -n 
# 基于已有的tag建立个名为newBranchV1.2的新分支
git branch newBranchV1.2  v1.2
# 切换到已有Tag,切换到指定tag/branch/commit都是此命令,用git switch也一样
git checkout [tag/branch/commit]
# 删除本地标签的命令
git tag -d 0.1.3
# 删除远端服务器的标签(把本地删除tag动作推送到远端)
git push origin :refs/tags/0.1.3

git stash

日常工作中,临时切换任务是常态了,有时带来的就是分支切换了,但是$\longrightarrow$本地还有修改没有add,切分支是切不过去的,可选的办法有2种:

  1. 将未add的修改add一下,然后commit保存起来;

  2. 不执行add,而是git stash push .指令表示将未add的修改推入一个cache的堆栈,暂存起来;

    等事情忙完后,切换回这个分支,再执行git stash pop即可拿回之前缓存的修改。

    Ps:还可以针对单一文件操作,比如只想暂存 file1 和 file2 ,就执行 git stash push file1 file2,需要注意的是 file1, file2 都必须是已经在git-tracked 状态,git stash的不能是新增的Untracked文件

git submodule

add submodule

执行git submodule add <remote-URL> <local_path>可以添加一个子模块到本仓库,local_path 如果省略,则是添加到当前目录。

🎈注意: local_path的结尾最好是和子模块的仓库名同名,不然仍旧会创建一个子仓库同名的目录。

还是针对上述的folly库,给他手动新增一个第三方图像库作为子模块,添加后自动克隆。

1
2
cd ~/Downloads/folly
git submodule add  https://github.com/nothings/stb.git 3rdImg/stb

根目录下.gitmodules文件的内容自动发生改动:

1
2
3
4
$ cat .gitmodules
[submodule "3rdImg/stb"]
        path = 3rdImg/stb
        url = https://github.com/nothings/stb.git

update submodule

有时候,子模块是远程仓库自带的,第一次克隆下来后需要手动 初始化+同步 ,执行:

1
2
rm -rf  3rdImg/stb/     #试验一下,将已有的先删除
git submodule update --init --recursive    #这时发现文件又全都回来了

如果不是第一次同步子模块,可以不需要--init参数。

子模块其实和文件类似,只是在仓库中,它的修改,是它的commit节点来记录和判别的。

如果子模块向前更新了,主仓库要添加这笔改动的话,像普通文件一样,git add --> git commit --> git push,一套流程带走子模块。

remove submodule

子模块的删除有2种方法,列举如下:

  1. 删除方法一(规范,推荐):git submodule deinit path_to_submodule 是官方文档推荐的做法:

    1
    2
    3
    4
    5
    
    # 举例要删除子模块subModAxxx 
    git submodule deinit  subModAxxx
    git rm subModAxxx 
    # 如果deinit命令再加上 --force 参数,就相当于上面2句
    git commit -m "delete submodule subModAxxx"
    
  2. 删除方法二(野路子,也能做到):

    注意按下述顺序操作:

    • 回退.gitmodules文件的修改,git restore回退之后git add暂存;
    • 删除目录树和索引文件,执行: git rm --cached path_to_submodule,(path_to_submodule替换成实际路径);
    • 删除.git目录的文件记录,执行:rm -rf .git/modules/path_to_submodule;
    • 删除本地文件:rm -rf path_to_submodule,最后git commit上述修改即可完全删除子模块 。

query submodule

对子模块状态的查询,有2个好用的命令。

  1. 其中一个是 git submodule status , 可以用来查看当前本地仓库子模块的实际SHA节点(还没commit);

  2. 另外一个更好用,git ls-tree <SHA值或tag名或HEAD>:目录 | grep 子模块名 可以用来查看这个指定节点的已经commit的子模块节点。

    或者可以省去目录指定,默认根目录,执行 git ls-tree <SHA值或tag名或HEAD> | grep 子模块名

以我博客仓库为例子,用于发布publish的子模块为 kissingfire123.github.io,放置于仓库根目录,则运行如下命令:

1
2
3
4
5
$ git submodule status             
+fd0529c190d63b9d6b2aa58d7467f92f8f6b02ff kissingfire123.github.io (heads/main)
 aa74e340906f29c8fafaf442cb6e1f766fb5df0f themes/LoveIt (v0.2.10-2-gaa74e34)
$ git ls-tree HEAD | grep kissingfire123.github.io 
160000 commit 433feb82a37da65bd2690615485be754515f49fc	kissingfire123.github.io

git grep

git grep 的作用是在仓库指定分支/节点的所有文本中搜索指定的关键字文本,默认是当前节点/分支。

所以可以不用切分支,就直接搜索指定分支下是否包含某个关键文本。

官方用法:

1
2
git grep -h                   
usage: git grep [<options>] [-e] <pattern> [<rev>...] [[--] <path>...

假设我们想看看 ParseInfo 这个接口,在当前仓库分支有没有合入,以及代码状态。

搭配了几个好用且常用的命令参数后,就可以这样搜索:

1
2
3
4
5
6
7
git grep ParseInfo
# -i 是忽略大小写,-n是指定行号
git grep -i -n parseInfo
# --name-only 只显示文本所在的那个文件名
git grep --name-only ParseInfo
# 命令参数后面指定 分支名 或 SHA 值 或 tag 值,可以指定 节点/分支 进行搜索
git grep -i parseInfo  v1.6.3 

other commands

命令 作用
git remote -v 查看仓库的远端地址
git name-rev <commit-id> 根据一个commit-ID确定该节点来自于哪个分支,ID可取前9位
git merge-base --fork-point br1 br2 打印出分支br1和分支br2的最新共同祖先节点SHA值
(共同节点:形状上类似2条单链表的共同节点)
git ls-tree <node>:<dir> 查看某个节点的对应所有子模块的SHA,node可以为主仓库的SHA值或tag名或HEAD,还可以后接 `

参考链接: