Git 重要操作和知识汇总

14 minute

前言

这里记录一些重要的 Git 操作和知识, 随着学习和工作中遇到的 git 相关问题而持续更新。

初始化配置

 1# 配置用户
 2git config --global user.name "akynazh"
 3git config --global user.email "akynazh@qq.com"
 4
 5# 配置 CRLF 自动转 LF
 6git config --global core.autocrlf false
 7
 8# 配置 http 代理
 9git config --global http.proxy "http://127.0.0.1:7890"
10git config --global https.proxy "http://127.0.0.1:7890"
11
12# 配置 SSH 代理
13# 编辑 ~/.ssh/config 如下:
14
15#########################################################
16Host github.com
17    User git
18    Port 22
19    Hostname github.com
20    IdentityFile ~/.ssh/id_rsa
21    # Winodws
22    # ProxyCommand connect -S 127.0.0.1:7890 -a none %h %p
23    # Mac
24    ProxyCommand nc -X 5 -x 127.0.0.1:7890 %h %p
25Host ssh.github.com
26    User git
27    Port 443
28    Hostname ssh.github.com
29    IdentityFile ~/.ssh/id_rsa
30    # Winodws
31    # ProxyCommand connect -S 127.0.0.1:7890 -a none %h %p
32    # Mac
33    ProxyCommand nc -X 5 -x 127.0.0.1:7890 %h %p
34
35# connect 命令是在 Windows 中位于 git 安装目录下的一个操作命令, 位置大致在:C:\Program Files\Git\mingw64\bin\connect.exe
36# nc 为 netcat 可通过 brew, apt 安装
37#########################################################

初始化仓库

首先从代码托管网站建立一个仓库,获取仓库地址 url (如果是 ssh 类型的 url, 需要上传本机公钥),然后进入项目所在文件夹,运行以下代码:

1url="your_repo_url"
2git init # 初始化仓库,生成.git文件
3git add . # 将项目文件的修改信息添加到 .git 内的一个暂存区(index)
4git commit -m “init” # 将暂存区的修改信息提交到分支
5git remote add origin $url # 添加远程仓库
6git push origin master # 将本地分支推送到远程仓库

这里执行完 git commit -m "init" 后,我们查看一下本地分支信息:

1> git branch
2* master

可见 git 自动为我们本地创建了一个 master 分支。

执行完 git push origin master 后,我们查看一下本地分支与远程分支的映射关系:

1> git branch -a
2* master
3  remotes/origin/master
4
5> git branch -vv
6* master 3a31f4c init

可见并没有产生映射。所以如果直接使用 git push 提交代码会报错,因为 git 不知道你要提交到哪个远程分支。为了方便提交, 我们可以建立映射关系:

1git branch -u origin/dev_r 将当前分支 dev_r 与远程分支 origin 建立映射关系

如果建立了映射关系,那么以后在当前分支 git push 时默认 push 到与当前分支建立关系的那个远程分支。

实际操作如下:

1> git branch -u origin/master
2Branch 'master' set up to track remote branch 'master' from 'origin'.
3> git branch -vv
4* master 3a31f4c [origin/master] init # 可看见产生了映射关系

HEAD 相关知识

HEAD 代表当前分支的最新提交名称,它可以由一些修饰符进行修饰进而产生不同的含义。

关于 HEAD~{n}

~ 是用来在当前提交路径上回溯的修饰符。

HEAD~{n} 表示当前所在的提交路径上的前 n 个提交(n >= 0):

  • HEAD = HEAD~0 (即当前最新的一次commit)
  • HEAD~ = HEAD~1
  • HEAD~~ = HEAD~2

关于 HEAD^{n}

^ 是用来切换父级提交路径的修饰符。

当我们始终在一个分支比如 dev 开发/提交代码时,每个 commit 都只会有一个父级提交,就是上一次提交。此时 HEAD~1 等价于 HEAD^

当并行多个分支开发,feat1, feat2, feat3,完成后 merge feat1 feat2 feat3 回 dev 分支后,此次的 merge commit 就会有多个父级提交。

  • HEAD^ = HEAD^1 第一个父级提交
  • HEAD^2~1 第二个父级提交的上一次提交

代码的提交与修改

修改上次提交

这里以对提交的信息和作者进行修改为例,注意邮箱两侧由 <> 包括住

1git commit --amend --message="modify" --author="your_name <your_email>"
2git push -f

修改多次提交

首先列出近 n 次提交:

1# 列出最近3次提交
2git rebase -i HEAD~3

注: 进行 git rebase 之前,先将本地修改提交完毕。

得到大致如下内容:

 1pick 8ae7972 update
 2pick 1a22dfd update
 3pick 00433e4 update
 4
 5# Rebase 368e5c8..00433e4 onto 368e5c8 (3 commands)
 6#
 7# Commands:
 8# p, pick <commit> = use commit
 9# r, reword <commit> = use commit, but edit the commit message
10# e, edit <commit> = use commit, but stop for amending
11# s, squash <commit> = use commit, but meld into previous commit
12# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
13
14......

可见确实列出了前面 3 次的修改记录。

将需要修改的 commit-id 前面对应 “pick” 改为 “edit”,即可在后续操作中对该分支进行修改。

之后,x 次修改就进行 x 次下面两条命令:

1git commit --amend --message="modify" --author="your_name <your_email>"
2git rebase --continue

经过 x 次以上命令,不出什么问题的话,将会有大致如下的提示出现:

1Successfully rebased and updated refs/heads/master.

现在,强制推送即可:

1git push -f

取消或删除提交

当你在某一次提交中不小心忘记了把一些敏感信息进行 ignore,那么你很可能需要删除那一次提交。

可以使用 git reset 删除提交,记录将被真正删除,是将 HEAD 指向某个 commit-id, 该操作需要谨慎使用。比如, 删除上一次提交,也就是把 HEAD 指向 HEAD^, 追加 --hard 使得工作区和暂存区里面的文件也回到以前的状态,下面是 reset 三种操作选项:

  • –soft: 重置 HEAD 到指定的提交,但保持工作目录和暂存区的更改。
  • –mixed(默认):重置 HEAD 到指定的提交,并重置暂存区,但保持工作目录的更改。
  • –hard:重置 HEAD 到指定的提交,并重置暂存区和工作目录的所有更改。这意味着所有未提交的更改将丢失。

如果使用 git revert 则只是取消修改,提交记录还在,并且多生成了一次 “revert” 提交记录,而对 “revert” 提交进行 “revert” 则会恢复对应修改,并生成一次 “reapply” 提交记录。注意 revert 对于敏感信息的处理是无效的。

在进行 revert 之前需要提交或贮藏(stash)本地修改,而 reset 可以无视当前工作区状态。下面是一些具体操作:

 1git revert [倒数第一个提交] [倒数第二个提交]
 2# e.g. git revert HEAD HEAD~1
 3
 4# 取消上次提交
 5git revert HEAD
 6# 接上,恢复上次提交
 7git revert HEAD
 8
 9# 取消上次提交(删除提交记录, 保留工作区修改)
10git reset HEAD^
11# 取消上次提交(删除提交记录, 保留工作区和暂存区修改)
12git reset --soft HEAD^
13
14# 删除本地所有最新提交, 直接使用远程分支覆盖本地分支
15git reset --hard origin/master
16# 删除上一次提交
17git reset --hard HEAD^
18# 删除某次提交
19git reset --hard commit_id

取消或移动未提交的更改

将本地未提交的更改合并到另一个分支:

通过 stash 操作存储我们在当前分支上所做的更改, 然后我们可以切换到正确的分支并将它们应用到它上面:

1git stash
2git checkout <right-branch>
3git stash pop

取消本地未提交的更改:

1git stash
2# git stash show
3git stash drop

合并某几个提交

如果需要另一个分支的所有代码变动,那么就采用 merge。而如果只需要部分代码变动(某几个提交),这时可以采用 cherry-pick(意为挑选最有利的一个)。

1# 将提交 A 和提交 B 集成到当前分支
2git cherry-pick <HashA> <HashB>
3# 如果有冲突则解决后继续执行 --continue, 如果回到操作前的样子则 --abort, 如果不想则 --quit
4git cherry-pick --continue

合并上游分支

1git remote add upstream https://github.com/whoever/whatever.git
2git fetch upstream
3git checkout master
4
5# Rewrite your master branch so that any commits of yours that
6# aren't already in upstream/master are replayed on top of that
7# other branch:
8
9git rebase upstream/master

代码恢复抢救

有时候脑子瓦特了做出一些离谱的操作, 导致辛苦写的代码白白消失不见, 这时候就需要抢救了……

恢复丢弃的 stash 数据

这是一次真实经历过的事件, 有个 stash 的记录忘记 pop 了, 后来操作失误直接 drop 了, 发现之后直接头脑一片空白! 记录下抢救措施!

1git fsck --lost-found

会看到一条条记录类似:

1dangling commit 6cb2480fa3a59c140b58a07ac734838a2d958d44
2dangling commit e9b4c85c437aacf628e724903df21648538963d4
3dangling blob 983515c6f78d232f1c8878e98598218c142aa9b9
4dangling commit 4ab67a050c5a8288da28154f79ea89106918ea36
5dangling commit c2363d6a03dad8f2bc986f8d21ab0fa38c74477b
6dangling blob 4338d18c5224397a03b40548b7e0f1ea9ca3ffff

复制 dangling commit 的 id (其他的 dangling blob 不用关心), 通过 show 操作查看具体的修改:

1git show 6cb2480fa3a59c140b58a07ac734838a2d958d44

其中日期是 stash 的日期, 摘要会记录你是在哪一条 commit 上进行 stash 操作。

再找到想要的记录后执行 merge 即可:

1git merge 6cb2480fa3a59c140b58a07ac734838a2d958d44

查看或恢复被覆盖掉的提交历史

之前我使用 git commit --amend 修改以前的提交,这样使得 git log 见不到上次提交的信息, 但事实上 git 并没有真的删除上次提交,而是重新创建了一批新的提交,并将当前分支顶端指向了新提交。

可以使用 git reflog 找到并且重新指向原来的提交来恢复它们,注意 reflog 保存的提交是有期限的,需要及时进行操作。

1git reflog
2
3# 可以查看到 amend 前的一条提交 99d2720 如下
43805bba (HEAD -> master, origin/master, origin/HEAD) HEAD@{0}: commit (amend): update
599d2720 HEAD@{1}: commit: update
6...

拿到 commit id 99d2720 之后就好办了, 使用 git reset --hard 99d2720 即可恢复。

开源仓库前清除历史提交

1git checkout --orphan x
2git add -A
3git commit -am "oss"
4git branch -D master
5git branch -m master
6git push -f origin master

子模块相关操作

子模块初始化及更新方法

 1# add >> .gitmodules
 2git submodule add https://github.com/chaconinc/DbConnector
 3# clone
 4git clone --recurse-submodules https://github.com/chaconinc/MainProject
 5# or
 6git clone https://github.com/chaconinc/MainProject && git submodule update --init --recursive
 7# renew
 8cd your/submodule/path && git checkout master && git pull && cd your/proj/root && git commit -am "update submodule" && git push
 9# checkout master on all submodules in a single line (submodules are pinned to a specific sha)
10git submodule foreach --recursive git checkout master
11# publish
12cd your/submodule/path && git commit -am "update" && git push && cd your/proj/root && git commit -am "update submodule" && git push

完全移除子模块的方法

  1. 手动删除子模块目录;
  2. 手动删除 .git/modules 下子模块暂存区目录;
  3. 更新 .gitmodules 文件,移除子模块信息块;
  4. 更新 .git/config 文件,移除子模块信息块;

建议用法

使用 rebase 来集成其他分支修改

rebase 通常用于重写提交历史。下面的使用场景在大多数 Git 工作流中是十分常见的:

  1. 我们从 master 分支拉取了一条 feature 分支在本地进行功能开发
  2. 远程的 master 分支在之后又合并了一些新的提交
  3. 我们想在 feature 分支集成 master 的最新更改

基本操作流程如下:

 1# [feature]
 2git checkout feature
 3# feature 集成 master 修改
 4git rebase master
 5# 手动解决冲突
 6# ...
 7git rebase --continue
 8git push origin feature -f
 9
10# 或者通过 git pull 完成:
11git pull origin feature --rebase
12# git config pull.rebase true # 也可以进行默认配置
13# 手动解决冲突
14git push origin feature
15---------------------------------
16
17# [master]
18git checkout master
19git merge origin feature
20git push origin master

这样, 提交的历史将是很干净的, 即使在 master 进行了 merge 但是提交历史仍然不会出现 “merge …” 之类的提交, 因为 merge 的目标分支 feature 经过了 rebase, 现在合并后 feature 上的提交就如同在 master 上进行提交一般。