# Git
文档:https://git-scm.com/doc
Pro Git:官方推荐免费教程中文版 https://git-scm.com/book/zh/v2
## Git 简介

[在线动态解析各部分之间的联系](http://ndpsoftware.com/git-cheatsheet.html):Stash - Workspace - Index - LocalRepository - UpstreamRepository
工作目录下的 .git 文件夹存放的就是本地 Git 仓库,可以进去琢磨琢磨:
* objects 目录 存储所有数据内容;
* refs 目录 存储指向数据(分支)的提交对象的指针;
* HEAD 文件 指示目前被检出的分支;
* index 文件 保存暂存区信息。
Git for Windows: Git 的 windows 版本,包含 Git 版本控制工具核心、Git Bash 和 Git GUI
## 常用命令速查表
```bash
$ git pull # 拉取代码 或
$ git fetch
$ git fetch bwh
$ git merge bwh/tabview
$ git push # 上传代码
$ git add . # 添加更改到 staged
$ git reset # 用最近一次 commit 的内容覆盖 index,相当于撤销 git add . 操作
$ git reset HEAD~2 # 用最近第2次 commit 的内容覆盖 index
$ git reset --hard # 清空工作目录中已跟踪文件到上次提交状态
$ git clean -df # 清空工作目录中新增的文件,含目录不含 .gitignore 中的文件
$ git stash # 临时保存还没有提交的工作(工作目录 + 暂存区)并恢复到初始状态,注意未 track 的新文件还留在工作目录
$ git stash pop # 恢复最近一次 stash 的内容,注意是保存的变动追加在目前状态之上,而非覆盖目前状态
# 撤销一次远程提交
$ git reset HEAD~1 # 回退一次提交
$ git push -f # 强制推送到远程仓库
$ git reset --hard origin/master # 强制用远端提交覆盖本地提交
# 分支操作
$ git branch -a # 列出本地和远程所有的分支
$ git checkout -b newBrach master # 创建并切换到新分支
$ git push origin :newbranch # 删除一个远程分支(冒号前为空,即推送一个空白分支到远程分支)
$ git branch -d newbranch # 删除一个本地分支
# patch 操作
$ git diff > ..\patch1023 # 生成一个 patch 文件,文件放到父目录
$ git apply ..\patch1023 # 在另一个地方导入这些更改
$ git cherry-pick .. # 将其他分支的多次连续提交复用到当前分支
# 修改 commit message
$ git commit --amend # 修改最近一次提交
$ git rebase -i HEAD~num # 批量修改最近 num 次提交
$ git fetch & git rebase origin/master & git push # 避免合并冲突时产生冗余提交记录
# 查看本地 Git 操作记录
$ git reflog # 强力后悔药,可以找回误删的 commit hash 并复原
```
代理设置
```bash
# 仅适用于对 http/https 加速,对 ssh+git 无效
# ssh+git 设置可参考 https://gist.github.com/ozbillwang/005bd1dfc597a2f3a00148834ad3e551
$ git config --global http.proxy socks5://localhost:1080
# 单次下载临时使用代理
$ git clone https://github.com/goldendict/goldendict.git --config "http.proxy=socks5://localhost:1080"
```
## 命令详解
### 设置与帮助
```bash
$ git config --global user.name "" # 指定全局默认用户名
$ git config --global user.email "" # 指定全局默认用户邮箱
$ git config --global color.ui auto # 指定终端输出颜色
$ git config --global alias.co checkout # 设定命令别名,git 不会在输入部分命令时自动推断命令
$ git config --global alias.st status # 设定命令别名,所以将一些常用操作设定别名会很方便
$ git config --global credential.helper wincred # 设定免密码登录(记住密码) [注1]
$ git config --global https.proxy 'socks5://127.0.0.1:1080' # git 使用 ss 代理加速
$ git config --global gui.encoding utf-8 # 解决图形界面乱码
$ git config --global pull.rebase true # pull 时用 rebase 替代默认 merge
# 配置使用 meld 作为 diff & merge 外部工具,注意需要添加 meld 路径到 PATH
# 如果命令行执行不成功,则手动修改 .gitconfig 文件
$ git config --global merge.tool meld
$ git config --global mergetool.meld.cmd 'meld.exe \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"'
$ git config --global diff.tool meld
$ git config --global difftool.meld.cmd 'meld.exe \"$LOCAL\" \"$REMOTE\"'
$ git config --list --global # 查看全局设置
$ git config user.name "Gavin" # 配置单个仓库的用户名
$ git config user.name # 查看当前仓库使用的用户名
$ git -h # 在终端显示指令的简明帮助
$ git --help # 通过指令的 help 选项来获取帮助,另开网页显示
$ git help # 通过 help 指令来获取帮助,另开网页显示
```
注1:GitHub 自动登录介绍 https://help.github.com/articles/caching-your-github-password-in-git/
```text
[diff]
tool = meld
[difftool "meld"]
cmd = meld.exe $LOCAL $REMOTE
```
### 获取或新建仓库
```bash
$ git init # 将现有目录初始化成 Git 仓库
$ git clone # 克隆远程仓库,会下载该仓库的所有历史版本
$ git clone <网址> <本地目录名> # 克隆时可以指定本地仓库目录名
$ git clone -o jq https://github.com/jquery/jquery.git # 克隆并指定远程仓库别名为 jq 而非默认的 origin
$ git clone --depth 1
# GitHub clone 快速下载项目分支,指定 depth 会自动开启 --single-branch,实现最小下载,--branch 指定具体分支
$ git clone --branch v4-dev --depth 1 https://github.com/twbs/bootstrap.git # 快速下载 bootsrap/v4-dev
```
git clone 实际上封装了多个命令:创建新目录,切换到新目录,初始化,为 URL 添加一个仓库别名,针对远程仓库执行 git fetch,最后通过 git checkout 将远程仓库的最新提交检出到工作目录。
### 变更操作
```bash
$ git add *.js index.html # 添加所有 js 文件和 index.html 到暂存区
$ git add . # 添加所有文件到暂存区
$ git rm # 将文件移出 index,并删除本地文件
$ git rm --cached # 将文件移出 index,但保留本地文件
$ git mv # 修改文件名并调整 index
# 相当于运行了3条命令: `$ mv oldname newfilename` `$ git rm oldname` `$ git add newfilename`
$ git commit -m # 提交记录到仓库 注:录入带空格的文字时,只能用双引号不能用单引号包裹
$ git commit -a # 提交前自动完成 add 或 rm 已跟踪文件,但未跟踪文件不受影响
$ git commit --amend # 重做(替换)最近一次提交,修改 commit message 很好用
# reset 重置 HEAD 到指定状态,HEAD 指示目前被检出的分支,reset 用于撤销一些提交(转为悬挂提交,然后再垃圾回收)
$ git reset # 用最近一次提交的内容覆盖 index,相当于撤销一个 add 操作,工作目录中文件不会变化
$ git reset [] [] # 将 HEAD 回退到指定的 commit,暂存区和工作区内容是否变化取决于 mode 设定:
# --soft 只回退 HEAD 指针到指定 commit,不影响 index 和工作目录
# --mixed 默认项,重置 index 但不会影响工作目录内容
# --hard 重置暂存区和工作目录,全部回到指定 commit 时的状态,会丢失工作内容
$ git revert # 生成一次新的提交来撤销某个指定 commit,reset 会丢失提交记录,而 revert 不会
# rebase 变基会修改 commit 变更历史
$ git rebase origin/master # 以变基的形式合并分支
$ git rebase --continue # 如变基出现冲突,冲突解决后继续
$ git rebase --skip # 如变基出现冲突,也可以直接跳过这步的 commit 继续 reapply 下一个 commit
$ git rebase --abort # 取消变基操作
$ git rebase -i HEAD~commit_count # 批量修改 commit message 很好用
$ git rebase --onto master topicA topicB # 更换 topicB 的上游分支 topicA 为 master 分支,具体见使用文档
# reset(回退) 用于私人分支,revert(撤销) 用于公共分支,rebase(变基) 适用于还没有 push 的提交
```
### 查询操作
```bash
$ git ls-remote # List references in a remote repository
# -h --heads # 列出远端分支信息
# -t --tags # 列出远端标签信息
# diff 显示不同提交之间,提交与工作目录,暂存区与工作目录之间的差别
# meld 提供了图形化显示比较信息,使用前须配置(见上文配置部分)
$ git diff # 查看工作目录与暂存区的差异
$ git diff --staged # 查看暂存区与最近一次提交之间的差异
$ git diff master branchB # 比较两个不同提交之间的差异,此处的不同分支指向不同的提交
$ git difftool # 不想使用内置的 git diff 时,启动一个外部工具来显示差异
# 注1 阮一峰的 diff 讲解:http://www.ruanyifeng.com/blog/2012/08/how_to_read_diff.html
# 注2 git show 默认会调用 git diff,而 git log 只有在提供 `-p` 或 `--name-only` 等选项时才会调 git diff
# 显示工作目录状态
$ git status # 显示当前状态
$ git status -s # 以简洁的形式显示当前状态,`-s` 同 `--short` 参数
# log 显示提交历史
$ git log # 显示当前分支的提交记录
$ git log --graph # 图形化展现提交历史,即左侧有树状图案
$ git log --oneline # 显示提交记录,每条记录只占一行
$ git log --grep= # 过滤输出记录
$ git log --follow # 跟踪一个文件的修改历史,包括重命名前后的情况
$ git log --all -- path/to/file # 查找单个文件的提交历史,--all 选项可以找出已删除文件的提交历史
$ git log -L ,: # 跟踪 file 文件的特定部分的变更记录,参数可以是行号或者正则
$ git log -L "/function ajax/",/}/:main.js # 跟踪 main.js 文件中的 ajax 函数的变更记录, 正则有空格的要带引号
$ git log --pretty="%h %s" # 精细控制输出格式
# blame 是调试指令,可以跟 log 指令配合使用
$ git blame -L 12,22 sth.cs # 查看 sth.cs 的 12-22行 都有谁在什么时候做了哪些修改
$ git show # 查看数据对象 blob 数对象 tree 提交对象 commit 标签对象 tag 等的内容
# 周工作量统计
$ git log --author="gavin" --since=2021-8-1 --pretty=tformat: --numstat | grep "src/" | grep -v "/api/" | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }' -
# 统计提交次数
$ git shortlog -s
# 代码行数统计
$ cloc ./ --exclude-dir=node_modules
```
### 分支操作
```bash
# branch 列出/创建/删除分支
$ git branch # 列出本地分支
# `-r` 列出远程分支,`-a` 列出本地 + 远程分支, `-v` 显示详细信息
$ git branch testing # 创建 testing 分支
$ git branch -d testing # 删除 testing 分支
$ git branch -m [] # 重命名分支
$ git branch -u origin next # (--set-upstream-to= [branchname]) 手动建立与 origin/next 间的追踪关系
$ git fetch -p # 当删除远程分支后,本地还是能看到 origin/branchname,就可以通过这条命令删除
$ git branch --merged | egrep -v "(^\*|master|dev)" | xargs git branch -d # 批量删本地已合分支
# checkout 切换分支或恢复工作目录文件
$ git checkout # 切换到某个分支
$ git checkout HEAD~2 # 往回退2个commit,这对于快速查看项目旧版本来说非常有用
# 此时工作在 detached HEAD 模式,所做的修改在切换到普通分支后将无法找回,除非你新建分支 checkout -b
$ git checkout -b renamed-feature origin/feature # 在 fetch 下来的 origin/master 的基础上创建一个新分支
$ git checkout --track origin/feature # 创建与远程分支同名的本地分支
$ git checkout # 从最近的一次提交中取回文件并覆盖工作目录中的文件
# 示例 git checkout -- a.txt b.txt / git checkout -- "path/to/*.txt"
$ git merge origin/master # 将刚拉取下来的 master 合并到本地分支
$ git merge topic # 合并 topic 分支内容到当前分支
$ git merge --no-ff future-name # 合并 future 分支到 master 分支,且不允许 fast-forward
$ git merge -e # 提交前暂停供编辑 message
$ git merge --abort # 有冲突暂停时,可回撤整个操作,效果同 `git reset --merge`
```
### 远程同步
```bash
# Fetch branches and/or tags (collectively, "refs")
$ git fetch <远程仓库名> # 获取远程仓库所有更新内容,取回内容通过 远程仓库名/分支 读取
$ git fetch <远程仓库名> <分支名> # 仅获取特定分支的更新内容,然后可 merge 也可 checkout -b
$ git fetch origin master # 获取 origin 仓库的 master 分支,取回的内容要用 origin/master 读取
$ git fetch origin +pu:pu maint:tmp # 获取 origin 仓库的 pu 和 maint 分支,并更新/新建本地 pu tmp 分支
# pu 前的 + 加号表示,即使不能 fast-forward 也强制更新(自动 merge)
$ git pull <远程仓库名> <远程分支名>:<本地分支名>
$ git pull # 获取最新版本并合并,相当于 `git fetch` + `git merge`
$ git pull --rebase # 相当于 `git fetch` + `git rebase --onto`,推荐使用这种方式
$ git pull origin next:master
$ git pull -p # `-p` 告诉 pull 如果远程仓库已经删除了该分支,那么可以将本地分支删除
$ git push [选项] <远程仓库名> <本地分支>:<远程分支>
# --all 推送所有分支
# -f | --force 如果远程仓库比本地版本新,Git 会报错,用 --force 可强制覆盖
# -u | --set-upstream
# -d | --delete
# --prune 如果远程仓库存在本地没有的分支,就将其删除
# --tags 在 push 时一并更新标签(默认不会推送标签)
$ git push -u origin master # 将本地的 master 分支推送到 origin/master 并添加跟踪关系
$ git push -d origin master # 删除远程 master 分支
$ git push origin # 如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略
$ git push # 如果当前分支只有一个追踪分支,那么仓库名都可以省略
$ git push origin HEAD:master # 将当前分支推送到 origin/master
$ git push origin :master # 推送一个空的分支到 origin/master,即相当于删除远程 master 分支
# 管理远程仓库
$ git remote -v # 列出所有远程仓库别名,并显示网址
$ git remote add <仓库别名> <网址> # 添加远程仓库
$ git remote rm <仓库别名> # 删除一个仓库别名,并一同删除本地的相关分支内容和配置
$ git remote show <仓库别名> # 查看远程仓库的详细信息
$ git remote rename <原仓库名> <新仓库名> # 修改指向远程仓库的别名
# 建立跟踪关系
$ git branch -u origin/current-branch-name
$ git push -u origin current-branch-name
# 实现一个 git push 同时推送到多个远程仓库
$ git remote set-url --add --push origin git://original/repo.git
$ git remote set-url --add --push origin git://another/repo.git
```
### 暂存 stash
```bash
# stash 藏匿/储存变动
$ git stash # 临时保存还没有提交的工作(工作目录 + 暂存区)并恢复到初始状态,注意未 track 的新文件还留在工作目录
$ git stash push -m xxx # stash 的时候还可以加下注释
$ git stash list # 列出所有 stash
$ git stash pop # 恢复最近一次 stash 的内容,注意是保存的变动追加在目前状态之上,而非覆盖目前状态
$ git stash apply # 恢复最近一次 stash 的内容,与 pop 的区别是,pop 之后该 stash 就删除了,而 apply 则不删
$ git stash show stash@{1} # 查看最近的第二次 stash 的具体内容
$ git stash show -p # --patch 显示 stash@{0} 的详细内容
$ gitk stash # 使用图形界面查看 stash@{0} 的详细内容
$ git stash branch [] # 根据 stash 生成新的分支以还原当时现场
$ git stash drop # 删除最近一次 stash
$ git stash clear # 清空所有 stash 的内容
```
### 标签 tag
```bash
# tag 创建/列出/删除/修改 标签对象
$ git tag -a v1.4 -m 'version 1.4' # 创建一个附注标签 annotated,包含创建时间、创建者等详细信息
$ git tag v1.4 # 创建一个轻量标签 lightweight,不包含详细信息,适用于临时标签
$ git show v1.4 # 查看标签信息
$ git tag -a v1.2 9fceb02 # 给历史提交补打标签,需提供 SHA-1 校验值
$ git tag -d v1.4 # 删除一个标签
$ git tag -f v1.4 # 删除原有的 v1.4 标签重新在当前提交上打标签
$ git tag # 查看本地标签
$ git ls-remote --tags origin # 列出远端所有标签
$ git push origin v1.5 # git push 命令并不会传送标签, 你必须显式地推送标签到远程服务器
$ git push origin --tags # 也可以一次性推送所有不在远程服务器上的标签
$ git push -d origin # 删除远端标签
$ git push origin :tagname # 删除远端标签
# Git 中你并不能真的检出一个标签,因为它们并不能像分支一样来回移动。如果你想要工作目录与仓库中特定的标签版本完全一样,可以使用 `git checkout -b [branchname] [tagname]` 在特定的标签上创建一个新分支:
$ git checkout -b version2 v2.0.0
```
```bash
$ git tag -l | xargs git tag -d # 批量删除本地标签
$ git fetch # 拉取远端标签
$ git tag -l | xargs -n 1 git push --delete origin # 批量删除远端标签
```
### 其他操作
```bash
# clean 清理工作目录
$ git clean # 从工作区中移除未跟踪的文件,.gitignore 中文件不受影响
$ git clean -X # 移除未跟踪的文件和目录,.gitignore 中的文件也会一并移除
$ git clean -df # 移除未跟踪的文件和目录
$ git clean # 在 path 范围内移除未跟踪的文件
# ls-files 显示暂存区和工作目录内文件的信息
$ git ls-files -u # 显示冲突的文件,-s 则显示标记为冲突已解决的文件
$ git ls-files --stage # 检查保存在 stage 的文件
$ git cat-file -p d67046 # 查看一个 Git 对象的内容,用于研究 Git 内部机制
```
```bash
# work on an existing branch in a new worktree
git worktree add
# creates new branch hotfix and checks it out at path ../hotfix
git worktree add ../hotfix
# creates a new worktree with a detached HEAD at the same commit as the current branch
git worktree add -d
git worktree prune
git worktree list
```
### 图形化操作
在安装 Git 的同时,你也装好了它提供的可视化工具,gitk 和 git-gui。
```bash
$ git gui # 调出图形界面
$ gitk [git log options] # 调用图形界面查看历史提交的详细信息
$ gitk stash # 查看 stash 的详细变更信息
$ git config --global gui.encoding utf-8 # 解决 gitk 中文乱码
```
#### gitk
```bash
$ gitk --first-parent # 查看存在频繁合并的分支记录非常有用
```
gitk 是一个历史记录的图形化查看器。你可以把它当作是基于 `git log` 和 `git grep` 命令的一个强大的图形操作界面。当你需要查找过去发生的某次记录,或是可视化查看项目历史的时候,你将会用到这个工具。
常用操作:
* 查看合并记录的文件变更 - 选中父提交并在当前合并提交右键调出 `Diff selected -> this`
* 利用外部工具查看具体文件变更 - 选择单个文档,右键调出 `External diff`
#### GitHub
GitHub Desktop: https://desktop.github.com/
GitHub 发布的本地客户端,相比网站操作提供了更多功能,也避免了 git 命令操作的各种麻烦和不给力。
### GitHub 操作
```bash
# 在 GitHub 上先新建项目仓库,然后
$ git init
$ git add README.md
$ git commit -m "first commit"
$ git remote add origin https://github.com/ooboqoo/primeng-webpack.git # 如果是现有项目,可以直接从这开始
$ git push -u origin master # -u / --set-upstream 具体讲解如下:
```
一般只有同时存在多个远程仓库时才会用到 --set-upstream。每个分支可以有自己对应的 upstream。假设你有两个 upstream,分别叫 server1 和 server2,本地 master 分支的 upstream 是 server1 上的 master,那么当你不带参数直接输入 git pull 或 git push 时,默认是对 server1 进行 pull/push。如果你成功运行 `git push -u server2 master`,那么在将本地 master 分支推送到 server2 的同时还会把 server2 设置成 upstream。
### 配置 .gitignore
文件 .gitignore 的格式规范如下:
* 所有空行或者以 `#` 开头的行都会被 Git 忽略。
* 可以使用标准的 glob 模式匹配,如 `*.[oa]` 会匹配以 .o 或 .a 结尾的文件。
* 匹配模式可以以 `/` 开头防止递归。
* 匹配模式可以以 `/` 结尾指定目录。
* 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号 `!` 取反。
所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。
* `*` 星号匹配零个或多个任意字符;
* `?` 问号只匹配一个任意字符;
* `[]` 方括号提供一个可匹配的字符集合,如 `[abc]` 会匹配 a b c 这三个字符;
* `-` 方括号中用短划线分隔两个字符,匹配这两个字符范围内所有字符,如 `[0-9]` 表示匹配所有数字;
* `**` 使用两个星号表示匹配任意中间目录,比如 `a/**/z` 可以匹配 a/z, a/b/z 或 a/b/c/z 等。
```ini
# no .a files
*.a
# but do track lib.a, even though you're ignoring .a files above
!lib.a
# only ignore the TODO file in the current directory, not subdir/TODO
/TODO
# ignore all files in the build/ directory
build/
# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt
# ignore all .pdf files in the doc/ directory
doc/**/*.pdf
```
注:将已跟踪的文件添加到 ignore 名单里并不会有效果。
https://stackoverflow.com/questions/1753070/how-do-i-configure-git-to-ignore-some-files-locally
https://stackoverflow.com/questions/1274057/how-to-make-git-forget-about-a-file-that-was-tracked-but-is-now-in-gitignore
```bash
$ git update-index --skip-worktree # 或
$ git update-index --assume-unchanged # `--no-assume-unchanged` 重新开启 track
```
#### .gitkeep
这只是个空文件,貌似也不是官方的用法,而更像是一个 hack,目的就是让 git 忽略其所在目录的同时保留该空目录。
## Git 流程规范化
### 分支管理规范
龙猫关于多人合作项目的经验分享:
* 多用客户端和工具,少用命令行,除非是在 linux 服务器上直接开发。
* 每次提交前,diff 自己的代码,以免提交错误代码或测试代码。
* 下班回家前,整理好自己的工作区并提交。
* 并行的项目,使用分支开发。
* 遇到冲突时,搞明白冲突的原因,千万不要随意丢弃别人的代码。
* 产品发布后,记得打 tag,方便将来拉分支修 bug。
### Commit message 规范化
每次提交代码都要写提交说明 commit message,应该清晰明地说明本次提交的目的。
目前,社区有多种 commit message 的规范,其中 Angular 规范是目前使用最广的写法,比较合理和系统化,并有配套的工具。
```bash
$ git commit -m "hello world" # -m 参数,就是用来指定 commit mesage 的
$ git commit # 如果一行不够,可以只执行 git commit,就会跳出文本编辑器,让你写多行。
```
commit message 的作用
* 提供更多的历史信息,方便快速浏览
* 可以直接从 commit 生成 changelog (见下文)
```bash
$ git log HEAD --pretty=format:%s # 利用 pretty 配置单行显示
$ git log HEAD --grep feature # 利用 grep 筛选
```
### Angular 规范
https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines
#### Commit Message Format
```
(): # 必填,其中的 scope 是可选的
# 可选,每行长度不的超过 100 个字符