Git

https://tom.preston-werner.com/2009/05/19/the-git-parable.html

Git基于SHA-1的对象存储机制会创建 .git/objects/3b/18e12dba79e4c8300dd08e728b8dad 这样的文件路径.
其中3b是SHA1的第一个字节, 之所以为第一个字节创建单独目录, 是因为一些文件系统在单个目录处理大量文件的效率很差,
该方法可以均匀创建256路分区缓解此问题.

  • HEAD 当前分支的最近提交
  • ORIG_HEAD 合并(merge)和复位(reset)时, 会用 ORIG_HEAD 指向旧版本的 HEAD.
  • FETCH_HEAD 最近抓取(fetch)分支的 HEAD, 该引用仅在抓取之后短暂有效.
  • MERGE_HEAD 合并(merge)进行时, 指向合并的分支(ORIG_HEAD 是master, MERGE_HEAD 是branch).

C~ / C~1 上一代提交(在只有一个父提交的情况下与 C^1 相同)
C~~ / C~1~1 / C~2 向上两代提交(即 C~1C~1)

C^C^1 第一个父提交(合并的提交拥有多个父提交)
C^^C^1^1C^2 第二个父提交(合并的提交拥有多个父提交)

提交范围的本质是"图的可达性"
{commit1}..{commit2} 指从commit1到commit2的所有提交(不包含commit1, 语义上commit1应该是commit2的祖先)
^{commit} 在范围内排除特定提交

{branch1}..{branch2} 技术上和commit范围一样, 语义上表示在branch2却不在branch1上的所有提交.

..{commit}HEAD..{commit} 的缩写
{commit}.. 即{commit}..HEAD 的缩写

{commit1}...{commit2} 取commit1和commit2的对称差(极少使用),
即取 commit1可达但commit2不可达的祖先提交 + commit2可达但commit1不可达的祖先提交

git config --global user.name "用户名"
git config --global user.email "电子邮箱"

配置文件为 ~/.gitconfig

git config --global alias.show-graph 'log --graph --abbrev-commit --pretty=online'
该命令添加了show-graph别名

git clone --bare existing_repo bare_repo_path 克隆为裸仓库
git clone --bare existing_repo 克隆裸仓库到当前目录

git remote add github git://github.com/BlackGlory/something.git 添加远程仓库

git fetch github 从上游获取最新代码
git merge github/master 合并上游master代码至当前分支

git fetch origin pull/{id}/head:{name} 获取某个PR的分支, 并命名

这种push的缺点在于push完后不会自动更新local/master, 需要用fetch重新拉取local/master.
如果项目需要同步推送到多个上游, 说明项目很可能处于一种不良模式, 应尽可能避免使用到此技巧.

# 让push的时候push到origin(不可省略, 否则不会push到origin)
git remote set-url --add --push origin $(git remote get-url origin)
# 让push的时候push到local
git remote set-url --add --push origin $(git remote get-url local)

git fetch --all

git pullgit fetchgit merge 的组合命令.

git pull --rebase 以rebase方式拉取, 本地commit会变基到最新的远程commit上.
注: rebase时, 需要用 git rebase --continue 而不是 git commit 提交冲突

第一行: 简单描述提交内容
第二行: 空行
第三方以后: 详细陈述提交内容

中止提交: 提交信息留空并退出编辑器

git commit --amend 重新编辑上一次提交(如果使用 git add, 则可以将新的修改并入此次提交)
git commit --amend --no-edit 重新提交

git branch {分支名} 创建分支
git checkout {分支名} 切换分支
git checkout - 切换到上一个分支
git checkoub -b {分支名} 创建并切换分支
git log --graph 以图表形式查看分支

git branch -a 查看包括本地和远程仓库的所有分支

git branch -m <oldname> <newname> 重命名本地分支
git branch -m <newname> 重命名本地当前分支

git merge-base {commit1} {commit2} 返回两个提交的共同祖先

git stash 暂存当前修改
git stash --include-untracked 暂存当前未提交的跟踪文件和未跟踪的文件(常用)
git stash apply 恢复最后一次暂存的文件
git stash pop 恢复最后一次暂存的文件并删除暂存(常用)

git stash list 查看全部的暂存列表
git stash drop {name} 删除暂存
git stash clear 删除全部暂存

不要在主分支等公共分支上使用rebase, 这会破坏历史.

git rebase {branch} 以变基的形式将分支并入当前分支

git rebase -i {commit} 以修改历史的形式变基到 {commit}, 例如从分支变基到最新的master
git rebase -i HEAD~5 以修改历史的形式变基最后5次提交

squash意味着此提交将合并到先前的提交中, 包括此提交的message
fixup意味着此提交将合并的先前的提交中, 但不包括此提交的message

git commit --fixup HEAD 创建提交作为HEAD的fixup, 为autosquash作准备
git commit --squash HEAD 创建提交作为HEAD的squash, 为autosquash作准备

git rebase -i --autosquash {commit} 在交互式编辑器里, 将自动处理先前提交的fixup和squash

为交互式rebase默认开启autosquash:
git config --global rebase.autosquash true

git merge --no-ff {分支名} 将对应分支合并至当前分支, 但需要手动填写合并提交信息
git merge --no-commit --no-ff {分支名} 手动进行合并

git merge --squash branch
将分支里所有commit对文件的修改添加到当前stage, 执行 git commit 以完成合并.
最终, 这相当于"将分支里的commit压缩(squash)为一个commit进行合并", 该commit默认情况下会以squasher为作者, 所有被压缩的commit为父commit.

git show {commit} 查看commit包含的修改
git show next:{filename} 查看filename在next分支上的样子

git log {filename} 查看目录或文件的日志
git log -p {filename} 查看包含diff的日志
git log --follow {filename} 查看该文件的历史日志
(不使用follow的情况下, 由于重命名等原因会不显示重命名之前的日志)

git log --oneline 单行显示日志
git log --oneline --graph 单行显示日志并打印节点图

git diff HEAD 将工作树与HEAD(最后一个提交)进行比较

git clean -fd 删除本地所有未追踪文件和目录

git reset --hard {commit} 将当前分支(HEAD)退回指定commit, 也用于撤销合并等操作
git restore . 与上一条作用相同

git clean -fd && git restore . 恢复至上一次提交(放弃提交内容)

git reset --soft HEAD~ 撤销上一次提交(保留改动内容)

git checkout {commit} -- path/to/file 从指定提交中恢复指定文件

git tag {tagName} 创建轻量级, 无注释(non-annotated)/轻量级标签.
git tag --annotate {tagName} 创建有注释(annotated)的标签

git tag -n99 查看所有标签及它们携带的消息(non-annotated标签会显示commit消息).

使用annotated标签被认为是最佳实践,
因为这种标签会生成对象, 记录标签的创建者, 创建日期和额外的标签消息
(在实际使用中, 标签消息没什么实际用处, annotated标签主要功能主要是为了记录标签创建者和创建时间).

由于这个原因, non-annotated标签被认为适合私人和临时性使用, 而annotated标签被用于发布.

两者的区别可以通过 git describe 体现出来, annotated标签能够被直接查询,
而non-annotated标签需要额外加上 --tags flag.

git push --follow-tags 在push的同时向远程发送 有注释 标签.
git push origin :{tagName} 删除远程tag, 与下一条等同
git push --delete origin {tagName} 删除远程tag, 与上一条等同

git revert HEAD 创建一个反转上一个提交的提交

git revert {commit}..HEAD 反转从特定提交开始至今的所有修改.

查看git的历史提交缓存, 从中可以抢救出那些被误删的提交.

git reset [email protected]{index} 前往reflog列出的指定提交.

cherry-pick可将一个或多个提交的 更改 抓取到当前分支上.
通常用于跨分支打补丁的场景, 当不想合并整个分支的时候, 使用cherry-pick.

自Git 2.23加入的新命令, 用于切换分支.
功能与checkout基本一样, 专门分离出一个命令是因为checkout承载了太多的功能.

Git 2.23加入的新命令.

git restore path/to/file
相当于
git checkout HEAD -- path/to/file

恢复指定提交的文件(缩写为 -s)
git restore --source commit path/to/file

和subtree不同, submodule将仓库作为引用加入到当前仓库中.

submodule会向仓库添加 .gitmodules 文件记录子模块仓库的元数据, 此文件应该被纳入版本控制.

添加子模块:
git submodule add 子模块的仓库地址 子模块路径

克隆具有子模块的项目:

git clone --recurse-submodules 项目地址
# 以上命令等价于以下步骤
git clone 项目地址
cd 项目地址
git submodule update --init
# 以上命令等价于以下步骤
git clone 项目地址
cd 项目地址
git submodule init # 作用是将`.gitmodules`复制到`.git/config`里
git submodule update

获取同时更新任何嵌套的子模块:
git submodule update --init --recursive

  • 期望仓库是嵌套的(一个仓库里有另一个仓库), 而不是多个仓库合并成的仓库.
  • 希望能更容易切换子模块的提交(子模块只跟踪提交而不是分支/标签).
  • 希望直接在当前仓库里贡献代码到子模块的仓库.
  • 子模块可能频繁的创建和删除.
  • 子模块永远具有可访问性.

和submodule不同, subtree将仓库作为副本加入到当前仓库中.

默认情况下会包括仓库原有提交, 这很适合将polyrepo迁移至monorepo:
git subtree add --prefix=子树路径 子树仓库的仓库地址 子树仓库的提交(或分支)

使用--squash选项可以在不包含仓库原有提交的情况下, 添加仓库为子树:
git subtree add --prefix=子树路径 --squash 子树仓库的地址 子树仓库的提交(或分支)

从源仓库拉取提交:
git subtree pull --prefix=子树路径 子树仓库的地址 子树仓库的提交(或分支)
注意, 由于子树对Git是透明的, 因此每次pull实际上都是直接对比远程仓库和本地仓库, 然后进行merge.

子树是来自社区的名为git-subtree的脚本, 而非Git的核心功能.
因此当一个项目被作为子树添加时, Git是不知道子树这个概念的, 它对于Git来说是透明的.
这也是为什么每次调用子树命令都需要使用额外的参数, 它其实更像是子模块的手动版本.

  • 基本上不需要向外部项目反馈代码时
  • 希望能够很方便地编辑引用的外部项目的内容
  • 希望将两个项目的源代码彻底合并(即希望将外部项目废弃)
  • 不需要其他使用者掌握额外的知识(子模块会引入很多复杂的新知识)

情况: 操作者对该案例的原始仓库A没有控制权, 对Pull Request的仓库B也没有控制权.
目标: 操作者需要修改他人PR里的分支, 发送新的PR给仓库B, 以便原始PR能够被成功合并仓库A.

  1. 1.
    git clone 仓库B的URL 克隆仓库B
  2. 2.
    git remote add target 仓库A的URL 将仓库A作为远程名称target加入到当前仓库
  3. 3.
    git fetch --all 下载仓库A和仓库B的所有远程分支
  4. 4.
    git checkout PR分支 签出仓库B里的PR分支
  5. 5.
    git checkout -b patch 创建一个B
  6. 6.
    git merge target/master 合并来自仓库A的最新代码, 不能使用变基, 因为仓库B没有仓库A的最新代码

另一种方案是直接fork仓库A, 用cheery-pick的方式将仓库B的分支捡进来.
虽然这种方案不方便发送PR给仓库B, 但直接发送PR给仓库A在结果上是一样的,
尤其是对那些很久都没有合并的PR而言, 原来的提交者可能早就不关心这件事了.

  1. 1.
    git checkout -b someone-patch-1 master 为它人的PR创建本地branch并切换至此branch
  2. 2.
    git pull https://github.com/someone/repo.git patch-name
    将他人仓库的patch-name分支下载(fetch)并合并(merge)进此分支
    经过试验, 这里使用git fetch替换git pull的效果是一样的
  3. 3.
    git checkout master 切换回主分支, 准备合并
  4. 4.
    git merge --no-ff someone-patch-1 合并
  5. 5.
    git push origin master 合并完成后上传

git tag | xargs git push --delete origin 清除所有远程tag
git tag | xargs git tag -d 清除所有本地tag

git grep <regexp> $(git rev-list --all) 搜索该仓库从创建至今的所有历史文本