Basic

status

git status -s 或者 –short 可以查看简略状态信息:

左侧的状态一共有两列。

  • ?? 表示还没有被追踪的文件。

  • 左侧的M,或者说绿色的M,表示该文件有修改被添加到暂存区 。

  • 右侧的M,或者说红色的M,表示该文件在工作区有修改,未添加到暂存区。

    综上,MM 表示文件先修改了一部分,add,再修改了一部分。

  • D 表示文件被删除。

diff

git diff 直接使用,可以查看工作区具体有哪些修改。

git diff --staged 可以对比暂存区和最近的一次修改,–cached 选项用法与此相同。

我们想知道 B 相对 A 多了哪些提交,如果 A 是 B 的祖先,直接 diff 即可。如果 A 和 B 分叉过,diff 会同时显示 A 中新增的内容“已删除”。为了避免这个现象,需要先找到 A 和 B 的公共祖先,再进行 diff。

git merge-base B A 会输出两个分支最近公共祖先的 Hash,目测交换顺序也可以,然后 diff,或者合并为一步。

git diff $(git merge-base B A)

git diff A...B 上边的语法糖,也会找最近公共祖先,不能交换 A B 的顺序。

commit

git commit -v 会在编辑 message 界面时,列举出当前的 diff 信息,表明当前要提交哪些修改。

rm

git rm 会直接从硬盘删除文件,如果文件在工作区或暂存区有修改,需要加 -f 强制删除。

如果想在硬盘中保留,但不想让 git 继续追踪,可以加 –cached,这样文件会变为 untracked file。

mv

git mv <old_name> <new_name> 可以用于文件改名,修改会被直接添加到暂存区,git 会认为这是一个 renamed file。

如果是直接在 vscode 对文件进行改名,则 git 认为原来的文件被删除,新的文件成为 untracked file。

不过,mv 只是一个语法糖,修改的本质还是先删后增,如果先 mv,再 git restore –staged,能观察到和上一行一样的效果。

log

git log 选项:

-p:详细显示每次提交都有哪些修改。

-<n>:限制 log 的条数,只显示最新 n 次修改的 log。

--stat:显示每次提交都有哪些修改的缩略信息,只能看到每个文件的++–符号。

--pretty:指明格式,值可以是 oneline,如 –pretty=online。这里还可以使用 format。

--oneline:是上边的缩略写法,同时也包括了--abbrev-commit 的作用,即不显示完整哈希值。

--graph:显示分支和合并图。

--since:指定时间。值可以是:2.weeks,”2008-01-15”,”2 years 1 day 3 minutes ago”。

--author:指定作者。

--grep:在 message 中搜索关键词。

-S:提供一个字符串,只列举增删了该字符串的提交。比如我在 a.txt 里新增了 Danmo 后提交,然后 git log -S “Danmo”,就能找到我的这一次提交。

--:提供文件路径,查找和该文件相关的提交的 log。

--all-match:如果指定了多个筛选条件,需要加这个选项,拿多个 –grep 举例,如果不加,只有第一个生效。

--decorate:查看分支和 HEAD 指向哪次 commit。

一般 git log 只显示该分支指向的 commit 及之前的记录,git log <branch_name> 查看特定分支的记录,–all 显示所有记录。

git log --no-merges issue54..origin/master 不显示合并记录,后面的参数表示 commit range,后者没有包含在前者中的记录。

git log branchA --not branchB 显示 branchA 中没有包括在 branchB 中的提交,用来查看他人新的提交。

amend

如果刚刚完成了一次提交,发现有 forgotten_file 忘了提交,此时可以把它加入暂存区,然后 git commit –amend。

这种方式同样适用于修改 commit message。

–amend 会更改最后一次提交的 HASH,如果已经 push 就不要再更改。

如果不想修改 commit message 还可以添加 --no-edit 选项。

remote

git remote 显示远程服务器 shortname,如果是 clone,默认名为 origin。

git remote -v 可以显示 shortname 和 URL,可见只是用 origin 指代远程服务器 URL。

1
2
3
4
5
6
git remote -v

origin https://github.com/DanmoSAMA/remote-sensing (fetch)
origin https://github.com/DanmoSAMA/remote-sensing (push)

git push https://github.com/DanmoSAMA/remote-sensing == git push origin

origin/master 可以访问 shortname 为 origin 的仓库的 master 分支。

git remote add <shortname> <url> add 后面可以自定义 shortname。

git remote show <remote> 查看 remote 的详细信息。

This command shows which branch is automatically pushed to when you run git push while on certain branches. It also shows you which remote branches on the server you don’t yet have, which remote branches you have that have been removed from the server, and multiple local branches that are able to merge automatically with their remote-tracking branch when you run git pull.

git remote rename <old_name> <new_name> 更改 shortname。

git remote remove <shortname> git remote add <shortname> <url>

tag

git tag -l "v1.8.5*" 使用通配符来筛 tag 列表,-l 指定通配符。

git tag -a v1.0 -m <message> 创建 annotated tag,并附注信息。

-d 选项可以删除 tag。

git show <tag_name> 可以连同对应的提交一起,查看某个 tag 信息。

Lightweight tag 仅仅用于标记某次 commit,创建时无需添加任何选项,git show 也只能看到 commit 信息。

如果想要对之前的 commit 打 tag,只需 git tab -a v1.0 <commit_hash>

git push <remote> tags 可以将所有 tags 发送到 remote。

git push <remote> --follow-tags 仅发送 annotated tag。

git push <remote> --delete <tagname> 删除 remote tag。

由于 tag 是指向 commit 的指针,因此可以应用于 detached HEAD 模式,git checkout -b version2 v2.0.0 从该 commit 分出了一个分支。

branch

basic

git switch - 回到上一次切换前的分支。

git branch -v 查看每个分支上的最后一次提交。

git branch -vv 在上方基础上,查看 tracking branch。

git branch --merge 查看哪些分支已经合并到了当前分支,–no-merge 查看没合并的分支。

没合并的分支,用 git branch -d 无法删除,需要用 -D 强行删除。

git branch --merge <branch_name> 同上,但无需先切换到那个分支上。

git branch --move <old_name> <new_name> 修改本地分支名。

git merge --abort 终止 merge。

remote

git push --set-upstream origin <new_name> 将此修改推到远程。

git push origin --delete <old_name> 删除远程的旧分支。

git ls-remote <remote> 列举远程分支。

git push origin 本地分支名:远程分支名 push 时希望修改分支名称。

git fetch 后,本地会新增 origin/xxx 分支,这个分支不能修改,可以把它合并到本地已有分支,也可以新建一个分支,拥有和它一样的指向:git checkout -b xxx origin/xxx

git switch -c <branch> --track <remote>/<branch> 同理,git switch --guess <branch> 为其简写。

上面的做法,新建的分支会成为 tracking branch,追踪远程的 origin/xxx 分支。

git checkout --track origin/xxx 是简写。

git branch -u origin/xxx 已有分支,更改上游。

如果存在上游分支,在 merge 的时候可以用 @{u} 指代上游,如:git merge @{u}

git fetch --all 拉取所有分支的更新。

git remote set-head origin <origin_branch_name> 设置远程 HEAD 指向。

rebase

git rebase master server 把 server 分支 rebase 到 master 上,这样的好处是不需要先切到 server 分支。

git rebase --onto master server client 先找到 client 分支,将 client 从 server 分叉之后的部分 rebase 到 master。

perils of rebase

如果对已经提交的分支进行 rebase,并且 push -f 强行提交,会出现问题。结合 git book 里面的图更好理解。

If you only ever rebase commits that have never left your own computer, you’ll be just fine. If you rebase commits that have been pushed, but that no one else has based commits from, you’ll also be fine. If you rebase commits that have already been pushed publicly, and people may have based work on those commits, then you may be in for some frustrating trouble, and the scorn of your teammates.

别人可能 fetch 过这个分支,并且合并到了自己的分支上(相当于 pull),假设这个分支包含了 C1 的修改。

此时,把 包含 C1 的分支 rebase,并且使用 push -f 提交。

别人再用 pull 时,把 rebase 后的分支与本地合并,此时 git log 会看到有两个一模一样的 C1 的提交,造成困惑。

如果团队中有成员进行过这样的操作,应该:You rebase, and I rebase.

如果是分步进行,则先 fetch,再 git rebase origin/xxx,直接把当前分支 rebase 到远程分支的后面,git 会智能地帮我们处理好一切。

如果是合并进行,则 git pull --rebase,可以设置 –rebase 为默认:git config --global pull.rebase true

distributed git

git diff --check 在提交之前,检查是否存在 “whitespace issues”。

The --squash option takes all the work on the merged branch and squashes it into one changeset producing the repository state as if a real merge happened, without actually making a merge commit. This means your future commit will have one parent only and allows you to introduce all the changes from another branch and then make more changes before recording the new commit. Also the --no-commit option can be useful to delay the merge commit in case of the default merge process.

假设要把分支 B 合并到分支 A 上,–squash 可以把 B 的变化直接拿过来,放到 A 上,不产生新的 commit,需要人工进行 commit。此后在查看 log 时,只能看到一个 parent,即 A。

HEAD “^” vs “~” vs “@{}”

在空白目录下运行以下 shell 脚本:

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
# 初始化
git init
git branch --move master main

# 构建分支
echo 1 >> file
git add file
git commit -m "main-commit-1" -a
echo 1 >> file
git commit -m "main-commit-2" -a
git checkout -b dev
echo 1 >> dev
git add dev
git commit -m "dev-commit-1" -a
git checkout -b prod
echo 1 >> prod
git add prod
git commit -m "prod-commit-1" -a
git switch main
echo 1 >> file
git commit -m "main-commit-3" -a
git switch dev
echo 1 >> dev
git commit -m "dev-commit-2" -a
git switch prod
echo 1 >> prod
git commit -m "prod-commit-2" -a
git switch main
git merge -m "Merged dev" dev
echo 1 >> file
git commit -m "main-commit-4" -a
git merge -m "Merged prod" prod

备注:图片中用的是 master 分支,没有改名为 main,不影响。

上结论:

  • HEAD^ HEAD^^… HEAD~ HEAD~~… HEAD1 HEAD2 HEAD~<n>… 都是在当前分支上找父结点,比如此时在 main 分支上 ,就只沿着 main 这条分支的父节点往前找。或者按 Git book 的说法来理解,每个 ^ 或 ~ 表示寻找其第一个父结点,多个叠加即寻找”第一个父结点的第一个父结点”。

    You can also specify a number after the ^ to identify which parent you want;

    This syntax is useful only for merge commits, which have more than one parent — the first parent of a merge commit is from the branch you were on when you merged (frequently master), while the second parent of a merge commit is from the branch that was merged (say, topic):

    HEAD~2 means “the first parent of the first parent,” or “the grandparent” — it traverses the first parents the number of times you specify.

  • HEAD^<n> 针对有 merge 的情况,HEAD^1 表示第一个父结点,HEAD^2 表示第二个父结点。如果当前 HEAD 不是 merge 得到的结点,那么它只有 HEAD^1,试图找 HEAD^2 会报错:

    1
    fatal: ambiguous argument '19588e4^2': unknown revision or path not in the working tree.
  • HEAD@{<n>} 针对 reflog,与操作的次序有关。以下是特殊用法:

    git log --no-walk HEAD^@ 可以列出当前结点的父结点,–no-walk 表示不显示祖先结点。比如 HEAD 在 merge-prod,该指令可以查看 merge 的两个父结点。如果不加 –no-walk,可以看到所有祖先结点。

    git show HEAD^@ 可以查看父结点的详细信息,不需要 –no-walk 也不显示祖先。

  • ^ 和 ~ 也可以打组合拳,明确了 ^ 和 ~ 的语义后不难理解,^ 是纵向的第几个 parent,~ 是横向的第几个 parent,HEAD^^^ 这种属于特殊情况,可以这样看待:由于 HEAD^1 和 HEAD^ 等价,可以做拆分:((HEAD^)^)^,而 HEAD^ 和 HEAD~ 等价,所以多个 ^ 也和 ~ 等价。

tools

git show <commit> 具体查看某一次提交。

git rev-parse <branch> 查看分支指向结点的 Hash。

double dot

You can ask Git to show you a log of just those commits with master..experiment — **that means “all commits reachable from experiment that aren’t reachable from master.**”

git log origin/master..HEAD 可以查看 HEAD 拥有而 origin/master 没有的提交,从而确认本次 push 有哪些 commit。HEAD 可以省略。

You can also leave off one side of the syntax to have Git assume HEAD. For example, you can get the same results as in the previous example by typing git log origin/master.. — Git substitutes HEAD if one side is missing.

multiple points

查看 B 有 A 没有的提交,有如下方法:

1
2
3
$ git log refA..refB
$ git log ^refA refB
$ git log refB --not refA

第二、三行的语法可以对比更多分支,查找对应的提交:

1
2
$ git log refA refB ^refC
$ git log refA refB --not refC

triple dot

… 可以查看两个分支的“异或”,即只能被某一分之访问的提交。

可以搭配 –left-right 选项使用,git log --left-right A...B: < 表示只能被 A 访问,> 表示只能被 B 访问。

patch mode

add -p 可以把一个文件分成多个 chunk,然后可将部分 chunk 添加到暂存区,如果 chunk 分得不够细,应用 s。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Stage this hunk [y,n,a,d,/,j,J,g,e,?]? ?
y - stage this hunk
n - do not stage this hunk
a - stage this and all the remaining hunks in the file
d - do not stage this hunk nor any of the remaining hunks in the file
g - select a hunk to go to
/ - search for a hunk matching the given regex
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
k - leave this hunk undecided, see previous undecided hunk
K - leave this hunk undecided, see previous hunk
s - split the current hunk into smaller hunks
e - manually edit the current hunk
? - print help

同样可以应用 patch mode 的还有 reset / checkout / stash,参考 7.2 节。

stash

git stash list 查看 stash 列表。

git stash apply stash@{<n>} 应用 stash,如果不指定,应用最新的 stash,应用后不会删除 stash。

添加 –index 选项,可以恢复 stash 之前的暂存区。

git stash drop stash@{<n>} 删除 stash。

git stash clear 清除所有 stash。

git stash pop stash@{<n>} 是 apply 和 drop 的结合,需要注意的是,pop 也可以接 stash 名称,不只是可以 pop 最新的 stash。

如果要连续应用两次 stash,并且存在冲突,需要先把第一次的 stash 添加到暂存区,再执行一次 pop,此时才能解决冲突。

否则报错:error: Your local changes to the following files would be overwritten by merge.

git stash --keep-index 仅把工作区的修改添加到 stash,不影响暂存区。

–include-untracked 或 -u 会把 untracked files 也加入 stash,但不会加入明确 ignored 的 file,如果要把它们也包含进来,用 -a 或 –all。

如果 stash 后在该分支上继续工作,应用 stash 的时候可能会遇到很多冲突,最佳实践是切到一个新的分支上应用 stash,可以简写为:git stash branch <branchname> [<stash>]

grep

git grep <content / regExp / wildcard> 查找。

选项需要加在 grep 后面,-c 显示数量,-n 显示行号,–and 指定多个条件。

rebase -i

git rebase -i <commit> 可以修改 commit 之前所有提交的记录。

edit

将动词修改为 e 或 edit,保存并退出。

接下来,在需要 edit 的地方会停止,此时执行 git commit –amend 来修改,修改完后 git rebase –continue 继续。

reorder / remove

remove 就是把对应 pick 行删除,reorder 就是改变 pick 行顺序。

但我发现一个问题,先看看 shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mkdir try-git
cd try-git
git init
git branch --move master main

echo 1 >> file
git add .
git commit -m "commit-1"
echo 2 >> file
git commit -m "commit-2" -a
echo 3 >> file
git commit -m "commit-3" -a
echo 4 >> file
git commit -m "commit-4" -a
echo 5 >> file
git commit -m "commit-5" -a
echo 6 >> file
git commit -m "commit-6" -a

使用 rebase -i 删除 commit-4,需要解决冲突,此时发现,即使 commit-4 删除了,所做的修改仍然保留着,并且在合 commit-5 的时候,把 commit-4 的修改也包含进来了,如果要丢弃此修改,需要细心甄别。

使用 drop 和直接删除某一行的效果类似。

squash

如果此时有三条 pick 记录,A B C,想要把 B C 合到 A,则保持 pick A 不变,修改 B、C 为 squash。

之后编辑新的 commit message,保存即可。

split

把想要拆分的 commit,改成 edit。

在这条 commit 处停下时,使用 git reset HEAD^ 使修改进入工作区,然后 split 成多次提交,完成之后 rebase –continue 即可。

reset

git reset 如果后接 commit,工作流程:

  1. 当前分支随 HEAD 一起指向新的 commit。<= --soft 停在这里
  2. 将版本库的内容复制到暂存区(index)。<= --mixed 停在这里,默认选项
  3. 将暂存区的内容复制到工作区。<= --hard

git status 的原理是对比 HEAD、暂存区、工作区。

因此,使用 –soft 时,由于 HEAD 指向曾经的 commit,并且暂存区不变,因此暂存区和 HEAD 存在差异,新的暂存还未提交,所以 status 显示绿色

使用 –mixed 时,由于 HEAD 的内容复制到了暂存区,现在暂存区和 HEAD 相同,但是工作区没有修改,所以工作区和暂存区有差异,status 显示红色

使用 –hard 时,status 显示没有新的改变。

如果先使用 –mixed 回退,再使用 –soft 恢复到之前的 commit,那么会出现版本库和工作区一致,暂存区落后的情况,status 又红又绿。

git reset 如果后接 path,那么不会发生上方第一步

git reset file 会执行第二步,相当于回退了暂存区,因此此时 reset 的功能相当于 restore。

git reset <commit> <file> 同样不会发生第一步,它会用该 commit 中的 file 去覆盖此时暂存区的 file,不影响工作区。此时只需要 提交即可。

reset 也可以用于合并多次 commit,只需要先 reset –soft 回退到某一版本,由于此时暂存区不变,只需要提交一次,即可把后面多次提交的内容合并为本次提交。当然不加 –soft 也可以,需要多加一次暂存区。

rerere

revere 是 reuse / recorded / resolution 的缩写,意为”复用冲突解决的记录”。

rerere 可以复用之前的 merge conflict resolution。

git config --global rerere.enabled true 打开 rerere。

此后,第一次 merge 时,会被 rerere cache 记录,merge 后的信息多了一行:

1
Recorded preimage for '<filename>' 

git rerere diff 可以查看当前 resolution 的状态。

然后解决冲突并提交,使用 reset --hard HEAD~ 回退,再次 merge,发现之前的 resolution 会自动生效。

如果需要恢复冲突状态,使用 git checkout --conflict=merge <filename>

在冲突状态下重新应用 rerere,使用 git rerere