参考:
https://missing.csail.mit.edu/
https://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html
第 6 课:Git
本节课的讲法是先大致讲清 Git 的数据模型(实现),再讲操作命令。个人认为很受用,强烈推荐。这里仅记录 Git 数据模型等内部相关的东西,关于 Git 的使用参见这里 。
一个数据模型的例子如下:
Copy <root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, content = "hello world")
|
+- baz.txt (blob, content = "git is wonderful")
blob、tree、commit、object、reference
注:这些东西都可以在 .git
目录中探索。
在 Git 中,文件被称作 blob。目录被称作 tree。commit 包含了父 commit,提交信息、作者、以及本次提交的 tree 等信息。object 则可以是前三者的任意一个。进一步,对每个 object 进行哈希,用哈希值代表每个 object。注意 object 是不可变的,而 reference 是一个映射(指针),字符串(例如 master)映射到 object 的哈希值。所以,一个仓库可以看作是一堆 object 与一堆 reference(即objects 与 references),Git 命令大多数是在操作 object 或者 reference,具体地说,是增加 object ;增加/删除/修改 reference(即改变其指向)。伪代码如下:
Copy type blob = array<byte>;
type tree = map<string, blob | tree>;
type commit = struct {
parent: array<commit>
author: string
message: string
snapshot: tree
}
type object = blob | tree | commit
objects = map<string, object> // objects[hash(object)] = object
// 只提供用 hash 值查找 object 以及对 objects 增加的接口
references = map<string, string> // 前一个 string 表示指针名,例如:master, 后一个 string 表示 object 的哈希值,这里的指针名的例子是:分支名、HEAD
workspace、stage、version
以下参杂个人理解:这三者实际上都是一个版本/快照,而所谓的快照基本上等同于一个 tree/commit 对象。
workspace 表示工作区,也就是 .git
目录以外的所有内容,workspace 可以看作是一个快照;
stage 是为了方便用户使用的一个机制,比如说开发了一个新特性,将其放入缓冲区,之后再增加了一些调试代码,那么提交时可以不提交调试代码。stage 中的信息被保存在了 .git/INDEX
文件内,stage 可以看作是一个快照;
version 则是历史提交的版本,因此实际上是若干个快照。
许多命令例如:git add
,git diff
,git restore
实际上就是利用上述三者之一修改/比较另外一个或多个快照。
branch、HEAD
每个 branch 都是一条直线的提交序列(一系列的 commit 对象),每个 branch 都有一个名字,例如 master、dev 等,它们指向其中的一次提交。git commit
命令实际上的作用是生成一个 commit 对象后,将生成的 commit 对象的父亲设置为当前的分支名指向的提交,而后将分支名指向的对象设置为新生成的对象,伪代码如下:
Copy def git_commit ( branch_name ):
new_commit = make_commit (stage)
old_commit = branch_name . current_commit
new_commit . parent = old_commit
branch . current_commit = new_commit
而 HEAD 指的是当前状态下的最近一次的提交。通常情况下,HEAD 总是与某个分支名的指向相同。但在某些情况下,HEAD 指向的提交不与任何的分支名的指向一致,称为 detached 的状态,例如使用类似如下的方式切换分支:
Copy git checkout a7141d0
git checkout HEAD~3
git checkout master~3
本质上而言,分支名与 HEAD 都是 reference 对象中的元素,存储的都是一个特定的 commit_id。而整个版本库存放的东西无外乎是一堆 commit 对象(每个 commit 对象包含指向其父亲的指针以及一个 tree 对象)。
.git
目录
依次执行
Copy git config --global user.name "BuxianChen"
git config --global user.email "541205605@qq.com"
git init
echo "abc" > a.txt
git add . # add_1
git commit -m "a" # commit_1
mkdir b
echo "def" > b.txt
git add . # add_2
git commit -m "b" # commit_2
git branch dev # branch_1
得到如下目录(省略了一些目录及文件)
Copy .git
│ HEAD # 文件内容是 refs/heads/<分支名>
│ index # add_1, add_2
│ ...
├─logs
│ ...
├─objects
│ ├─24
│ │ c5735c3e8ce8fd18d312e9e58149a62236c01a # blob (./b/b.txt), add_2
│ ├─3e
│ │ bc756fee46dfcb9410ab7f07980a8ff0e71d82 # commit, commit_2
│ ├─43
│ │ 8e5d5f895ccf4910e1a463ff5f31e52c28df3c # tree (./), commit_2
│ ├─83
│ │ edaf0d7f419929b1b0b84c8a7550f38daf97ac # tree (./b), commit_2
│ ├─8b
│ │ 3d54f8c5d0ebd682ea6e83386451e96a541496 # tree (./), commit_1
│ │ aef1b4abc478178b004d62031cf7fe6db6f903 # blob (./a.txt), add_1
│ ├─f7
│ │ 496edd08d97d10773a6a76eabd9d24d96785c2 # commit, commit_1
└─refs
├─heads
│ dev # branch_1, 文件内容是某个 commit 的哈希值
│ master # 文件内容是某个 commit 的哈希值
└─tags
注释项代表该条命令运行之后生成了该文件。除了 objects
目录以及 index
文件外,其余均为文本文件。为了获取 objects
目录及 index
文件的内容,可以使用以下两行命令:
Copy git cat-file -p 24c5735c3e8ce8fd18d312e9e58149a62236c01a # 查看 objects 目录下的文件内容
git ls-files -s # 查看当前缓冲区内容, 即 .git/index 中的内容
结论:
git commit
命令会同时生成 tree 和 commit 对象
HEAD
指向某个分支名,git checkout <分支名>
会同时修改 HEAD
及 index
的内容,并且切换分支时 git status
的结果必须为 clean
,否则无法执行。
object 的 hash 值
一个 blob 对象的hash值的计算过程如下:
假定 readme.txt
文件内容为123
,它被 git add
的时候,objects
目录下会增加一个以二进制序列命名的文件
Copy d8/00886d9c86731ae5c4a62b0b77c437015e00d2
一共40位(加密算法为SHA-1),其中前两位为目录名,后38位为文件名
使用 python 可以用如下方式计算出来:
Copy import hashlib
# `header`+内容计算
# `header` = 文件类型+空格+文件字节数+空字符
hashlib . sha1 ( b 'blob 3\0' + b '123' ). hexdigest () # d800886d9c86731ae5c4a62b0b77c437015e00d2
hashlib . sha1 ( b 'blob 5\0' + '12中' . encode ( "utf-8" )). hexdigest ()
# ec493cf5f7f9a5a205afbc80d7f56dbb34b10600
# len('12中'.encode("utf-8"))
# '12中'.encode("utf-8")
术语
stage/cache/index/缓冲区/暂存区都是指的同一个东西
Git 命令简介
所有与 Git 有关的命令最好用 git bash 打开。当然,将 git.exe 所在目录加入到了 path 环境变量后,使用普通的 shell(例如:cmd, powershell, bashrc 等)基本也都没问题。所有的 Git 命令除了 git init
与 git clone
外,一般都要在 .git
的同级目录下执行。
git init
其效果是为当前目录建立一个 .git
目录,正常情况下不要去修改这个文件夹下的任何内容,可以这样理解:之后的每一条以 git 开头的命令执行后,git.exe 会依据命令内容对 .git
目录下的文件或依据 .git
目录内的文件对工作区进行修改。注意:
git init 命令不一定需要在空目录,是否在空目录下执行产生的 .git 目录内容是一样的。
若 .git 目录已存在, 使用 git init 命令的结果会重新初始化,一般不会这样用
git config
设置提交commit信息时的用户名及邮箱
备注: 此项设定与远程代码库的拉取/推送权限无关
Copy git config --global user.name "John Doe"
git config --global user.email johndoe@example.com
设置http协议远程库记住密码
Copy git config --global credential.helper store
git log
以下命令用于显示所有的提交信息
Copy git log --all --graph --decorate --oneline
git log --pretty=format: "%h %s" --graph
git reflog
Copy # 查看历史命令, reflog译为回流
# 注意输出的每一行前面的数字串是执行该条命令后的版本号
# 注意输出顺序由上到下: 由最新的命令到旧命令,
# 注意不是所有的命令都会保存, 只保留引起head发生变化的命令
git reflog
# 输出
# 5ef5acd (HEAD -> master) HEAD@{0}: reset: moving to 5ef5
# 3b34b14 HEAD@{1}: reset: moving to HEAD^
# 5ef5acd (HEAD -> master) HEAD@{2}: commit: append GPL
# 3b34b14 HEAD@{3}: commit: add distributed
# 271efb4 HEAD@{4}: commit (initial): wrote a readme file
git status
输出分为两部分:
Changes not staged for commit
: 显示工作区相对暂存区的变动记录
Changes to be committed
: 显示暂存区相对最近一次提交的变动记录
用下面的命令可以简化输出:
Copy git status --short/-s
# 以下为输出结果
# M README
# MM Makefile
# A lib/git.rb
# M lib/simplegit.rb
# ?? LICENSE.txt
左边的 M 表示工作区文件修改了(相对最近一次提交)并且放入了暂存区, 右边的 M 表示工作区文件修改了但是没有放入暂存区。此处表示 README
修改了,但是还没有使用 git add README
命令将其放入暂存区;lib/simplegit.rb
被修改后放入了工作区,之后未被修改过;Makefile
在工作区被修改后放入了暂存区,而后工作区又做了修改。
左边的 A 表示 lib/git.rb
是工作区相对最近一次提交新增的文件,并已经放入了暂存区。
??
表示 LICENSE.txt
是工作区相对最近一次提交新增的文件,但没有放入暂存区中。
git diff
Copy git diff HEAD -- readme.txt
git diff # 显示工作区相对于暂存区的修改
git diff --staged # 显示暂存区相对最近一次提交的修改
如果需要查看两个 commit 中某个文件的差异,可以使用如下
Copy git diff <source_commit_id> <target_commit_id> -- <filename>
例子及解释如下
source_commit_id 的行用 -
进行标识,target_commit_id 的行用 +
进行标识,没有变化的行用空格进行标识。
显示方式按差异块的形式呈现:
Copy @@ -6,12 +6,8 @@ from ..modules import Module
表示的是 source_commit_id 的第 [6, 6+12-1] 行的内容与 target_commit_id 的第 [6, 6+8-1] 行的内容有差异。
Copy $ # pytorch 源码
$ git diff v1.9.1 v1.6.0 -- torch/nn/parallel/data_parallel.py
diff --git a/torch/nn/parallel/data_parallel.py b/torch/nn/parallel/data_parallel.py
index d85d871a5d..86d2cf801d 100644
--- a/torch/nn/parallel/data_parallel.py
+++ b/torch/nn/parallel/data_parallel.py
@@ -6,12 +6,8 @@ from ..modules import Module
from .scatter_gather import scatter_kwargs, gather
from .replicate import replicate
from .parallel_apply import parallel_apply
-from torch._utils import (
- _get_all_device_indices,
- _get_available_device_type,
- _get_device_index,
- _get_devices_properties
- )
+from torch.cuda._utils import _get_device_index
+
def _check_balance ( device_ids ) :
imbalance_warn = """
git add/rm
git add
的作用是为工作区产生变化的文件生成 blob,并将这些文件添加至暂存区
如果 filename
在工作区存在,且与暂存区中的内容不一致或暂存区中没有该文件。具体执行过程为:首先为 filename
创建一个 object (blob)放在 .git/objects
下,之后将该 object 放入暂存区。
如果 filename
在工作区中不存在,且在暂存区中存在,那么效果等同于 git rm <filename>
。具体执行过程为:将暂存区中相应的 object 删除
git rm
的主要作用是在暂存区中删除文件,可以通过不同的参数选择是否也删除工作区中的文件
Copy git rm < fil e > # 从工作区与缓冲区中同时删除文件
git rm -f < fil e > # 修改过<file>后, 使用了git add <file>, 此时希望将文件从工作区与缓冲区删除
git rm --cached < fil e > # 只删除缓冲区中的<file>
git commit
Copy git commit
git commit -m "xxx"
git commit 命令表示将当前的暂存区放入版本库中,前者会打开 Git 默认的文本编辑器(可以使用 git config 进行设置)供开发者添加描述信息,在公司里开发项目推荐用这种方式。后者一般用于添加简略的描述信息,适用于不那么正式的个人项目中使用。
git reset
参考 Pro Git 7.7 节
git reset
操作当前分支的三种操作如下
Copy # commit_id 可以是git commit id或者分支名,或者用类似HEAD^来代表
git reset --soft < commit_i d >
git reset --mixed < commit_i d > # 加不加--mixed都一样
git reset --hard < commit_i d >
下面分别说明上面三条命令在做什么,首先假定当前分支为 foo
,那么此时 HEAD
也指向了 foo
。
git reset --soft HEAD^
表示的意思是将 foo
的指针指向次新一次的提交,而 HEAD
依然指向 foo
git reset --mixed HEAD^
表示的意思是在上一步的基础上,使用 foo
指向的提交更新暂存区
git reset --hard HEAD^
表示在执行前两步后,用暂存区的内容再更新工作区,这样三个区域便完全一致了
备注:这里有一个用法可以用在代码审查中,不确定是否为最佳实践。
VSCode中工作区相对于暂存区 的修改在代码行的左侧有 gutter indicators 进行标识,现在假设原始仓库的地址为:https://github.com/base/project
,而开发者foo将此代码库进行了 fork 操作,仓库地址为:https://github.com/foo/project
。经过代码修改过,首先更新了自己仓库的 feature
分支,之后提出 Pull Request 合并至原始仓库的 dev
分支。此时可以用如下方式在本地看出代码修改了哪些部分
Copy # git clone https://github.com/base/project # 原始仓库
git checkout dev # 注意严格地说这里需要切换到与PR相匹配的远程dev分支的commit处
git checkout -b foo-feature dev # 建立新的分支,并切换至foo-feature分支
# 备注:这种方式代码审查人可以自己手动将PR中不合理的地方进行改正
git pull https://github.com/foo/project feature # 将PR分支并入本地foo-feature
# 如果只希望利用VSCode的gutter indicators查看修改处,则可以临时使用如下命令
git reset dev # 将foo-feature直接回退至dev分支,并且暂存区也将更新
# 确认/修改后先回到原始的PR状态
git reset < origin_feature_commi t > # 可以通过git reflog命令查询
# git add and commit ...
# 审查完后可以将foo-feature删除
git branch
显示已有分支
Copy git branch # 显示本地分支
git branch -r # 显示远程分支
git branch -a # 显示远程与本地分支
git branch -vv # 显示本地分支以及与其关联的远程分支, 最多只能关联一个
创建删除分支
Copy git branch < 待创建的分支 名 > # 创建分支
git branch -d < 待删除的分支 名 >
git branch -D < 待删除的分支 名 > # 强制删除,即使被删除的分支还未被合并
为分支设定 upstream 分支,在 Git 中,每个分支至多只能有一个 upstream 分支。注意:这里的 upstream 与数据模型中的 parent 是不同的概念。设定后可以不加参数地使用 git pull/push/fetch。
Copy git branch --set-upstream-to < 远程仓库 名 > / < 远程仓库分支 名 > < 本地分支 名 >
类似地,可以使用如下命令来新建本地分支,并与远程分支关联。所谓建立关联, 就是关联后可以不加参数(不指定远程分支名)地使用 git pull/push/fetch
Copy git branch --track dev origin/dev # 新建本地分支 dev,并建立与远程origin/dev分支间的关联
git branch --track origin/dev # 将当前分支与远程的origin/dev关联
git stash
git stash 用于暂存一些文件,但不进行提交。参见例 4。
Copy git stash # 暂存
git stash list # 查看已暂存的东西
git stash pop stash@{ 0 } # 将暂存的东西取出, 并且不保留该份存储
git stash drop stash@{ 0 } # 丢弃某份存储
git stash apply stash@{ 0 } # 将暂存的东西取出, 并且保留该份存储
git stash clear # 清除所有的暂存
git checkout/switch/restore
git checkout 命令用于切换分支以及文件的版本切换作用
git checkout 用于切换分支
本质上是修改 HEAD 指向的 commit_id
Copy git checkout < branch_name/commit_i d >
git checkout --detach < branch_name/commit_i d >
注意两条命令只有一些微妙的区别:第一条命令如果切换后 HEAD 与现有的某个分支名的指向一致,则 HEAD 的状态为非 detached 状态,否则为 detached 状态;第二条命令切换后必然处于 detached 状态。所谓 detached 状态指的是切换后相当于处于匿名分支上,如果在匿名分支上发生了提交,之后又切换到别的分支,如果还想切换回匿名分支,那么只能用 commit_id 来切换回去。
当工作目录发生了修改,但此时希望切换到另一个分支,并且要切换到的分支与当前工作目录的版本存在冲突时,使用 git checkout
命令将会失败,此时如果使用
Copy git checkout --force brach_name
那么等效于先丢弃工作区的全部修改,再进行分支切换
git checkout 用于文件版本切换
Copy git checkout -- readme.txt # 将工作区回退到暂存区或版本库其中之一, 哪个最新就回退到哪个
较新版本的 Git 引入了两个命令将 checkout 的两大功能进行了分离。其中 git switch 用于分支切换,git restore 用于文件的版本切换。
git switch
Copy git switch < branch_nam e > # 注意:此处不能用commit_id进行切换,因此切换后必不为 detached 状态
git switch --detach < branch_name/commit_i d > # 切换后为 detached 状态
git restore
Copy git restore [--worktree]/[-W] README.md # 工作区文件内容发生变动, 撤销相对于暂存区的修改
git restore --staged/-S README.md # 工作区内的文件内容不变, 撤销暂存区相对最近一次提交的修改,等价于 git reset README.md
git restore -s HEAD~1 README.md # 将工作区的文件内容恢复到最近提交的上一个提交版本
git restore -s dbv231 README.md # 将工作区恢文件内容恢复到特定提交版本
git merge
分支合并的一般流程为:
Copy # step 1:
git merge < branch_nam e > # 将 branch_name 分支合并至当前分支
# step2: 手动解决冲突(即修改好发生冲突的文件)
# step3: 将解决好了的冲突文件进行添加
git add .
# step 4: 继续合并
git merge --continue # 填写好提交信息后就完成了合并
# 或者直接使用 git commit 也是ok的
git rebase(待研究)
原理
从合并的文件上看与 merge 效果一样,但提交历史有了改变。
假定分支情况为:
Copy c1 <- c2 <- c3 <- c4 <- c5 # master分支
<- c6 <- c7 # dev分支
使用 git rebase
的流程为:
Copy git checkout dev
git rebase master
# 手动解决冲突
git add xxx
git rebase --continue
# git checkout master
# git merge dev # fast-forward
效果是 dev 分支的提交历史变为
Copy c1 <- c2 <- c3 <- c4 <- c5 <- c6’ <- c8
所谓 rebase 的直观含义是将 dev 的“基” 从 c2 修改为了 master分支的 c5。使用变基得到的另一个好处是切换回 master 分支后将 dev 分支合进来就不用解决冲突(不产生任何 git object,仅仅是修改了 git ref)
rebase 与 merge 的区别
上述过程如果只用 merge,流程为
Copy git checkout dev
git merge master
# 手动解决冲突
git add xxx
git merge --continue
# git checkout master
# git merge dev # fast-forward
效果是
Copy c1 <- c2 <- c3 <- c4 <- c5 <- c8(dev/master)
<- c6 <- c7
实际上,merge c5 和 c7 的过程为,对 c5 和 c7 对应的 tree 进行合并,解决冲突后使用 git add
命令时,会得到一些新的 blob,使用 git merge --continue
时,会用 .git/INDEX
(暂存区)里对应的 tree 写入 .git/objects
目录,并得到一个新的 commit 对象 c8,也写入 .git/objects
目录。注意:c8 这个 commit 对象的 parent 有两个,即 c5 和 c7。
而 rebase 的过程为:对 c5 和 c7 进行合并后得到的 commit 对象 c8 的 parent 仅有 c5 一个。另外,还会产生一个新的 commit 对象 c6’,其提交信息与 c6 一致,但其 parent 与 c6 不同,并且所对应的 tree 对象也有所不同。
git cherry-pick
Copy # git checkout dev
git cherry-pick <commit-id>
上述命令的作用是,将 <commit-id>
相对于它前一次提交的修改,作用到当前分支 dev
上,并形成一次新的提交。注意:假设 <commit-id>
对应于另一个分支例如 master
,新的提交依旧可能与 master
分支有合并冲突。
Copy # 使用-n或--no-commit阻止自动提交
git cherry-pick --no-commit < commit-id 1> .. < commit-id 2>
将commit-id2相对于commit-id1的连续多个修改作用于当前分支
git clone
Copy git clone git@github.com:username/repository_name.git
git clone git@github.com:username/repository_name.git -b dev
上述两条命令内部的详细过程为:两者都会将 .git
中的所有内容(远程仓库的所有分支)下载到本地。下载后,第二条命令本地的默认分支 dev 由远程分支 origin/dev 产生。
git remote
git submodule
参考:pro-git 7.11
参考:git-tower (一个不错的教程,感觉比 pro-git 还要清晰)
有时候,项目开发时需要引入另一个项目,并希望同时保留两个项目各自的提交历史,此时需要使用 git submodule 命令
例如:在项目 a
中需要引入 https://github.com/example/b.git
作为子模块,可以使用:
Copy # 当前目录 a/
git submodule add https://github.com/example/b.git
此时,a/
目录下会多出一个 .gitmodules
文件,并且多出一个 a/b
文件夹,文件目录类似如下:
Copy a/
- .git/
- .gitmodules
- ...
- b/
- .git/
- ...
此时 a/.git
目录不记录 b
目录的具体修改(只关心在对 a
提交时 a/b
的 commit id 是否发生变化)。git
命令在 a/
目录与 a/b
目录下分别只对 a/.git
与 a/b/.git
进行修改,并且只关心各自部分的文件修改历史。
如果需要克隆一个带有 submodule 的仓库,可以使用如下几种方式进行:
Copy # 一步步操作
git clone https://github.com/example/a
cd a
git submodule init
git submodule update
# 后两步也可以合为一步
# git submodule update --init
# git submodule update --init --recursive # submodule中也包含submodule
# 一步到位
git clone --recurse-submodules https://github.com/example/a
git fetch/pull/push
Copy git pull < 远程主机 名 > < 远程分支 名 > : < 本地分支 名 >
# 例子:git pull origin dev:release #表示将
git push < 远程主机 名 > < 本地分支 名 > : < 远程分支 名 >
# 例子:git push origin release:dev
git push -u < 远程主机 名 > < 本地分支 名 > : < 远程分支 名 >
# 之后可以直接使用 git push,不加其余参数
git pull 的具体行为是:首先 git fetch 指定远程分支的更新,之后在指定的本地分支上进行 git merge 的操作,如果当前的 HEAD 刚好位于指定的本地分支上,则移动 HEAD 的指向到 merge 后的位置。
git fetch 命令只会将远程分支的修改更新到 .git
目录内部,但不会新建分支,也不会对本地的工作目录进行修改。
参考 stackoverflow 问答,可以按如下方式拉取所有远程分支的更新:
Copy git branch -r | grep -v '\->' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done # 建立与远程分支同名的本地分支,并一一关联
git fetch --all # 等价于 git remote update, 作用是拉取全部分支的远程更新
git pull --all # 更新全部的本地分支
git bundle
bundle 文件用于离线传输 git objects. 使用上可以参考 文档 中的例子, 但由于实际使用时与例子中的需求不完全匹配, 所以这里复述一下文档中的例子(部分), 并加以补充. bundle 文件可以理解为一个离线的 repo, 可以将其视为受限的“远程仓库”, 进行 git remote add
, git pull
, git fetch
, 但与真正的远程仓库的重要区别是: 不能对 bundle 文件进行 git push
.
假定场景是这样: 在网络环境 1 的机器 A 上有一个仓库 R1, 在网络环境 2 上有一个机器 B, 在时刻 T1, 将 R1 通过某种方式复制去了机器 B 的 R2, 随后 R1 继续做了修改/提交, R2 可能也继续做了修改/提交. (可以理解 R1 是某个 GitHub 上的开源项目, R2 为公司在确认开源协议后, 决定基于此项目做二次开发). 现在在 T2 时刻, 希望将 R1 在 T1 时刻之后的所有修改 (git objects) 保存在一个文件 F 中, 然后将 F 拷贝至机器 B, 然后 R2 仓库按需从 F 中 cherry-pick/merge/fetch 等.
文档 中的解决方案
T1 时刻同步
此步骤是 T1 时刻将 R1 “复制” 到机器 B, 首先在机器 A 上操作
Copy machineA$ cd R1
machineA$ git bundle create file.bundle master
machineA$ git tag -f lastR2bundle # 打 tag 不是必要的, 只是方便 T2 时刻做同步
将 file.bundle
通过某种方式复制进
Copy machineB$ git clone -b master /home/me/tmp/file.bundle R2
Copy [remote "origin"]
url = /home/me/tmp/file.bundle
fetch = refs/heads/*:refs/remotes/origin/*
T2 时刻同步
上述描述的 T2 时刻同步的做法是与 T1 时刻同步方式匹配的, 实际情况中 T1 时刻的同步还存在这几种情况:
将整个 R1 仓库打包为一个压缩包, 传入内网后解压以获取 R2 仓库
R1 仓库本身也有远程库例如 GitHub, 而 R1 仓库在 T1 至 T2 期间, 使用 git fetch
或 git pull
更新 git objects 时, 有许多远程分支 remotes/origin/xx
没有相对应的本地分支 xx
这种情况下, T2 时刻的同步也需要做些适当的调整, 这里有一个示例, 但需要确保自己知道每一步在做啥:
这个例子需要理解这个 stackoverflow问答 以及这个 简书博客
T1 时刻同步
将整个 R1 仓库打包为一个压缩包, 传入内网后解压以获取 R2 仓库
T2 时刻同步
Copy machineA$ git bundle create xx.bundle --all
# 传输 xx.bundle 至 machineA
machineB$ git add remote bundle /path/to/xx.bundle
# 手动修改.git/config的内容
[remote "bundle"]
url = /path/to/xx.bundle
fetch = refs/heads/*:refs/remotes/origin/*
# 改为:加号表示强制,冒号左侧表示远程,右侧表示本地,例如原本的写法里,远程(xx.bundle)中的dev分支在执行fetch时会在本地是refs/remotes/origin/dev。基于此,下面改动的含义自明,需确认是我们想要的再该
[remote "bundle"]
url = /path/to/xx.bundle
fetch = +refs/*:refs/*
# 执行(请参考上面的简书中文博客确认git fetch几种用法的准确含义)
machineB$ git fetch bundle --all
备注: 参考这个 blog , git lfs 文件似乎没法使用 git bundle 做迁移
* git cat-file
查看 .git/objects
目录下的文件内容
Copy git cat-file -p 24c5735c3e8ce8fd18d312e9e58149a62236c01a # 查看 objects 目录下的文件内容
* git ls-files
Copy git ls-files -s # 查看当前缓冲区内容, 即 .git/index 文件中的内容
* git ls-remote
Copy # 查看远程库所有的tag, branch 等:
git ls-remote origin
输出
Copy 919ecbc81882fb3d8139d340e8ed32ed305c23ce HEAD
dc41dd5ac4890b3155e1a6cb7c1f986049f63c2e refs/heads/dev
919ecbc81882fb3d8139d340e8ed32ed305c23ce refs/heads/main
f179eef20d0db126f8aa3d3758f3f88a904f1163 refs/pr/1
6844761b3145f904df40e56acf2e0d5df2dfb88d refs/pr/2
847491ff49d8ae93a9708c68b4de9095eec0c6ff refs/pr/3
919ecbc81882fb3d8139d340e8ed32ed305c23ce refs/pr/6
git lfs
使用如下方式对大文件进行特殊管理(由 git lfs 管理)
Copy git lfs track "*.psd"
git add .gitattributes
git add file.psd
git commit -m "Add design file"
git push origin main
git clone 时可以选择是否下载大文件
Copy # 不下载大文件的方式进行下载
GIT_LFS_SKIP_SMUDGE=1 git clone xxx.git
git clean
慎用
Copy # TODO: 各个参数是什么意思
git clean -d -f -x -n # 先看下会删除什么
git clean -d -f -x
常用命令备忘录
不切换分支拉取远程代码与本地分支做fast-forward合并
Copy git fetch <remote> <sourceBranch>:<destinationBranch>
http协议记住密码
Copy git config --global credential.helper store
删除分支
Copy git branch -d <branch-name> # 删除本地分支
git branch -rd <remote-name>/<branch-name> # 删除本地保存的远程分支(不影响远程代码库)
git push -d <remote-name> <branch-name> # 删除远程代码库的分支
疑难杂症
一般使用
Copy # 忽略权限修改
git config core.filemode false
# 查看git配置
cat .git/
# 忽略某些文件的修改, gitignore只能忽略untracked的文件
git update-index --assume-unchanged [<file> ...]
# 取消忽略
git update-index --no-assume-unchanged [<file> ...]
代理问题
关于代理引发的 git clone 失败问题,参考链接
重置代理
Copy git config --global --unset https.https://github.com.proxy
git config --global --unset http.https://github.com.proxy
根据实际端口情况修改
Copy git config --global http.https://github.com.proxy http://127.0.0.1:7890
git config --global https.https://github.com.proxy http://127.0.0.1:7890
一次性使用
Copy git clone -c http.proxy="http://127.0.0.1:60264" https://github.com/huggingface/huggingface_hub.git
网络问题
Copy error: RPC failed; curl 56 GnuTLS recv error (-9): Error decoding the received TLS packet.
暂未找到解决方法
终端显示分支
在 terminal 中提示当前所在分支,将如下代码段加入到 ~/.bashrc
中即可
Copy function git_branch {
branch="`git branch 2>/dev/null | grep "^\*" | sed -e "s/^\*\ //"`"
if [ "${branch}" != "" ]; then
if [ "${branch}" = "(no branch)" ]; then
branch="(`git rev-parse --short HRAD`...)"
fi
echo " ($branch)"
fi
}
export PS1='\[\033[01;36m\][\u@\w]\[\033[01;32m\]$(git_branch)\[\033[00m\] \$ '
http 免密
Copy git remote add origin https://{username}:{password}@123.234.3.24
详例
注意, 测试2.1与2.2表示的是在测试1的基础上尝试两种做法的结果
例 1(待清晰化)
本例着重解释了git status/diff/restore
三个命令
Copy 测试流程,一共有4条路径
1 -> 2.1
1 -> 2.2
1 -> 2.3 -> 2.3.1
1 -> 2.3 -> 2.3.2
Copy git init
# 新建一个README.txt, 添加内容line one
git add README.txt
git commit -m "first commit"
# 在README.txt中添加内容line two
git add README.txt
# 在README中添加内容line three
测试 1
Copy $ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.txt
$ git diff
diff --git a/README.txt b/README.txt
index 4c00b39..fa58e34 100644
--- a/README.txt
+++ b/README.txt
@@ -1,2 +1,3 @@
line one
-line two
\ No newline at end of file
+line two
+line three
\ No newline at end of file
$ git diff --staged
diff --git a/README.txt b/README.txt
index 017bf5c..4c00b39 100644
--- a/README.txt
+++ b/README.txt
@@ -1 +1,2 @@
-line one
\ No newline at end of file
+line one
+line two
\ No newline at end of file
总结:
git status
命令的输出分为两部分:
Changes not staged for commit
: 显示工作区相对暂存区的变动记录
Changes to be committed
: 显示暂存区相对最近一次提交的变动记录
相应地, git diff
命令地解释如下:
Copy git diff # 显示工作区相对于暂存区的修改
git diff --staged # 显示暂存区相对最近一次提交的修改
测试 2
总结: 接下来, 在操作之前先解释git restore
的两种用法
Copy git restore --staged <file> # 将<file>的暂存区记录删除, 即暂存区恢复至最近提交状态
git restore <file> # 将工作区<file>恢复至暂存区的状态
测试 2.1
Copy $ git restore README.txt # 此时工作区的README回到只有两行文字的状态
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.txt
测试 2.2
Copy # 注意工作区文件不变, 暂存区文件回到最近提交的状态
$ git restore --staged README.txt
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.txt
no changes added to commit (use "git add" and/or "git commit -a")
# 注意到显示的修改是文件增加了两行内容
$ git diff
diff --git a/README.txt b/README.txt
index 017bf5c..fa58e34 100644
--- a/README.txt
+++ b/README.txt
@@ -1 +1,3 @@
-line one
\ No newline at end of file
+line one
+line two
+line three
\ No newline at end of file
# 执行完毕后, README.txt恢复至一行内容的状态
$ git restore README.txt
测试 2.3
总结: git restore
还有第三种用法如下
Copy # 将<file>恢复至版本库中的某个版本
git restore --source/-s 7173808e <file>
git restore --source/-s HEAD <file>
测试如下:
Copy # 执行完后工作区的README.md只有一行内容
$ git restore --source HEAD README.txt
54120@DESKTOP-7LQFJM3 MINGW64 ~/Desktop/git_test (master)
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.txt
# 注意git diff显示的是工作区相对暂存区的差异, 这里表示工作区比暂存区少了一行
$ git diff
diff --git a/README.txt b/README.txt
index 4c00b39..017bf5c 100644
--- a/README.txt
+++ b/README.txt
@@ -1,2 +1 @@
-line one
-line two
\ No newline at end of file
+line one
\ No newline at end of file
# 注意git diff --staged显示的是工作区相对最近提交的差异, 这里表示工作区比最近提交多了一行
$ git diff --staged
diff --git a/README.txt b/README.txt
index 017bf5c..4c00b39 100644
--- a/README.txt
+++ b/README.txt
@@ -1 +1,2 @@
-line one
\ No newline at end of file
+line one
+line two
\ No newline at end of file
测试 2.3.1
Copy $ git restore --staged README.txt
$ git status
On branch master
nothing to commit, working tree clean
测试2.3.2
根据上面的总结, 解释如下过程:
Copy $ git restore README.txt
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.txt
# 没有输出
$ git diff
$ git diff --staged
diff --git a/README.txt b/README.txt
index 017bf5c..4c00b39 100644
--- a/README.txt
+++ b/README.txt
@@ -1 +1,2 @@
-line one
\ No newline at end of file
+line one
+line two
\ No newline at end of file
$ git restore --staged README.txt
$ git restore README.txt
# 恢复至工作区, 暂存区均只有一行内容的状态
$ git status
On branch master
nothing to commit, working tree clean
例 2(待补充)
本例着重解释 git rm
命令
备注: 本机已经准备好了如下环境
Copy git init
# 新建一个README.txt, 添加内容line one
git add README.txt
git commit -m "first commit"
例 3(待删减)
由于被各种命令搞晕, 于是决定干脆打开.git
目录一探究竟, 难免会有许多错误, 待日后修改
当所有操作仅限于本地时, .git
目录大概长这样:
Copy .git/
|-- hooks/ # 暂且不需要管, 用处大致是在执行git命令时可以自动执行一些附加操作
|-- applypatch-msg.sample # (文本文件)一些shell脚本代码
|-- commit-msg.sample # (文本文件)一些shell脚本代码
|-- ... # (文本文件)一些shell脚本代码
|-- info/
|--exclude # (文本文件)感觉与.gitignore有关
|-- logs/
|-- refs/
|-- heads/
|-- dev # (文本文件)记录了dev分支的历史提交信息, 格式见下面的说明
|-- master # (文本文件)记录了master分支的历史提交信息, 格式见下面的说明
|-- HEAD # (文本文件)记录了HEAD指针的历史提交信息, 格式见下面的说明
|-- objects/
|-- e6/ # 大概是散列技术的索引
|-- 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 # (字节码文件) 对应于一个文件的提交状态
|-- c15228661d6bfce44a215fb3fbaaea1397059c # (字节码文件) 对应于一个文件的提交状态
|-- ee/
|-- 2c1ea0cbf0f242452802fbf32b1ae0abe92467 # (字节码文件) 对应于一个文件的提交状态
|-- ...
|-- info/ # 不清楚
|-- pack/ # 不清楚
|-- refs/
|-- heads/
|-- dev # (文本文件)记录了dev分支的当前版本号?
|-- master # (文本文件)记录了master分支的当前版本号?
|-- tags/ # 未知
|-- COMMIT_EDITMSG # (文本文件)似乎是"最近"一个提交的提交信息
|-- config # (文本文件)配置文件, 格式是标准配置文件格式
|-- description # (文本文件)大概是对整个项目的描述信息
|-- HEAD # (文本文件)文件内容示例: ref: refs/heads/dev
|-- index # (字节码文件)暂存区信息, 大概是当前暂存区的快照
|-- ORIG_HEAD # (文本文件)文件内容为某个版本号
以下为一个完整的测试(我们主要关注logs
, objects
, refs
文件夹以及index
, HEAD
, ORIG_HEAD
文件的变化)
step 1
注意: 此时logs
文件夹与ORIG_HEAD
未被创建, objects
目录下还没有字节码文件
HEAD
文件的内容为
Copy ref: refs/heads/master
refs/heads/master
文件还未被创建
step 2
在工作区增加一个文件show_git.py
此时, objects
目录下新增了一个./74/ba2162d599d4e44dcf3d7811cbbd84d43e911d
字节码文件(对应于新增的show_git.py
文件), index
文件夹也做了更新.
refs/heads/master
文件还未被创建
备注: objects
目录下计算出的16进制值只与文件内容与文件名有关
step 3
Copy git commit -m "add show_git.py 0.1.0 version"
此时.git
目录的变化为:
log
目录被创建, 目录结构如下
Copy |-- logs/
|-- refs/
|-- heads/
|-- master # (文本文件)记录了master分支的历史提交信息
|-- HEAD # (文本文件)记录了HEAD指针的历史提交信息
master与HEAD的文件内容如下
Copy # master
0000000000000000000000000000000000000000 92628406280a94f4efbdcbf59dcb60a5b44ab124 BuxianChen <541205605@qq.com> 1599265251 +0800 commit (initial): add show_git 0.1.0 version
# HEAD
0000000000000000000000000000000000000000 92628406280a94f4efbdcbf59dcb60a5b44ab124 BuxianChen <541205605@qq.com> 1599265251 +0800 commit (initial): add show_git 0.1.0 version
若此时使用git flag pretty=oneline
命令, 输出结果为:
Copy 92628406280a94f4efbdcbf59dcb60a5b44ab124 (HEAD -> master) add show_git 0.1.0 ver
sion
refs
目录下的heads/master
文件被创建, 内容如下
Copy 92628406280a94f4efbdcbf59dcb60a5b44ab124
注意: 这个值大概与时间有关(怀疑是uuid)
objects
目录新增了两个文件7b/9a4ced4ae9fc215a7cbb2e1c52b6fce706e051
, 92/628406280a94f4efbdcbf59dcb60a5b44ab124
不知道为什么要新增两个文件, 有一个疑惑是git reflog
命令需要的输出保存在哪, 是否与这个有关?
HEAD
文件内容保持不变
Copy ref: refs/heads/master
例 4(git stash 与 git merge 综合实例)
适用场景如下,例如:master 分支为线上分支,现在需要开发一个新功能,则基于该分支创建一个 dev 分支,但还没修改完毕并且不想提交时,发现 master 分支上出现了 bug,需紧急修复。此时直接使用 git branch master
会报错,此时可以使用 git stash 命令将改动的文件暂存,这样便可以正常切换分支。完整过程如下
Copy # 在dev分支上做了一些还不想提交的修改
git stash # 暂存所有修改过的文件,但不产生提交
git checkout master
git checkout -b bug
# 修复bug后, 将bug与master合并
git merge master
git checkout master
git merge bug # Fast-forward, 不会有冲突
git branch -d bug # 删除bug分支
git checkout dev
git stash list # 展示暂存的东西
git stash pop stash@{0} # 将暂存的东西取出, 并且不保留该份存储
# 对dev分支修改完毕后
git commit
git merge master
# 解决冲突
git add .
git merge --continue
git checkout master
git merge dev # Fast-forward, 不会有冲突
git branch -d dev
例 5 (代码库同步)
场景设定: 公司内网与 GitHub 网络不通, 公司内网存在类似于 Gitlab 的代码管理服务器, 数据文件可从外网上传至内网服务器. 现在希望对 GitHub 项目手动与内网项目同步.
方案1 (不合适, 也没有真正实践过)【待确认!!!】
参考:
https://github.com/git-lfs/git-lfs/issues/2342
步骤如下:
第一次同步
Copy git clone --bare <SOURCE-URL> # github url
cd xx.git
git lfs fetch origin --all
cd ..
tar -czvf xx.git.tar.gz xx.git/
# 复制进内网
tar -xzvf xx.git.tar.gz
# 可选(推送至内网代码托管平台)
cd xx.git
git push --mirror <TARGET-URL> # 内网 git 托管 url
git lfs push --all <TARGET-URL>
# 重新clone一个有workspace的本地库
git clone xx.git xx
内网需保留 xx.git
目录(以增量更新), xx
目录如果不需要可以删除
内网需保留 <TARGET-URL>
(以增量更新)
第二次同步(更新)
Copy # 外网
cd xx.git
git fetch origin --all
git lfs fetch origin --all
cd ..
tar -czvf xx.git.tar.gz xx.git/
# 复制进内网
tar -xzvf xx.git.tar.gz
mv xx.git xx.git.tmp # 这个临时保存一下
cd xx.git
git add remote tmp xx.git.tmp
git fetch tmp --all
git lfs fetch origin --all
rm -rf xx.git.tmp
# 可选(推送至内网代码托管平台)
git push --mirror <TARGET-URL> # 内网 git 托管 url
git lfs push --all <TARGET-URL>
# 有workspace的本地库使用 pull/fetch 更新
例 6: bare repo 与正常 repo 转换
参考: stackoverflow
正常 repo -> bare repo
Copy cd repo
mv .git ../repo.git # renaming just for clarity
cd ..
rm -rf repo
cd repo.git
git config --bool core.bare true
Copy git bundle create xx.bundle --all
git bundle verify # 几个HEAD的指向似乎有问题
bare repo -> 正常 repo
Git 合作模式
模式一:
master 分支只用作合并,且合并过程自动完成,无需解决冲突。dev 分支用做开发人员的公共基库,各开发人员(例如:f1,f2 分支)完成相应的开发后,在 dev 分支上完成手动解决冲突后的合并。最后将 dev 分支合并至 master 分支。
Copy git branch dev
git checkout dev
git branch f1 # A: feature 1
git branch f2 # B: feature 2
# do some commit in f1, f2...
git checkout dev
git merge f1 f2
# 手动解决冲突...
git add .
git merge --continue
git checkout master
git merge dev
GitHub Flow
在 Github 上为他人项目贡献代码的工作流如下:
在 GitHub 网页的功能在官方原始仓库 新建 issue
git clone 个人远程仓库 至本地仓库 , 务必从希望最终被合入的官方分支新建分支进行开发
如果官方原始仓库 发生修改, 则使用 GitHub 网页的功能将个人远程仓库 与官方原始仓库 同步
git pull/fetch 将个人远程仓库 的更新内容拉至本地仓库
使用 GitHub 网页的功能提 PR 请求, PR 请求关联 issue
Git hooks
hooks 通常译为“钩子”,Git hooks 本质上是位于 .git/hooks
下的一些脚本,它们会在特定的事件触发时(也就是某些特定的命令被执行时)被自动运行,例如:执行 git commit
命令时。其文件名是固定的(对应着相应的事件),git 默认为每个仓库都提供了默认的 hooks,它们的扩展名均为 .sample
,如果需要启用 hooks,只需要将相应脚本的扩展名删除即可。hooks 的特点是在 git clone
时,这些脚本不会被克隆下来,另外默认 hooks 的语言为 shell 脚本,但也可以使用其他脚本语言例如 Python,只需要修改文件的 shebang 行即可。
一个看起来还不错的教程 。
注意:不要为了加 hooks 而加 hooks,它只是一个工具。
github、gitlab
Github 对比同一仓库的两个分支: https://github.com/username/Repo_A/compare/29ea234..84ae245
Git Internals
git reset 与 git checkout
Copy # 显示某个commit的文件结构
$ git ls-tree -r <commit-id>
100644 blob 652ac0a2b320a855a5bdc6c09a5cdbcc822340f8 a.txt
100644 blob 61780798228d17af2d34fce4cfbdf35556832472 b/b.txt
100644 blob f2ad6c76f0115a6ba5b00456a849810e7ec0af20 b/c.tct
100644 blob f2ad6c76f0115a6ba5b00456a849810e7ec0af20 c/c.txt
$ git ls-tree <commit-id>
100644 blob 652ac0a2b320a855a5bdc6c09a5cdbcc822340f8 a.txt
040000 tree a04216f8c13a0c97348ec26ccbe5738224f1951e b
040000 tree cf67e9ef3a0fc6d858423fc177f2fbbe985a6f17 c
Copy # 显示暂存区的文件
$ git ls-files -s
100644 652ac0a2b320a855a5bdc6c09a5cdbcc822340f8 0 a.txt
100644 61780798228d17af2d34fce4cfbdf35556832472 0 b/b.txt
100644 f2ad6c76f0115a6ba5b00456a849810e7ec0af20 0 b/c.tct
100644 f2ad6c76f0115a6ba5b00456a849810e7ec0af20 0 c/c.txt
git add
的时候,将新增加的文件内容加入至 .git/objects
目录,而不增加 tree
类型的 object。在 git commit
的时候才创建 tree
类型的 object,并将其添加至 .git/objects
目录。
git reset
与 git checkout
的区别:
git reset
的调用方式有如下几种
Copy git reset --soft <commit-id>
git reset --mixed <commit-id>
git reset --hard <commit-id>
git reset --mixed <commit-id> -- <file>
git reset --hard <commit-id> -- <file>
其中前三条命令的执行逻辑是依次进行如下三步:
将当前的 branch 指向 , HEAD 依旧指向 branch, 因此最终也指向
备注: 只有第三种被认为是危险的
而后两条命令的执行逻辑是:
git checkout
的调用方式有如下几种
Copy git checkout <commit-id>
git checkout <commit-id> -- <file>
git checkout -b <branch_name> <commit-id>
第一条命令的执行逻辑是:
将HEAD指针本身指向, 将暂存区的内容改为中的内容, 将中的内容与工作区的内容合并(如果有冲突, 命令本身会报错)
特殊目录与文件
.github
: github 网页端的 issue, pull requests 等模板
.gitattribute
: Git-LFS 管理的文件信息