Git安装和常用命令(最新)

摘要

Git 安装

dnf / yum 安装

  • 安装简单,缺点是版本较低。CentOS Stream 9 / RHEL 9 请优先使用 dnfyum 多为 dnf 别名)。

1
2
sudo dnf install git -y
# 旧系统仍可使用:sudo yum install git -y

Source 安装

  • 源码编译安装,可以根据需要安装指定的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sudo dnf groupinstall -y "Development Tools"
sudo dnf install -y curl-devel expat-devel gettext-devel openssl-devel perl-devel zlib-devel
# 下载git源码
mkdir -p /usr/local/git/src
cd /usr/local/git/src
# 当前最新版为 2.51.1
wget https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.51.1.tar.gz
tar -zxvf git-2.51.1.tar.gz
cd git-2.51.1
# 编译安装
sudo make prefix=/usr/local/git all
sudo make prefix=/usr/local/git install
# 配置git环境变量
echo 'export PATH=/usr/local/git/bin:$PATH' | sudo tee /etc/profile.d/git.sh
source /etc/profile.d/git.sh
# 测试
git --version

基本概念

Git的5种工作区域

  • 工作目录:用于新增、修改、删除文件,实际我们用于编写代码的目录

  • 暂存区:执行add命令可以将工作目录对应的文件提交到暂存区,只有加入到暂存区的文件才会参与版本控制,其实际为一堆索引文件,保存在.git/objects目录下,记录每个文件的快照(hash)

  • 本地版本库:执行commit命令可以将暂存区的文件提交到本地版本库,每一次提交都会记录版本日志,其实际保存位置也是.git/objects目录下的快照文件,每一次commit如果文件发生变化都会生成新的快照,.git/refs/heads/下记录每个分支的最新一次commit的版本号

  • 远程跟踪区:.git/refs/remotes/origin/下记录每个分支的最新一次更新后的远程版本号,执行fetch\pull\push时都会更新为最新的远程版本号。如果只执行fetch仅仅会更新远程跟踪区,并不会更新本地目录,执行pull命令会同时更新远程跟踪区和本地目录

  • 远程版本库:例如github

文件的状态

  • Untracked:未跟踪的文件,尚未加入过暂存区的文件

1
2
3
4
5
6
7
8
9
10
➜  git:(release) touch a.txt
➜ git:(release) ✗ git status
位于分支 release
您的分支与上游分支 'origin/release' 一致。

未跟踪的文件:
(使用 "git add <文件>..." 以包含要提交的内容)
a.txt

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)
  • 要提交的变更[新增],加入暂存区

1
2
3
4
5
6
7
8
➜  git:(release) ✗ git add .
➜ git:(release) ✗ git status
位于分支 release
您的分支与上游分支 'origin/release' 一致。

要提交的变更:
(使用 "git restore --staged <文件>..." 以取消暂存)
新文件: a.txt
  • 已提交版本库待发布到远端,将暂存区的文件加入本地版本库

1
2
3
4
5
6
7
8
9
10
➜  git:(release) ✗ git commit -m 'add a.txt'
[release 9292bb2] add a.txt
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 a.txt
➜ git:(release) git status
位于分支 release
您的分支领先 'origin/release' 共 1 个提交。
(使用 "git push" 来发布您的本地提交)

无文件要提交,干净的工作区
  • 尚未暂存以备提交的变更,加入过暂存区的文件发生修改

1
2
3
4
5
6
7
8
9
10
11
12
➜  git:(release) echo "hello" >> a.txt
➜ git:(release) ✗ git status
位于分支 release
您的分支领先 'origin/release' 共 1 个提交。
(使用 "git push" 来发布您的本地提交)

尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git restore <文件>..." 丢弃工作区的改动)
修改: a.txt

修改尚未加入提交(使用 "git add" 和/或 "git commit -a"
  • 要提交的变更[修改],加入过暂存区的文件重新加入暂存区

1
2
3
4
5
6
7
8
9
➜  git:(release) ✗ git add .
➜ git:(release) ✗ git status
位于分支 release
您的分支领先 'origin/release' 共 1 个提交。
(使用 "git push" 来发布您的本地提交)

要提交的变更:
(使用 "git restore --staged <文件>..." 以取消暂存)
修改: a.txt

切换分支时值得注意的地方

  • 只要文件没有被commit,无论是新增还是修改,切换分支时,文件的状态都会被带到切换后的分支

  • 所以切换分支前,一定要执行commit

HEAD指针和分支指针

  • 我们在查看git的log时会看到类似于* 6d93a15 (HEAD -> main) message这样的信息,6d93a15就是commit时的版本号,main是分支名称,HEAD就是HEAD指针,实际上这里的main也是一个指针,他就是分支指针

  • 分支指针永远指向当前分支最新的一次提交版本,分支指针对应版本号保存在.git/refs/heads/目录下对应的分支文件中

  • HEAD指针表示我们当前的工作目录是基于哪个版本检出的,通常情况下HEAD指针指向分支指针,但当我们通过命令git switch --detach <commit号>(旧写法:git checkout <commit号>)切换到某个版本时,HEAD指针就不再指向分支指针,这个情况有个名字叫作detached HEAD(头分离)HEAD指针对应的版本号保存在.git/HEAD文件中,这也是为什么我们每次进入项目,git都知道我们当前所在分支或版本号是什么。

  • Git 2.23+ 提示git checkout 仍可用,但切换分支推荐 git switch,恢复/丢弃工作区或暂存区修改推荐 git restore

  • 参考资料:https://www.zsythink.net/archives/3412/

问题与方法

分支名说明:下文命令示例默认主分支为 main(GitHub 等平台现行默认)。若你的仓库仍用 master 或其他名称,请自行替换。mainmaster 仅是分支名,命令用法相同。

1.别人在远程仓库中创建了新的 branch,我本地执行git branch -a却看不到,如何才能看到并切换过去?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 1.先要获取远端全部信息
git fetch origin
# git fetch 远程名称,通过git remote查看,一般就是origin。
git fetch # 也可以不加远程名称
# 该命令无论在哪个分支上执行,都会更新本地所有的远程分支
# git fetch 完成了仅有的但是很重要的两步:
# 1.从远程仓库下载本地仓库中缺失的提交记录
# 2.更新远程分支指针(如 origin/main)
# git fetch 实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。
# git fetch 并不会改变你本地仓库的状态。它不会更新你的 本地 分支,也不会修改你磁盘上的文件。

# 2.再查看分支信息
git branch -a

# 3.创建本地分支并切换到新创建的远程分支
git switch -c release(本地分支名) --track origin/release(远程分支名)
# 旧写法:git checkout -b release origin/release
# fetch 后若本地尚无该分支,也可直接:git switch release(远程分支名)
控制台输出:
分支 'release' 设置为跟踪来自 'origin' 的远程分支 'release'
切换到一个新分支 'release'

# 4.查看当前分支跟踪的远程分支
git rev-parse --abbrev-ref --symbolic-full-name @{u} # @{u} 是 @{upstream} 的简写
控制台输出:
origin/release

2.如何查看本地分支与远程分支的区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.先要获取远端全部信息
git fetch origin
# 或者
git fetch

# 2.切换到待比较的本地分支,如 main
git switch main

# 3.比较当前分支与 origin/main 之间的不同,--stat只显示哪些文件有不同,如果要查看每个文件不同的详细信息就去掉--stat
git diff origin/main --stat

# 4.比较任意两个分支的不同,--stat只显示哪些文件有不同,如果要查看每个文件不同的详细信息就去掉--stat
git diff origin/main..main --stat

3.如何查看本地两个分支之间的区别?

1
2
3
4
5
# 1.比较任意两个分支的区别
git diff main..dev --stat

# 2.比较当前分支与 main 分支的区别
git diff main --stat

4.如何查看本地的发生了哪些更改?

1
2
3
4
5
6
7
8
9
10
11
12
# 当前工作目录的索引和上次提交索引之间的差异,只有已经被commit过的文件才会被比较,如果是新增的文件则看不到
git diff --stat

# 可以通过`git status`命令查看本地都有哪些变化,包含新增未加入暂存区的,新增已加入暂存区的,已提交过但有改动的,等等
git status

# 查看下次执行`git commit`时会被提交的文件(--staged 与 --cached 等价)
git diff --staged --stat
# 或:git diff --cached --stat

# 查看下次执行`git commit -a` 时会被提交的文件,-a表示先add再commit
git diff HEAD --stat

5.如何提交本地仓库?

1
2
3
4
5
6
7
8
# 本地无论是新增文件或修改文件,都要add后才能commit

# 1.先add再commit
git add .
git commit -m 'message'

# 2.add同时commit,注意,如果存在尚未跟踪的文件,需要使用 "git add" 建立跟踪
git commit -a -m 'message'

6.git reset: 如何回滚到指定版本或分支?(有风险,不推荐)

  • git reset的作用是修改HEAD的位置,即将HEAD指向的位置改变为之前存在的某个版本,如果想恢复到之前某个提交的版本,且那个版本之后提交的版本我们都不要了,就可以用这种方法。

  • 注意,如果reset包含了已经发布(git push)的的版本,此时如果用git push会报错,因为我们本地库HEAD指向的版本比远程库的要旧,需要执行 git push --force-with-lease 强制更新远程(比 git push -f 更安全)

  • 注意参数--hard有和没有的区别,有–hard,则完全回退到上一版本,丢弃所有其它修改,清空暂存区,同步工作目录到指定版本。没有–hard,则该版本之后的变化会变为Modified状态保留在工作目录,只清空暂存区。

  • 适用场景

    • 需要把远程 HEAD 回退到某个早期提交,从而删除最近的错误提交记录(适合个人分支或在所有协作者都同意的情况下,对共享分支慎用)。
    • 提交并不包含长期敏感信息,但你想让远程仓库看起来像从未发生过该提交。
  • 优点

    • 真正从分支历史上移除那些错误提交(在被改写的分支上看不到它们)。
    • 简单直接,适合误 push 的最近一次提交。
  • 缺点与风险

    • 改写公共历史:会导致其他开发者本地分支与远程产生分歧,他们必须手动同步(rebase/重置或重新克隆);如果不小心会造成冲突或丢失工作。
    • 如果分支受保护(protected branch),远程可能禁止 force-push。
    • 若提交包含敏感信息,仍可能在其他人的克隆中存在、也可能在远程的缓存/镜像中残留
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 1.回滚到上一个版本
git reset --hard HEAD^ # 完全回退到上一个版本,丢弃所有其它修改,清空暂存区,同步工作目录到指定版本,也就是说,上一次提交之后新增或修改的文件内容都没有了
git reset HEAD~1 # 则该版本之后的变化会变为Modified状态保留在工作目录,只清空暂存区,也就是说,上一次提交之后新增或修改的文件内容得以保留

# 2.回滚到上上个版本
git reset --hard HEAD^^
git reset HEAD~2

# 3.回滚到指定版本的分支
git reset --hard 版本号

# 3.1 查看版本号
git log --oneline

# 4.回滚到指定分支
git reset --hard 分支名称

# 4.1 回滚到与本地远程分支一样的状态,一般本地仓库搞坏了会这么做
git reset --hard origin/main

# 5. `git log`看不到reset的历史,可以通过如下命令查看
git reflog

# reset 后推送,需要强制推送
# 使用更安全的强制推送(优先使用 --force-with-lease)
git push --force-with-lease origin main
# 如果被拒绝,可用 git push --force origin main(但风险更大)

7..gitignore: 在git中如果想忽略掉某个文件,不让这个文件提交到版本库中,要怎么做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 在工作目录下创建 .gitignore 文件
# 其格式为:
# 此为注释 – 将被 Git 忽略
*.a # 忽略所有 .a 结尾的文件
!lib.a # 但 lib.a 除外
/TODO # 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
build/ # 忽略 build/ 目录下的所有文件
doc/*.txt # 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt

# 忽略文件默认为当前项目目录下的.gitignore文件,也可以通过如下命令指定文件路径和名称
git config core.excludesfile .gitignore_dev

# 也可配置为全局文件,这样就不需要为每个项目都创建.gitignore 文件
git config --global core.excludesfile ~/.gitignore_global


# .gitignore 文件 只对git add起作用,如果有文件在被加入到.gitignore 文件前就已经被commit了可以使用如下方法
# 1.删除暂存区、分支上内容,本地保留。解除该文件的追踪关系,脱离版本控制。
git rm --cached 文件名 # 删除文件
git rm -r --cached 文件夹 # 删除文件夹 -r 表示允许递归删除
git rm -r --cached . # 删除当前目录下全部文件的暂存区

git rm -r 文件夹/文件名 #删除本地、暂存区、分支上内容,如果该文件不需要在本地保留,就可以测底删除

# 2.重新加入暂存区
git add . # 将当前目录下所有文件加入暂存区
git add file/dir #将指定文件或目录加入暂存区,支持通配符

# 3.查看暂存区内容(--staged 与 --cached 等价)
git diff --staged

# 4.提交
git commit -m "message" # 将暂存区内容提交到本地版本库
git commit -am "message" # 先提交暂存区再提交到本地版本库

8.git commit --amend: commit后发现有内容要修改或者注释写错了,但是不想创建新的一次commit,要怎么办呢?

  • 以下命令如果直接合并到已经push过的版本,再次 git push 时会提示「更新被拒绝,因为您当前分支的最新提交落后于其对应的远程分支」,此时应使用 git push --force-with-lease(比 git push -f 更安全;仅在个人分支或团队同意改写历史时使用)。

1
2
3
4
5
# 覆盖上一次提交,这样不会产生新的提交
git commit --amend -am "注释"

# 在上次提交中附加一些内容,保持提交日志不变
git commit -a --amend --no-edit

9.git log–如何查看提交日志?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 显示版本历史,如果有用git reset --hard xxxxx回退操作,则只会显示到xxx之前的历史
git log # 显示当前分支之前的全部日志
git log --all # --all 显示全部分支
git log --oneline # 单行显示

git log --oneline --all --graph # 图形化显示全部分支log

# 在所有提交日志中搜索包含「homepage」的提交
git log --all --grep='homepage' #模糊匹配

# 获取某人的提交日志
git log --author="hanqf" #模糊匹配

# 查看完整版本历史,也就是说即便有git reset也会显示
git reflog

10.如何创建本地仓库并绑定到远程仓库?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1.首先要在对应的git服务器创建一个新的仓库,一般的git服务器创建新仓库后都会提示你如何绑定该仓库的

# 2. 创建本地仓库
git init #将当前目录加入版本控制
git init dir #将dir加入版本控制
# 默认分支名可用 init.defaultBranch 设为 main(GitHub 等平台默认)
# 旧版 Git 默认名为 master,二者仅是分支名
git config --global init.defaultBranch main
# 已 init 的仓库若要改名:git branch -m main

# 3. 创建新的文件后提交到本地仓库
git add . && git commit -m 'message'
git commit -am "备注"

# 4.绑定远程仓库
git remote add origin https://xxxxx (远程仓库地址)

# 5.提交本地仓库的变更到远程仓库(GitHub 等默认主分支为 main)
git pull --rebase origin main #获取远程库与本地同步合并(如果远程库不为空必须做这一步,否则后面的提交会失败)
git push -u origin main #第一次推送加上 -u,以后可直接 git push
git push origin main
# 或者
git push # 将当前分支提交到其对应的远程仓库
  • 以下是绑定到GitHub仓库的步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
# 推送一个全新的仓库
echo "# docker_test" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/hanqunfeng/docker_test.git
git push -u origin main

# 推送一个现存的仓库
git remote add origin https://github.com/hanqunfeng/docker_test.git
git branch -M main
git push -u origin main

11.远程仓库地址变更后如何更新?

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1.命令行修改
git remote set-url origin [NEW_URL]

# 2.手工编辑.git目录下的config文件
[remote "origin"]
url = https://xxxxxx (修改为新的地址)
fetch = +refs/heads/*:refs/remotes/origin/*

# 3.查看修改后的地址
git remote get-url origin #只会显示可以fetch的地址

# 4.查看远程仓库地址
git remote -v # 会显示push和fetch的地址

12.如何将本地仓库同时绑定到多个远程仓库?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.按照`10.如何创建本地仓库并绑定到远程仓库?`中的步骤完成第一个仓库的绑定

# 2.手工编辑.git目录下的config文件
[remote "origin"]
url = https://xxxxxxxx1 # 此处为第一个远程仓库的地址,即可以push又可以fetch
url = https://xxxxxxxx2 # 在此添加第二个仓库的地址,以此类推,可以添加多个,只可以push,只能备份使用
fetch = +refs/heads/*:refs/remotes/origin/*

# 3.保存后再次执行(主分支名按仓库实际为准,GitHub 一般为 main)
git push -u origin main
# 或者
git push # 将当前分支提交到其对应的远程仓库

# 4.查看远程仓库地址
git remote -v

13.如何在git pull时不用每次都输入凭据?

  • 注意:GitHub 自 2021 年 8 月起已禁用账号密码进行 HTTPS 操作,需使用 Personal Access Token(PAT) 或改用 SSH(见第 17 节)。

  • 不推荐 credential.helper store:凭据会以明文保存在 ~/.git-credentials

  • 推荐按平台选择凭据助手:

1
2
3
4
5
6
7
8
9
10
11
# macOS(钥匙串)
git config --global credential.helper osxkeychain

# Windows / 跨平台(推荐)
# 安装 Git Credential Manager 后通常自动配置

# Linux(示例,需安装 libsecret)
git config --global credential.helper /usr/lib/git-core/git-credential-libsecret

# 配置后执行一次 git pull,按提示输入用户名和 PAT(非登录密码)
git pull
  • 若仍使用 store(仅个人机器、且了解安全风险时):

1
2
git config --global credential.helper store
git pull # 首次输入用户名与 PAT,之后从文件读取

14.如果文件已经git add到暂存区,但是尚未commit,此时如何将文件从暂存区中移除?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 如果文件还没有放入暂存区(丢弃工作区修改,回到 HEAD 版本)
git restore <文件名>
# 旧写法:git checkout -- <文件名>

# 如果文件已经`git add`到暂存区,但是尚未commit
# 移出单个文件
git restore --staged <文件路径>

# 移出本次全部 add 到暂存区的文件
git restore --staged .
# 旧写法:git reset .

# 可以通过git status查看文件路径
git status

# 如果已经commit,可以通过如下方式移除
# 删除暂存区、分支上内容,本地保留。解除该文件的追踪关系,脱离版本控制。
git rm --cached <文件名> # 删除文件
git rm -r --cached <文件夹> # 删除文件夹 -r 表示允许递归删除
git rm -r --cached . # 删除当前目录下全部文件的暂存区,.代表当前目录

15.git config: 如何设置和查看git配置信息?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 使用git config命令进行设置和查看

# 全局设置用户名和邮箱,全局配置是保存在 ~/.gitconfig中的
git config --global user.name "你的用户名"
git config --global user.email "你的邮箱"

# 只对当前项目有效,项目根目录下执行,去掉--global,项目配置优先级更高,项目配置保存在.git/config中,可以直接修改
git config user.name "你的用户名"
git config user.email "你的邮箱"

# 修改你的用户名和邮箱
git config --global --replace-all user.name "你的用户名"
git config --global --replace-all user.email "你的邮箱"

# 查看配置
git config --list # 此时项目配置和全局配置都会显示

git config --global --list # 只显示全局配置,全局配置是保存在 ~/.gitconfig中的

git config user.name # 查看某个配置的值

# 修改全局配置的值,--replace-all会匹配所有行,也可以直接在~/.gitconfig中修改
git config --global --replace-all user.name "你的用户名"
git config --global --replace-all user.email "你的邮箱"

# 修改项目配置的值,--replace-all会匹配所有行,也可以直接在.git/config中修改
git config --replace-all user.name "你的用户名"
git config --replace-all user.email "你的邮箱"

16.提交的文件太大,导致 push 失败怎么办?

  • 现代 Git(2.9+)很少需要调大 http.postBuffer;推送失败更常见原因是单文件超过远端限制(如 GitHub 单文件约 100MB 需用 LFS)。

  • 推荐:大文件使用 Git LFS,不要把大二进制直接提交进普通 Git 历史。

1
2
3
4
5
6
7
8
9
# 安装并初始化 Git LFS(各平台包管理器或官网安装)
git lfs install

# 跟踪某类大文件(示例:*.zip)
git lfs track "*.zip"
git add .gitattributes
git add large.zip
git commit -m "add large file via LFS"
git push
  • 若确认为 HTTP 传输层问题(少见),可尝试(通常不必全局设置):

1
git config http.postBuffer 524288000   # 单位 B,约 500MB

17.clone代码最简单的方式是通过https的形式,不过一般这样做需要输入用户名和凭据,如果不想每次输入可以使用git@xxxx的 SSH 形式,如何实现呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 基于 SSH 密钥,无需每次输入密码
# 推荐 ed25519(更短、更快、更安全);RSA 仍可用但非首选
ssh-keygen -t ed25519 -C "your_label"
# 生成 ~/.ssh/id_ed25519 与 id_ed25519.pub

# 若服务器仅支持 RSA,可使用:
# ssh-keygen -b 4096 -t rsa -C "your_label"
# -C 仅为备注标签,便于在服务器上辨识,不必是邮箱

# 将公钥内容(cat ~/.ssh/id_ed25519.pub)复制到 Git 服务器的 SSH 公钥设置页
# Github: Settings → SSH and GPG keys → New SSH key
ssh -T git@github.com

# Coding: 个人设置 → SSH 公钥
ssh -T git@e.coding.net

# Gitee: 设置 → 安全设置 → SSH公钥
ssh -T git@gitee.com

# clone 示例
git clone git@github.com:hanqunfeng/reactive-redis-cache-annotation-spring-boot-starter.git

18.git branch / git switch: 如何创建分支\切换分支\查看分支\删除分支\发布分支?

  • Git 2.23+:切换分支用 git switch,检出提交(头分离)用 git switch --detach;下文标注「旧写法」的 git checkout 仍广泛可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 从当前分支创建 release 分支,但不切换
git branch release

git switch release # 切换分支(旧:git checkout release)
git switch --detach <commit号> # 检出某提交,头分离(旧:git checkout <commit号>)

# 从当前分支创建并切换(旧:git checkout -b release)
git switch -c release

# 从 dev 分支创建 release,但不切换
git branch release dev
# 从 dev 创建并切换
git switch -c release dev # 旧:git checkout -b release dev

# 重命名当前分支
git branch -m release
git branch -M release # 目标名已存在时强制覆盖

# 查看分支
git branch # 本地分支,* 为当前分支
git branch -r # 远程分支(旧:git branch --remote)
git branch -a # 本地 + 远程

# 从指定提交创建 release
git branch release 799fb04
git switch -c release 799fb04 # 旧:git checkout -b release 799fb04

# 从远程 origin/release 创建本地分支并切换
git switch -c release --track origin/release
# 旧:git checkout -b release origin/release
# fetch 后也可:git switch release

git switch - # 切回上一分支(旧:git checkout -)

cat .git/HEAD
git symbolic-ref HEAD

# 删除分支(不能在当前分支上删自己)
git branch -d release
git branch -D release # 未合并时强制删除

# 发布新分支并建立上游跟踪(推荐)
git push -u origin <new branch name>
# 或分步:
git push origin <new branch name>
git branch -u origin/<new branch name> # 旧:--set-upstream-to=...

git rev-parse --abbrev-ref --symbolic-full-name @{u}

19.merge还是rebase? 如何合并分支?

一般开发流程:

  1. 更新主分支 git pull

  2. 从主分支创建一个开发分支 git switch -c dev(旧:git checkout -b dev),每个开发人员都会创建一个自己的开发分支

  3. 开发分支完成测试后合并回主分支,这里推荐使用git rebase,开发分支的log会被移动到主分支的顶端,这样主分支始终是一条线

  4. 这样主分支在保持干净的同时还保留了每一次commit的log,便于开发人员追溯历史

  5. 也可以使用git merge --no-ff,其也会保留每次的log到主分支上

一般发布流程:

  1. 将主分支测试完成后合并到发布分支,一般都是由一个专门的人员进行合并,此时推荐使用git merge,这样每次一发布的版本都会产生一个新的节点,而主分支上的多次commit的log不会被记录到发布分支上

  2. 这样发布分支看起来就是每一次大的发布才会合并一次并记录log,log中可以编辑本次发布的内容。

示例准备,初始化一个待合并的目录

以下终端示例在本地使用分支名 master 演示,与 main 等价;切换分支处已用 git switch(旧写法为 git checkout)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
mkdir git_test
cd git_test
touch 1.txt
➜ git init
[master(根提交) 5b91fc1] init...
1 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 1.txt
➜ git:(master) git add . && git commit -m 'init...'
➜ git:(master) git switch -c dev
➜ git:(dev) echo "dev1" >> a.txt
➜ git:(dev) ✗ git add . && git commit -m 'dev1'
[dev 8d57739] dev1
1 file changed, 1 insertion(+)
create mode 100644 a.txt
➜ git:(dev) ✗ git switch master
切换到分支 'master'
➜ git:(master) ✗ echo "master1" >> a.txt
➜ git:(master) ✗ git add . && git commit -m 'master1'
[master fa2400a] master1
1 file changed, 1 insertion(+)
create mode 100644 a.txt
➜ git:(master) ✗ git switch dev
切换到分支 'dev'
➜ git:(dev) ✗ echo "dev2" >> a.txt
➜ git:(dev) ✗ git add . && git commit -m 'dev2'
[dev 0006560] dev2
1 file changed, 1 insertion(+)
➜ git:(dev) ✗ git switch master
切换到分支 'master'

merge

将dev分支merge到当前分支

1
git merge dev
  • merge会将dev分支和当前分支合并后创建一个新的节点放到当前分支最顶端,所以解决完冲突,需要执行如下命令创建一个新的commit

1
git add . && git commit -m 'merge dev'
  • merge的log会按照时间顺序显示

  • merge后如果删除了dev分支,则在log中就看不到这个分支信息了,但是日志内容还在

merge示例
1
2
3
4
5
6
7
8
9
➜ git:(master) ✗ git merge dev
冲突(add/add):合并冲突于 a.txt
自动合并 a.txt
自动合并失败,修正冲突然后提交修正的结果。
➜ git:(master) ✗ vim a.txt
➜ git:(master) ✗ git add . && git commit -m 'master merge release'
➜ git:(master) git log --oneline # 此时才能看到日志
➜ git:(master) git branch -D dev
已删除分支 dev(曾为 0006560)。


删除dev分支后

rebase

将dev分支rebase到当前分支

1
git rebase dev
  • rebase会将dev分支的每一个节点转移到当前分支最顶端,而不会产生一个新的节点,这样rebase后的分支节点看起来就是一条线

  • 解决完冲突,执行git add .git rebase --continue,不会产生额外的commit

  • rebase后如果删除了dev分支,则在log中就看不到这个分支信息了,但是日志内容还在,这样看起来就是主分支自己的节点,没有产生过新的分支

  • 如果要放弃本次合并,可以运行git rebase --abort

rebase示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  git:(master) git rebase dev
冲突(add/add):合并冲突于 a.txt
自动合并 a.txt
error: 不能应用 0657382... master1
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
不能应用 0657382... master1
➜ git:(e4410b4) ✗ vim a.txt
➜ git:(e4410b4) ✗ git add .
➜ git:(e4410b4) ✗ git rebase --continue
[分离头指针 bd01ea0] master1
1 file changed, 1 insertion(+)
成功变基并更新 refs/heads/master。
➜ git:(master) git log --oneline
➜ git:(master) git branch -D dev
已删除分支 dev(曾为 e4410b4)。


删除dev分支后

20.git rebase -i: 我commit了很多次,发现都是干的一件事,此时如果直接rebase到主分支会有很多没必要的log,是否可以合并这些commit为一个呢?

1
2
3
4
5
6
7
# git rebase -i 即可以合并多次提交,也可以合并分支,-i表示可以编辑提交过程,编辑过程中将后面的提交命令修改为s即可,保存后会提示你是否重新编辑log内容,按需编写即可。
git rebase -i commit号 #将commit号到最新的提交合并为一个提交,需要在弹出的交互页面中编辑合并过程
git rebase -i start_commit号 end_commit号 #合并指定commit号之间的的提交
git rebase -i HEAD~3 #合并最新的三次提交,编辑页面将后两次命令修改为s,wq后编辑新的commit

# 该命令也可以合并分支
git rebase -i 分支名称 #将指定分支合并到当前分支,合并时将指定分支的多个提交合并为一次提交,需要在弹出的交互页面中编辑合并过程
  • 如果修改的提交节点距离结束的提交节点中间有多个节点,而上一次和下一次都需要合并文件,这个过程就要进行多次。个人感觉这个过程虽然可以重新整理提交节点,使节点更准确清晰,但是如果修改的点比较远,文件内容变化复杂,这个多次合并的过程还是比较痛苦的,不推荐在这种情况下使用。

  • 推荐的使用场景为,针对同一个功能进行修改,在commit后尚未进行pushsa时发现有些内容需要变化,如果此时没有进行commit,可以执行git commit --amend -am "注释",如果已经执行了commit,则可以执行git rebase -i HEAD~2

示例

1
git rebase -i HEAD~3
  • 注意这里会进入两次编辑页面,一次是多个提交的处理方式,按提示进行修改即可,一般保留第一行的命令pick,后面的行命令修改为s,然后wq保存。

  • 注意合并多个分支时如果包含已经发布过的分支,就要调整顺序,将已经发布的最后一个分支放到第一行,否则再次发布时会提示需要先执行git pull,这是因为按照上面的逻辑只有第一行的是提交操作,后面的不会产生新的提交,所以版本号就会落后于远端分支,但是即便调整顺序,也会提示版本偏离,还是要先执行git pull,待手工合并冲突后再次提交一个新的commit,所以,最好的方法就是不要包含已经发布过的分支,只针对未发布的分支进行合并。
    编辑后的:

  • 第二个页面是要求你编辑本次合并的log说明,此时每次的log都会显示在这个页面,去掉不需要的,重新编辑一下保存即可。

    编辑后的:

查看日志:git log --oneline

21.git revert: 如何撤销指定的提交呢,就是把这次提交回滚到其前一次提交的状态,但是又不影响其之后的提交?

  • git revert 是回滚某个commit ,不是回滚“到”某个

  • git revert是用一次新的commit来回滚之前的commit,git reset是直接删除指定的commit。

  • git reset 撤销到某次提交, git revert 撤销某次提交,撤销并不意味着删除本次提交,其log里仍然会有这次提交,只不过revert后会产生一个新的节点用于提交,而reset是会删除之前的log。

  • 需要回滚到上一个版本时,可以通过git reset HEAD~1实现,也可以通过执行git revert HEAD实现。区别是 reset 后再次发布通常需要 git push --force-with-lease,而 revert 只需普通 git push,因为 revert 会在顶端产生新提交。

  • 还有一点需要注意,例如 dev 是从 main 分支 git switch -c dev 拉出的,然后针对 dev 执行 reset 时,若 reset 的版本 main 里也包含,再将 dev merge 回 main 时,main 里可能仍保留本应变更为 reset 的内容;revert 则通过逆向 commit「中和」原提交,一般无此问题。

  • 适用场景

    • 想“撤销”某次/若干次提交的改动,但不想改写历史(适合共享分支、多人协作、受保护分支)。
    • 提交不是敏感信息(即不需要从历史中物理删除内容),只是代码不该存在。
  • 优点

    • 不改写历史,安全,对协作影响最小。
    • 无需其他同事强制同步;已有的提交记录仍然完整且可审计。
  • 缺点

    • 撤销是通过新增一个反向提交实现,历史中仍保留「错误提交」的内容(因此若包含秘密/密码,这种方法不够)。
1
2
3
4
5
6
7
8
9
10
11
12
# 撤销并commit一次新的提交
git revert HEAD #撤销前一次 commit
git revert HEAD^ #撤销前前一次 commit
git revert HEAD~3 #撤销倒数第四次提交
git revert commit号 #撤销指定的版本,撤销也会作为一次提交进行保存。

# 如果只撤销,即只改变本地目录和索引,但不执行commit,可以加上-n参数,然后手工执行commit操作
git revert -n HEAD~3
git revert -n <commit1>..<commit2> # 撤销多个版本,commit1 > x >= commit2,此时要加上-n参数,否则每个撤销都会创建一个新的commit号

# 推送到远程(常规推送,不需强制)
git push origin main

22.从整个历史彻底删除敏感内容:git filter-repo

  • 适用场景

    • 提交中包含 密码/密钥/证书/私人信息,必须彻底从仓库历史中移除(不仅是最新提交)。
    • 需要在所有历史提交中删除某个文件或某类信息。
  • 优点

    • 可以把敏感数据从历史中彻底移除(在多数场景下有效)。
  • 缺点与风险

    • 改写历史 — 需要全员协调并强制推送。所有开发者必须重新克隆或手动重写本地历史。
    • 即使在仓库历史中删除,也不能保证已经被第三方(例如 CI 缓存、备份、GIT 镜像、搜索引擎快照)抓取到的副本被删除。若包含密钥/密码,务必更换(rotate)这些凭据。
    • 要求在远程(比如 GitHub/GitLab)上有足够权限,并可能需要删除/重新创建受保护分支或清理缓存(例如 GitHub 的“清理缓存”步骤或联系支持)。
  • 安装 git-filter-repo

1
2
3
git clone https://github.com/newren/git-filter-repo.git
cd git-filter-repo
sudo cp git-filter-repo /usr/local/bin/ # macOS/Linux
  • git filter-repo 示例(删除文件名为 secret.txt 的历史)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 先备份或克隆一份仓库以防万一
git clone --mirror git@host:repo.git repo-mirror.git
cd repo-mirror.git

# 使用 git-filter-repo 删除文件(需先安装 git-filter-repo)
git filter-repo --path secret.txt --invert-paths
# 后缀替换敏感字符串
git filter-repo --replace-text replacements.txt
# replacements.txt 文件格式:
<要替换的字符串>==><替换后的字符串>
# 比如
abc123secret==>REMOVED
password123==>REMOVED
PRIVATE_KEY==>REMOVED

# git filter-repo 运行结束后,你的 Git 历史已经 被“改写”并直接写入为新的 commit 树,不需要再执行 git add 或 git commit。

# 把清理后的仓库强制推送回远程
## --all 表示:推送所有本地分支到远程。
git push --force --all
# --tags 表示:推送所有标签(tags)到远程。
git push --force --tags
方法 是否改写远程历史 协作影响 是否移除历史中的文件/内容 适用场景
git revert 低(安全) 撤销错误改动,不含敏感信息
git reset + git push --force-with-lease 是(部分历史) 高(需通知) 是(移除最近提交) 误 push 最近提交,团队允许改写
git filter-repo 是(全历史) 非常高(需全员协同) 是(彻底移除历史) 包含敏感信息,必须从历史消除
git rm --cached + commit 否(历史仍包含旧版本) 停止跟踪当前文件,但不需删除历史

23.如何打tag

1
2
3
4
5
6
7
8
9
git tag -a "prod_x.x.x" -m "message" #打标签, -a是标签名 -m注释
git push origin "prod_x.x.x" #将这个新标签推送到远程仓库

git tag #查看标签信息
git show prod_x.x.x #显示被打标签的commit的详细信息

git switch -c hotfix_x.x.x prod_x.x.x # 从标签版本开修复分支(旧:git checkout -b ...)
git push -u origin hotfix_x.x.x
# 若未用 -u 推送,可事后:git branch -u origin/hotfix_x.x.x hotfix_x.x.x

24.git pull = git fetch + git merge这种说法对吗?

  • 默认情况下,git pull = git fetch + git merge

  • 如果配置了 pull.rebase=true,那么 git pull 不再执行 merge,而是执行 rebase

1
2
3
4
5
6
7
8
9
10
11
# 设置为 rebase,--global 表示全局配置
git config --global pull.rebase true

# 恢复默认行为为 merge
git config --global pull.rebase false

# 查看当前配置
# 检查全局配置
git config --global pull.rebase
# 检查当前仓库配置
git config pull.rebase
  • 其实更好的比较是git pull --rebasegit fetch + git merge的区别,其相当于git fetch + git rebase

为了说清楚这个问题,我们需要先明白git的三个仓库:

  1. 本地仓库:如 main,修改代码、add、commit 即写入本地仓库

  2. 本地远程跟踪仓库:如 origin/main,不能直接 commit,保存上次从远端拉取的状态

  3. 远程仓库:Git 服务器(如 GitHub)

  • git fetch 将远端更新同步到 origin/* 跟踪分支;在 main 上执行 git merge origin/main 会把远端变更合并进本地并可能产生新提交。

  • git pull = git fetch + git merge(或配置了 pull.rebase 时为 fetch + rebase)。

25.如何确定开发–>测试–>发布流程?

开发A模型:基于主分支(main/master)的开发模型

  • 使用 git fetch + git rebase origin/main(或 git pull --rebase)将远程内容合并到当前主分支,保持线性历史

  • 模型结构简单,始终记住使用 git fetch + git rebasegit pull --rebase 更新主分支

开发B模型:基于dev的子分支开发模型

  • 增加了一个 dev 子分支,合并回主分支时可通过 git rebase -i 将多个 commit 压成一个

  • 每次开发创建新分支,完成后删除;紧急任务可从主分支 git switch -c hotfix/xxx 拉出子分支

  • 使用 git pull --rebase 更新远程,用 git rebase dev 合并子分支,目标仍是主分支历史尽量成一条线

测试模型:基于 A/B 模型的主分支

  • 开发完成后,从主分支 git switch -c release 拉出测试分支

  • 测试过程中如有 bug,开发人员按开发模型修复后 merge 到 release

  • 测试通过后,将 release merge 到 prod(生产分支,首次可从 release 创建)

  • release 仅为本次发布服务,发布后可删除

发布模型

  • release 测试通过后 merge 到 prod,上线前为 prod 打 tag

  • 只有打了 tag 的代码才能部署;线上 bug 从对应 tag git switch -c hotfix/xxx <tag> 开修复分支

  • 修复后从 hotfix 拉 hotfix_release 测试,流程同测试模型

  • 测试通过后按发布模型上线,并将 hotfix_release merge 回主开发分支,删除临时 hotfix 分支

说明

  • 长期保留的通常是主开发分支(main/master)与 prod 发布分支

  • 每次上线前一定要打tag,tag对应的就是当前生产环境

  • 测试分支和修复分支根据需要进行创建,用后可以删除。

26.git stash: 我已经修改了本地的代码,但是还没有commit,但是又想切到其他分支,不想丢弃修改,也不想把这些修改带到其它分支,因为代码还有用,当切回时还需要恢复这些修改,那么该怎么处理?

  • git stash可以做到这一点,其会将工作区中的更改(包括未暂存和已暂存的文件)保存到一个临时区域(称为 “stash 栈”),并将工作目录恢复为干净的状态。

  • 其特点是非破坏性操作,改动不会丢失,可以随时取回,这在切换分支或处理紧急任务时非常有用。

创建一个 stash

  • 将当前分支上所有未提交的更改(包括工作区和暂存区的内容)保存到 stash 栈中。

  • 执行后,工作目录会变得干净(与最近的一次提交一致)。

1
2
3
4
5
6
7
8
# 创建 stash
$ git stash
保存工作目录和索引状态 WIP on main: c4f0eaf 2024-12-05 11:43:57#brew

# 创建带描述的 stash(git stash save 已弃用,请用 push -m)
$ git stash push -m "描述信息"
保存工作目录和索引状态 On main: 描述信息

查看 stash

  • 查看当前的 stash 栈,包括每个 stash 的编号、提交信息、日期和时间等。

1
2
$ git stash list
stash@{0}: WIP on main: c4f0eaf 2024-12-05 11:43:57#brew

恢复 stash

  • 从 stash 栈中恢复最近一次保存的 stash,但 不会删除 stash。

  • 执行后,工作目录会恢复到 stash 保存的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 恢复最近一次保存的 stash
$ git stash apply
位于分支 main
您的分支与上游分支 'origin/main' 一致。

尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git restore <文件>..." 丢弃工作区的改动)
修改: source/_posts/git-command.md

修改尚未加入提交(使用 "git add" 和/或 "git commit -a"

# 恢复特定的 stash
$ git stash apply stash@{1}

删除 stash

1
2
3
4
5
6
7
8
9
10
# 删除最近一次的 stash
$ git stash drop
丢弃了 refs/stash@{0}(6f483ee0c0753762a497d24f424c8998372e96a9)

# 如果需要删除特定的 stash(非顶部的),需要指定索引(stash@{n})
$ git stash drop stash@{1}
丢弃了 stash@{1}(50a9e83d3bf96e2f3ca5368d2a248113874507b9)

# 删除所有 stash
$ git stash clear

恢复并删除 stash

  • 等价于 git stash apply + git stash drop

1
2
3
4
5
6
7
8
9
10
11
12
13
# 恢复并删除最近一次的 stash
$ git stash pop

位于分支 main
您的分支与上游分支 'origin/main' 一致。

尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git restore <文件>..." 丢弃工作区的改动)
修改: source/_posts/git-command.md

修改尚未加入提交(使用 "git add" 和/或 "git commit -a"
丢弃了 refs/stash@{0}(63a7551a9bc920f82802cbda542da99800b8a58c)

注意事项

  • 如果有未追踪的文件,需要用 git stash -ugit stash --include-untracked 才会保存。

典型的使用场景

  • 1.假设当前分支有未完成的更改,但需要切换到其他分支紧急处理一些问题

1
2
3
4
5
6
7
8
# 暂存未完成的更改
git stash
# 切换到要处理问题的分支
git switch 其他分支
# 切换回原来的分支
git switch 原分支
# 恢复未完成的更改
git stash pop
  • 如果 stash 恢复时遇到冲突,比如文件被修改了,则需要手动解决冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 恢复 stash 是遇到冲突
$ git stash pop
错误:您对下列文件的本地修改将被合并操作覆盖:
source/_posts/git-command.md
请在合并前提交或贮藏您的修改。
正在终止
位于分支 main
您的分支与上游分支 'origin/main' 一致。

尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git restore <文件>..." 丢弃工作区的改动)
修改: source/_posts/git-command.md

修改尚未加入提交(使用 "git add" 和/或 "git commit -a"
贮藏条目被保留以备您再次需要。

# 先将修改提交到缓存区
$ git add .

# 再次恢复 stash,但要注意,此时虽然是 pop 操作,但是 stash 并没有被删除,需要手工删除
# 恢复后冲突会被合并到有冲突的文件上,此时需要手工解决
$ git stash pop
自动合并 source/_posts/git-command.md
冲突(内容):合并冲突于 source/_posts/git-command.md
位于分支 main
您的分支与上游分支 'origin/main' 一致。

未合并的路径:
(使用 "git restore --staged <文件>..." 以取消暂存)
(使用 "git add <文件>..." 标记解决方案)
双方修改: source/_posts/git-command.md

修改尚未加入提交(使用 "git add" 和/或 "git commit -a"
贮藏条目被保留以备您再次需要。

# 查看 stash
$ git stash list
stash@{0}: WIP on main: c4f0eaf 2024-12-05 11:43:57#brew

# 删除 stash
$ git stash drop
  • 2.本地修改了部分文件,但此时需要更新远程仓库,此时需要先将本地修改的文件暂存,再更新远程仓库,最后再恢复本地修改的文件。

1
2
3
4
5
6
7
8
9
10
# --autostash:
# 如果当前分支有未提交的更改(工作区或暂存区),Git 默认会拒绝执行 rebase 或 pull 操作。
# --autostash 会自动将未提交的更改临时存储到一个 Git "栈"(stash)中。
# 执行完成后,这些更改会自动还原到原来的状态。
# 整体过程:
# 创建一个临时 stash。
# 完成 pull 操作(包括 rebase)。
# 恢复之前的 stash。
# 避免手动执行 git stash 和 git stash pop 的麻烦。
git pull --rebase --autostash

27.如何创建PR

  • PRPull Request(拉取请求),是 GitHub 上代码协作的核心机制。

简单来说:你提交了一些代码,希望别人审核并合并到目标分支,于是发起一个 PR。

PR 的工作流程

  • 假设项目有一个主分支:

1
main
  • 你要开发一个新功能:

1
2
main
└── feature/login
    1. 创建功能分支
1
git switch -c feature/login
    1. 开发并提交
1
2
git add .
git commit -m "feat: add login page"
    1. 推送到 GitHub
1
git push -u origin feature/login
  • 推送成功后,GitHub 通常会提示:

1
Compare & pull request
  • 点击即可创建 PR。

  • 也可以通过命令创建 PR

1
gh pr create

PR 的本质

  • PR 表示:

1
2
3
4
5
6
请将
feature/login

合并到

main
  • 审核者可以:

1
2
3
4
5
查看代码变更
评论代码
提出修改意见
通过 CI 检查
最终合并

在 GitHub 网页创建 PR

  • 进入你的仓库:

1
2
3
4
5
点击 Pull requests
点击 New pull request
选择:
base: main
compare: feature/login
  • 意思是:

1
把 feature/login 合并到 main
  • 点击:

1
Create pull request
  • 填写:

1
2
3
4
5
6
7
Title:
feat: add login page

Description:
- 新增登录页面
- 新增登录接口调用
- 新增用户状态管理
  • 然后提交。

一个基于 PR 开发模式的大致流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建新的开发分支
git switch -c feature/xxx

# 开发后提交代码
git add .
git commit -m "feat: xxx"

# 推送到远程仓库
git push -u origin feature/xxx

# 创建 PR
gh pr create

# 在 GitHub 上合并 PR

# 切换到 主分支
git switch main
# 拉取最新的代码
git pull