Skip to content

Git

Git Basics

git config

对于具体项目有三层 gitconfig 配置文件,分别是系统级 --system,用户级 --global,和项目级 --local,并且逐级覆盖(项目 .git 文件夹下优先级最高)
配置个人信息
每次提交都需要个人信息,所以需要配置

git config --global user.name "Shibo Xia"
git config --global user.email oscarsrc@outlook.com
--global 为针对用户的设置,如果有特殊需求,可以不加 --global,默认 --local
设置默认编辑器
git config --global core.editor emacs
Windows 下需要指定文本编辑器的完整路径
默认分支名
默认为 master,通过如下命令修改
git config --global init.defaultBranch main
显示具体配置
全部配置(相同名称以最后为准) git config --list
具体键值对 git config user.name
查询配置具体来源 git config --show-origin user.name

git help

查询指令帮助的三种方式

git help <verb>
git <verb> --help
man git-<verb>

git clone

指定文件夹名称的克隆

git clone https://github.com/libgit2/libgit2 mylibgit

git status

用于查看目前文件状态,会显示除去 unmodified 文件的状态
git status -s 缩略显示文件状态,输出比较少

git add

开始 track 或 stage 文件

.gitignore

Github 提供 .gitignore 模板:https://github.com/github/gitignore

• Blank lines or lines starting with # are ignored.
• Standard glob patterns work, and will be applied recursively throughout the entire working tree.
• You can start patterns with a forward slash (/) to avoid recursivity.
• You can end patterns with a forward slash (/) to specify a directory.
• You can negate a pattern by starting it with an exclamation point (!).
Glob patterns are like simplified regular expressions that shells use. An asterisk () matches zero or more characters; [abc] matches any character inside the brackets (in this case a, b, or c); a question mark (?) matches a single character; and brackets enclosing characters separated by a hyphen ([0-9]) matches any character between them (in this case 0 through 9). You can also use two asterisks to match nested directories; a/*/z would match a/z, a/b/z, a/b/c/z, and so on.

 # ignore all .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 any directory named build
 build/
 # ignore doc/notes.txt, but not doc/server/arch.txt
 doc/*.txt
 # ignore all .pdf files in the doc/ directory and any of its subdirectories
 doc/**/*.pdf

git diff

git diff 更加详细的 git status,具体到哪行被修改,默认不显示 staged 的改变,只有 tracked 但是 unstaged 的部分
git diff --staged 比较已经 staged 的改变

git commit

git commit 提交默认打开编辑器编辑信息
git commit -v 在编辑器的注释里显示更多更改的信息
git commit -m msg 行内编辑信息
git commit -a 会让 git 自动执行 stage 的步骤,stage 所有 tracked 文件
如果上一次提交忘记了 stage 需要的文件或者输入了错误的 commit message,可以用 git commit --amend 填补上一次提交,示例如下:

git commit -m 'Initial commit'
git add forgotten_file
git commit --amend
需要注意,并不是完全覆盖,而是一种补充

git rm

git rm 用于删除文件,如果只是用系统命令删除,还需要额外通过 git rm 去 stage 这次修改,如果在删除之前该文件位于 staging area 或者处于 modified 状态,那么还需要 -f 参数防止误操作
如果有文件被添加到 staging area 但是希望从该状态变成 untracked 并保留源文件(比如忘记添加 .gitignore 或者误添加的编译文件和日志等)

git rm --cached README
还可以使用和 .gitignore 类似的语法
git rm log/\*.log
这里的 \ 用于转义 *,防止命令行提前解析 *,确保 * 可以留给 git 解析

git mv

git mv file_from file_to 用于重命名文件,和一下方式同理:

mv README.md README
git rm README.md
git add README

git log

git log 查看 commit 历史
-p--patch 参数展示具体每次提交的区别
-2 显示最近两次
--stat 更简明的更改说明
--pretty=oneline|short|full|fuller 美观输出 ``bash $ git log --pretty=format:"%h - %an, %ar : %s" ca82a6d - Scott Chacon, 6 years ago : Change version number 085bb3b - Scott Chacon, 6 years ago : Remove unnecessary test a11bef0 - Scott Chacon, 6 years ago : Initial commi

`--graph` 输出文字图像,通常配合 `--pretty` 中的 `oneline` 和 `format` 使用更美观,如 `git log --pretty=format:"%h %s" --graph`  
`git log -S function_name` 查找某个字符串出现次数改变的 commit  
`git log -- path/to/file` 查找文件在哪些 commit 被改变  
`--no-merges` 排除 merge commit  
`--abbrev-commit` 尽可能短显示 checksum  

### git reset

`git reset HEAD <file>` 将文件从暂存区移除,通常用于有些文件没改好但是不小心更新到暂存区,或者有时为了保持 commit 的单一与纯粹,注意和 `git rm --cached` 的区别,后者是让 git 不再追踪某文件  

### git checkout -- filename

`git checkout -- <file>` 用于将 `modified` 的文件恢复到上一次 commit 或克隆好的状态,注意慎重使用  

### git restore

`git restore --staged <file>` 将文件从暂存区移除,恢复到 `unstaged` 状态  
`git restore <file>` 恢复到上一次 commit 的状态  

### git remote

`git remote` 查询配置好的远程服务器,如果为克隆,则至少有 `origin`,这是 Git 给予源服务器的默认名称  
`-v` 同时显示每个名称对应的 URL  
`git remote add <shortname> <url>` 添加远程地址,添加之后就可以用缩略名代替整个地址,添加之后可以用 `git fetch pb` 从 Paul 的仓库拉取信息,拉取之后 Paul 的 master 分支会出现在本地的 `pb/master`  
`git remote show <remote>` 显示和远程仓库的状态并显示每个分支的 track 状态  
`git remote rename pb paul` 把缩略名 pb 改成 paul  
`git remote rename paul` 删除某个远程连接  

### git fetch

`git fetch <remote>` 从远程地址拉取所有本地没有的分支,如果是克隆仓库,那么克隆的地址就自动变成 `origin` 的内容  

### git pull

如果某一个本地分支被设置好追踪某一个远程分支,则 `git pull` 可以自动拉取那个远程分支并且合并  
>From Git version 2.27 onward, git pull will give a warning if the pull.rebase variable is not set. Git will keep warning you until you set the variable.  
>If you want the default behavior of Git (fast-forward if possible, else create a merge commit): git config --global pull.rebase "false"  
>If you want to rebase when pulling: git config --global pull.rebase "true"

### git push

`git push <remote> <branch>`  
将某个分支上传到远程服务器,必须要有写权限,如果其他人更早就 push,必须要先 fetch 并且整理好 commit 才能再上传  

### git tag

`git tag` 列出所有 tags  
`git tag -l "v1.8.5*"` 带有通配符的查找  
tag 分两种,一种是 lightweight tags,只是指向某一次提交的指针,一般用于快速标记,另一种是 annotated tags,包含更多信息,并且作为对象储存,一般用于发布版本,是里程碑  
annotated tags 创建命令:`git tag -a v1.4 -m "my version 1.4"`  
`git show v1.4` 展示 tag 的信息  
创建 lightweight tags:`git tag v1.4-lw`  
对于之前的某一次 commit 创建 tag:`git tag -a v1.2 9fceb02` 后面是某次提交的 checksum(或者一部分)  
`git push` 默认不上传 tags 到服务器,需要 `git push origin <tagname>` 上传某一次 tag  
>`git push <remote> --tags` will push **both lightweight and annotated tags**. There is currently no option to push only lightweight tags, but if you use `git push <remote> --follow-tags` only **annotated tags** will be pushed to the remote.

删除 tag:`git tag -d v1.4-lw`  
删除同步到服务器:`git push origin --delete <tagname>` 或 `git push origin :refs/tags/v1.4-lw`(冒号前相当于传了空值)  
切换到 tag:`git checkout v2.0.0`,这样会看到文件同时到达 detached head 的状态,可以 commit,但是无法在任何分支找到,除非是用 checksum 值,为了在某个 tag 修改或者修复 bug,可以用 `git checkout -b version2 v2.0.0` 创建新分支 version2 做修改  

### git alias

别名,用于简化命令,例如
```bash
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
之后就可以用co代替commit,还可以git config --global alias.unstage 'reset HEAD --'` 简化复杂的命令

Git Branching

分支的本质就是创建一个指针指向某一次 commit,而每一次 commit 内其实存储着对应的 tree,parent(s) 和其他作者信息等

git branch

git branch testing 创建名为 testing 的分支,指向 HEAD 指向的分支对应的 commit 或者直接是 HEAD 指向的 commit(处于 detached head 状态),但是创建分支仅仅是创建了指针,并没有改变 HEAD,可以用 git log --decorate 查看分支指针指向哪一次 commit
git checkout testing 会改变 HEAD 指针的位置,切换到 testing,切换之后,用 git log 只显示当前分支有关的 commit
git log testing 查看特定分支的历史
git log --all 查看所有提交
git log --oneline --decorate --graph --all 树状形式展示所有提交

git checkout

git checkout -b <newbranchname> 创建同时切换到分支

Switch to an existing branch: git switch testing-branch.
Create a new branch and switch to it: git switch -c new-branch. The -c flag stands for create, you can also use the full flag: --create.
Return to your previously checked out branch: git switch -.

一般要做修补的时候,先切换到生产分支,再创建分支做修补,测试通过后再合并到生产分支,例如:

git checkout -b iss53
vim index.html
git commit -a -m 'Create new footer [issue 53]'
git checkout master
git checkout -b hotfix
vim index.html
git commit -a -m 'Fix broken email address'
git checkout master
完成上述操作,目前应该处于 master 分支,假设 master 分支指向 commit C2,hotfix 指向 C4,iss53 指向 C3,现在要把 hotfix 合并到 master,只需要 git merge hotfix,由于 hotfix 完全领先于 master,基于 master,所以合并会非常简单快捷,git 会直接把 master 的指针前移,合并完成后,可以用 git branch -d hotfix 删除 hotfix 分支,此时返回到 iss53 的工作:
 $ git checkout iss53
 Switched to branch "iss53"
 $ vim index.html
 $ git commit -a -m 'Finish the new footer [issue 53]'
 [iss53 ad82d7a] Finish the new footer [issue 53]
 1 file changed, 1 insertion(+)
现在假设 iss53 指向 C5 commit,此时想把 iss53 合并入 master:
 $ git checkout master
 Switched to branch 'master'
 $ git merge iss53
 Merge made by the 'recursive' strategy.
 index.html |    1 +
 1 file changed, 1 insertion(+)
由于此时的结果不同于之前的两次 commit,所以 git 会自动创建 commit C6,并改变 master 指针的位置,当然也会要求输入提交信息
如果两个分支的文件内部有冲突,git 会暂停 merge 指令并要求解决冲突,此时用 git status 可以查看状态,并且 git 会主动在文件有冲突的部分增加标识符,可以解决冲突后用 git add <filename> stage 有冲突的文件告诉 git 冲突已经被解决(这个过程可以用 git mergetool 替代),然后再用 git commit 可以提交 merge commit

更多分支操作

git branch 查看所有分支
git branch -v 查看所有分支的最后一次提交
git branch --merged 查看所有已经合并到当前分支的分支,反之,没有合并的分支用 git branch --no-merged 查看,这两个指令也可以提供参数,查询任何分支对应的(未)合并分支
git branch -d 分支名 可以删除已经合并的分支,对于没有合并的分支,如果要强制删除,需要遵照提示
git branch --move bad-branch-name corrected-branch-name 为分支(除了主分支以外的)改名,改名后如果要同步到服务器,需要 git push --set-upstream origin corrected-branch-name,其中 --set-upstream 为设置上游分支(就是之后再用 git pushgit pull 的时候不用再设置上游分支,否则 git 不知道本地分支对应远程哪个分支,如果不设置但是新建分支,第一次会在远程新建一个同名分支但是之后上传拉取会遇到问题)
git push origin --delete bad-branch-name 然后需要把远程旧分支删除
更改主分支名字(牵连甚广,要慎重)

git branch --move master main
git push --set-upstream origin main
git push origin --delete master
表面上更名成功,实际上还要更改所有牵涉到项目的配置文件等

远程分支

git ls-remote <remote> 展示所有远程指针(包括分支、标签等等),git remote show <remote> 展示所有分支
git fetch origin 会更新 origin 包含的所有分支的内容,这时比如远程 master 的内容可以在 origin/master 找到
git push origin serverfix:awesomebranch 把本地的 serverfix 分支推到 awesomebranch 分支,如果同名,可以直接写一次分支名
git checkout -b serverfix origin/serverfix 在本地创建分支并且 track 远程分支,缩略形式 git checkout --track origin/serverfixgit checkout serverfix 效果相同
设置分支的追踪关系:git branch -u origin/serverfix
git branch -vv 查看分支的追踪状态,但是这里的状态其实是和本地的远程缓存对比,如果要最新的还需要 git fetch --all
删除远程分支:git push origin --delete serverfix

git rebase

rebase 相当于把相对于该分支共同祖先做的操作对于目标分支再做一遍

git checkout experiment
git rebase master
先切换到 experiment,然后再把 experiment 对于之前 commit 做的操作对 master 做一遍,此时 experiment 领先于 master,可以再
git checkout master
git merge experiment
 ```
 experiment 合并到 master(因为此时 experiment 可以认为领先于 master 并且基于 master,所以图形很清晰)
```bash
git rebase --onto <new-base> <upstream> <branch>
--onto 可以做更精准的有基底的变基,<new-base> 是要变基到的新位置,upstream 是想剪掉的起始提交,branch 是要变基的分支(可以省略,默认当前分支)
比如提交图:
A---B---C---G---H (feature)
     \
      D---E---F   (main)
想要把 G 和 H 应用到 F 上,可以 git checkout feature;git rebase --onto F C,此时提交图变成:
A---B---C
     \
      D---E---F---G'---H'  (feature)
然后再进行 main 和 feature 的合并即可
git rebase <basebranch> <topicbranch> 不切换分支完成变基
注意:避免分支在被推送到远程并且有其他人在使用该分支时变基!
如果其他人强行对远程分支变基,git 也能够自动识别并且在本地可以用 git fetch;git rebase <branch> 强行挽回错误(仅限于少数特殊情况),简化可以运行 git pull --rebase

If you are using git pull and want to make --rebase the default, you can set the pull.rebase config value with something like git config --global pull.rebase true.

分布式 Git

Integration-Manager Workflow

 1. The project maintainer pushes to their public repository.
 2. A contributor clones that repository and makes changes.
 3. The contributor pushes to their own public copy.
 4. The contributor sends the maintainer an email asking them to pull changes.
 5. The maintainer adds the contributor’s repository as a remote and merges locally.
 6. The maintainer pushes merged changes to the main repository.

Dictator and Lieutenants Workflowhierarchical

1. Regular developers work on their topic branch and rebase their work on top of master. The master branch is that of the reference repository to which the dictator pushes.
 2. Lieutenants merge the developers' topic branches into their master branch.
 3. The dictator merges the lieutenants' master branches into the dictator’s master branch.
 4. Finally, the dictator pushes that master branch to the reference repository so the other developers can rebase on it.

协作注意事项

空白字符问题,由于编辑器和系统不同,开发者可能有空白字符问题,可以用 git diff --check 检查
git add --patch 可以交互式逐块选择是否 stage 该更改,这样可能一个文件不同修改部分用于不同的 commit
git log --no-merges issue54..origin/master 显示在 origin/master 有但是在 issue54 没有的 commit
为要发起的 pull request 生成邮件文本:
git request-pull origin/master myfork
myfork 是远程仓库缩略名
如果 featureA rebase 到 origin/master 改变了分支结构,需要用 git push -f myfork featureA 强制 push(因为此时 featureA 不再是 myfork/featureA 的后代)
git merge --squash featureB 可以把分支 featureB 捏到现有分支上,此时不会自动执行 merge commit,可以再做修改,然后 git commit,但是这次 commit 会显示只有一个父 commit,--no-commit 也可以起到只合并不提交的效果
Public Project over Email 需要生成补丁文件然后通过邮件传给开发者,git format-patch -M origin/master 生成补丁文件,git 还可以配置 IMAP 发送邮件,管理者用 git am 0001-limit-log-function.patch(如果用 format-patch 生成补丁)git apply /tmp/patch-ruby-client.patch (如果用 diff 生成)可以应用补丁,然后可能还涉及到一些冲突问题等,需要更多命令解决
git log contrib --not master 查看不属于 master branch 的 commit,然后还可以用 -p 显示具体差异
查找共同祖先 git merge-base contrib master
然后用返回的 checksum 值再用 git diff 36c7db 就能知道差异,两个命令等同于 git diff master...contrib
git tag -s v1.5 -m 'my signed 1.5 tag' 创建一个 signed tag,相当于比 annotated tag 多了签名 ``bash gpg --full-generate-key # 生成密钥,问到姓名和邮箱最好和 git 配置相同,位数建议 4096,更安全 gpg --list-secret-keys --keyid-format LONG # 查看密钥 id,其中 id 类似于 3AA5C34371567BD2 git config --global user.signingkey 3AA5C34371567BD2 # 配置签名密钥

接下来还需要能够分发公钥(公钥用于验证签名)  
`gpg -a --export 3AA5C34371567BD2 | git hash-object -w --stdin` 添加公钥为 git 对象,`--export` 后面跟密钥的 id  
得到 hash 输出以后,用 `git tag -a maintainer-pgp-pub 659ef797d181633c87ec71ac3f9ba29fe5775b92` 创建标签,push 时用 `git push --tags` 其他人就可以从 tag 查看公钥从而验证签名,如果任何人想验证,需要 `git show maintainer-pgp-pub | gpg --import`,此时公钥位于钥匙串中,需要 `git tag -v <tag-name>` 校验,同理 commit 也可以签名,还可以设置自动签名  
`git describe master` 生成描述,`最近的 tag 名-距离 tag 的提交次数-g部分哈希值` 例如 `v1.6.2-rc1-20-g8c5b85c`,默认检索注释标签或者签名标签,使用 `--tags` 可以把轻量标签也算在内,其中返回的 describe 可以作为 `git show` 和 `git checkout` 的目标  

### 发布

```bash
git archive master --prefix='project/' | gzip > `git describe master`.tar.gz
生成压缩包,项目文件存储在压缩包内 project 文件夹下 生成 zip 包,可以
git archive master --prefix='project/' --format=zip > `git describe master`.zip
git shortlog --no-merges master --not v1.0.1生成v1.0.1` tag 以后的 master 分支的 changelog(输出以人名分组)

Git Tools

git rev-parse

git rev-parse topic1 显示分支对应的 hash 值

git reflog

git reflog 展示 HEAD 指针和分支指针的变化,根据显示还可以 git show HEAD@{5}git show master@{yesterday} 查看详细信息等,想要 git log 时带有 reflog 信息,需要 git log -g master
有时候使用 Powershell,大括号需要被转义

git show HEAD@{0}     # will NOT work
git show HEAD@`{0`}   # OK
git show "HEAD@{0}"   # OK

^

git show HEAD^^ (caret) 置后展示父级元素,同理需要转义

git show HEAD^^    # OK
git show "HEAD^"   # OK
d921970^2 后面接 2 代表第 2 个父元素(仅对 merge commit 有效)

~

一个时代表父元素,同 ^HEAD~2 代表父元素的父元素,即第二层父元素,类似地还可以 HEAD~~~ 第三层父元素,两种符号还可以组合运用

..

git log master..experiment 展示 master 没有而 experiment 含有的分支
git log origin/master..HEAD 展示和远程分支差多少,也可以 git log origin/master..,当省略时默认为 HEAD

前置 ^--not

git log ^refA refB # 在 refB 但是不在 refA 的 commit
git log refB --not refA # 另一种表示方法

...

git log master...experiment 两个分支不都共有的 commit,这时加上 --left-right 显示每个 commit 所属的分支

git add -i

git add -i 交互式 stage,例如 u2 代表要开始 stage 文件,会有交互式的逐块选择,即先选择要做的操作,再选择操作涉及的代码块

git stash

暂存功能用于更改并未完成,不想提交 commit 但是有需要切到其他分支
有时修改以后,git status 可以看到 modified,但是想切换分支,这时用 git stash 或者 git stash push 就可以暂存,再用 git status 可以发现工作环境干净,可以切换分支,git stash list 查看暂存情况,git stash apply 恢复刚暂存的更改,后面加编号恢复特定更改
git stash apply --index 同时把之前 stage 的文件重新 stage
git stash drop stash@{0} 在应用之后,删除一次 stash
git stash pop 应用后删除暂存
--keep-index 在 push 时保留暂存区的内容(同时也会 push 暂存区),--include-untracked 保留 untracked 文件(不包括 .gitignore 提到的文件),如果还需要保留 ignore 的文件,需要 --all
git stash --patch 分块选择是否 stash
有的时候直接恢复 stash 会有合并冲突,可以 git stash branch <new branchname> 直接把 stash 恢复到新分支(stash 时的 commit 和保存的修改)
git clean -f -d 删除未跟踪的文件和目录,不包括 .gitignore 忽略的文件,还有其它参数涉及到忽略的文件

git grep

git grep 可以用于查找文件中的内容,有很多参数选项