序:读前须知
- 本篇文章为观看nina@nnja.io的课程的笔记,并不适合阅读,如果感兴趣可以找到视频进行学习,nina也将资料整理在了github上了
- 文章中可能存在翻译不准理解错误等问题
- 课程内容主要是对git相对深入的一个了解,告诉你每条命令背后在干什么,穿插了大量其他内容,找命令工具书的同学可以出门左拐了
一、Git数据结构
1.1 Git
Git 是分布式版本控制系统,采用key-value的方式存储
1.1.1 the key - SHA1
- 一个密码散列函数
- 给定一段数据,产生一个40位的十六进制数
- 相同的输出,相同的输出
- 内容可寻址存储系统,可以用内容生成key
1.1.2 the value -BLOB
git将事物存储为git对象的方式,产生的基本单元叫做blob
- git 将数据压缩到一个blob单元当中,带有一个元数据的头部
- 整个blob包括
- blob标识符
- 长度
- \0分隔符
- 内容本身
1.1.3 GIT HASH-OBJECT
- 通过git得到内容的SHA1,bash输入如下指令
echo 'hello world' | git hash-object --stdin
//输出如下
3b18e512dba79e4c8300dd08aeb37f8e728b8dad
- 通过其他工具也能得到对应内容的SHA1,注意输入内容要为blob的格式,长度为\0 + 内容长度
echo 'blob 12\0hello world' | openssl sha1
//输出如下
3b18e512dba79e4c8300dd08aeb37f8e728b8dad
- 同一内容得到同一输出,两条内容生成同一输出的碰撞可能性无限小
1.2 Git存储
1.2.1 Git存储位置
git将数据存储在Git目录中
- 初始化一个空的Git存储库,生成.git目录,.git目录下包含了关于数据库存储的所有数据
git init
Initialized empty Git repository in /Users/yihangyang/Desktop/workspace/.git/
- 删除.git目录相当于删除了存储库,文件并不会被删除
1.2.2 blob存储位置
在git init后的目录下,键入如下指令,发现objects下面有一个3b开头的子目录,子目录下是一个18e512开头的子目录,对应我们之前SHA1得到的输出,blob就存储在该目录下
echo 'hello world' | git hash-object -w --stdin
tree .git
1.2.3 Tree
如果我们的输入不是一段文字而是一个文件,那么blob缺失了一些信息
- 文件名
- 目录结构 git将这些信息存储在了tree上
tree(使用SHA1)包含:
- 指针
- 使用SHA1指向blob的指针
- 指向其他tree的指针
- 元数据
- 指针类型(blob或tree)
- 文件名或者目录名
- mode (可执行文件/符号链接) 后缀?
受到staging area限制,不存储空目录,相同的内容只会被存储一次
1.2.4 其他优化 packfiles,deltas
- git objects都是压缩过的
- 当文件改变的时候,内容大部分保持一致
- git利用以上将这些文件一起压缩成一个packfile
- packfile存储git object,以及deltas(增量)| 一个文件的当前版本和下一个版本的不同之处
- packfile的生成时间:
- gc(垃圾回收)期间存在太多objects
- push到remote
二、commit和references
2.1 Git Commits
2.1.1 Commit Object
一个commit包含:
- 指向tree的commit指针
- 元数据
- 作者和提交人
- 日期
- 捎带信息
- parent commit(一个或者多个)
- commit的SHA1是所有这些信息的hash值
- commit内的指针指向parent commits 和 trees,下图中fae12为较早的commit,ab29d为之后的commit
- 一个commit是项目在那个时间点的snapshot,是上一次提交的暂存区更改的组合
2.1.2 做一次commit
在git init的目录下,键入如下命令
echo 'Hello World!' >hello.txt
git add hello.txt
git commit -m "Initial commit"
2.1.3 查看git object
- 无法使用cat直接查看git object,它们已经被压缩过了
cat .git/objects/98/0a0d5f19a64b4b30a87d4206aade58726b60e3
xK??OR04f?H????/?IQ?Hak **%**
- 使用git cat-file -t|-p (sha1) 查看对应的git object 其中的sha1不必全部打出,足够区分即可
git cat-file -t (sha1)
输出类型 commit | blob |tree -t代表 print the type
git cat-file -p (sha1)
输出内容 如果是blob会直接反hash输出原内容,
如果是tree则显示tree相关信息,
mode 可执行文件或其他
blob 指向的是tree还是blob
sha1 指针指向的对象的sha1
文件名
如果是commit则显示commit相关信息
tree tree对应sha1
author 时间戳
committer 时间戳
parent commit sha1 没有则不显示
message
- commit无法被更改,commit的data变化时,commit会生成新的SHA1 hash,即便是file没有改动,commit的日期也会变化。(现在好像不允许没有data变化的commit,可能课程制作的时候git有所不同)
- git的commit相关数据无法被修改,确保了其安全性
2.2 Git中的References
2.2.1 Reference
指向commits的指针,包含:
- Tags
- Branches
- HEAD - a pointer to current branch,通常是当前所在分支,也有可能指向当前的commit,如果你在一个分支上,它指向最后一次提交的那个分支
签出一个分支的时候,HEAD也会指向当前分支
2.2.2 查看references
- .git/refs/heads 是所有分支所在的目录
查看.git目录结构
tree .git
输出的目录如下,存在HEAD和refs两个相关目录
.git
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks
├── index
├── info
├── logs
├── objects
└── refs
├── heads
│ └── master
└── tags
查看master分支的sha1
cat .git/refs/heads/master
输出对应sha1
查看HEAD
cat .git/HEAD
输出 ref: refs/heads/master
2.3 工作区,暂存区,仓库
2.3.1 工作区 Working Area
- 在工作区的文件不会在暂存区,不被git处理,只是在本地的目录当中
- 也叫做untracked files
2.3.2 暂存区 Staging Area, AKA Index,Cache
- 暂存区的文件将成为下一次提交的一部分
- 是git了解当前commit和下次commit的变化的方式
- 干净的暂存区并不是空的
- baseline的暂存区是最近一次commit的copy,包含上一次提交的文件列表,以及这些文件在上次提交的SHA1哈希值
- 索引是.git目录下的二进制文件
- 当添加、删除、重命名文件到暂存区时,Git知道是因为被改变的文件的SHA1不同了
可以通过以下命令查看暂存区索引
git ls-files -s
输出 100644 980a0d5f19a64b4b30a87d4206aade58726b60e3 0 hello.txt
代表 mode 文件的SHA1 文件名
将文件添加或者移出暂存区
添加
git add (file)
分块添加 逐个决定是否添加到暂存区
git add -p
移除
git rm (file)
重命名或移动
git mv (source) (destination)
从暂存区unstage file
- 没有移除文件本身
- 只是用在repository的副本替换了
2.3.3 仓库 Repository
- git实际知道的文件
- 包含所有commits
2.3.4 git stash
- 保存un-committed 的内容
- 在执行破坏性操作的时候,先进行stage是安全的
- 当你的操作可能删除或者覆盖你的修改的时候,比如git reset或者git checkout,可以先stage
git stash的基本使用
保存修改内容
git stash
展示stash列表
git stash list
展示stash内容 0代表stash的次序
git stash show stash@{0}
apply the last stash
git stash apply
apply a specific stash 0代表stash的次序
git stash apply stash@{0}
keep untracked files 再次apply的时候依然文件是untracked
git stash --include-untracked
keep all files(even ignored ones!)
git stash --all
git stash的高级操作
为stash起名字作为参考
git stash save "information"
从一个stash开始一个新的分支
git stash branch <可选的branch name>
从stash 当中获取单个文件 如果工作区、暂存区的文件已经修改过了,这个命令会覆盖掉该文件
git checkout <stash name> -- <filename >
删除上一次的stash并且进行apply 如果存在merge conflict则不会删除stash
git stash pop
删除上一次的stash
git stash drop
删除某次的stash
git stash drop stash@{n}
清空stash
git stash clear
部分内容 stash 类似git add -p
git stash -p
2.4 reference
三种类型的reference
- Tags & Annotated Tags
- Branches
- HEAD
2.4.1 Branch
- barnch是指向一个特定的commit的指针
- 当commit发生后,当前的branch对应的commit跟着变化
2.4.2 HEAD
- 告知git了解当前位于那个branch上,以及下个commit的parent
- 是一个指针
- 指向当前分支的名字
- 也能指向一个commit(detached HEAD)
- 当发生以下行为的时候,HEAD会移动:
- 在当前活跃的分支上make a commit
- checkout到新的分支上
查看当前分支
cat .git/HEAD
输出 ref: refs/heads/分支名
2.4.3 tag && annotated tag
- tag是指向commit的指针
- 不带参数的创建tag会指向HEAD指向的内容
- git tag -a 能存储作者、捎带信息、日期这些其他内容
- tag不会随commit发生指向的变化
创建指向HEAD内容的tag
git tag <tag-name>
创建带其他信息的tag
git tag -a <tag-name> -m "message"
list tag
git tag
查看所有tag并且带上指向的commit
git show-ref --tags
查看指向某个commit的所有tag
git tag --points-at <commit>
展示某一个tag
git show <tag-name>
2.4.4 HEAD-LESS / DETACHED HEAD的情况
- 有些时候需要checkout到一个特定的commit(or tag)而不是branch
- git会将HEAD指针指向那个commit,这个时候变成DETACHED HEAD
- 一旦你checkout到不同的branch或者commit,HEAD的值会变成对应的SHA1
- 在DETACHED HEAD的情况下,进行commit的操作,(之后checkout)git将不能通过reference指向commit
- 此时做出的commit不再进行任何操作的情况下,丢失了
- 保存DETACHED HEAD下做出的commit,需要创建DETACHED HEAD后的最后一次commit的branch
- 以最后一次commit为基础的原因是,commit本身知道其parent,所以不必保存每个parent
- 如果不建立新的branch,这些commit会成为dangling commit,这些commit不会存在reference,最后会被gc
- 在垃圾回收启动之前还是有办法找到这些commit的
以commit为基准创建branch
git branch <branch-name> <commit>
DETACHED HEAD出现的提示
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
dangling commit出现的提示
you are leaving 1 commit behind, not connected to any of your branched: <SHA1>
三
3.1 merging and fast forward
3.1.1 merging
- merge commits也是commit
- merge commits会具有多个父级的提交
- 大多数merge commit会具备两个父级
查看commit信息
git cat-file -p <sha1>
3.1.2 fast forward
- 当feature工作完成时,我们checkout 到 master上进行merge,master如果在feature分支生成之后都没有commit,则发生fast-forward, master的指针指向feature指针指向的commit
- fast forward发生时,feature作为新增的部分,直接到了master上,难以辨别出来,不利于找bug
3.1.3 git merge --no-ff(no fast forword)
- 即便master在feature分支创建后没有其他commit,我们也希望feature在merge到master上的时候保留痕迹,而不是fast forward
- 此时会强制进行一次merge commit,即便没有必要
git checkout master
git merge --no-ff <branch-name>
git --no-pager log --graph
3.1.4 merge conflicts
- 尝试去merge,但是文件已经发生更改,此更改和你的commit不兼容,发生在多个开发者开发同一内容
- git会停止操作直到conflict被解决
3.1.5 工具 GIT RERERE - reuse recorded resolution
- 开启时,保存你解决冲突的方式
- 下一个冲突采用相同的方式解决冲突
- 对于重构或者变基是有用的
- 它会自动应用记录下的冲突解决方式,但不会自动commit,需要自己确认
- 该匹配机制相对完善
开启git rerere
git config rerere.enabled true
--global,在所有projects中开启
git checkout master
git merge feature
Auto-merging ...
Recorded preimage for ... (rerere已经生成原像记录)
出现冲突,自动解决失败,手动解决
查看diff
git rerere diff
解决diff
emacsclient -t <filename>
git add file
git commit -m "resolve conflict"
Record resolution for 'feature'
下一次merge再次出现冲突
git merge feature
...
Resolved 'feature' using previous resolution
自动合并失败,解决冲突后再次commit
上面显示通过上次的记录解决了,再次添加
git add file
查看不同 确认后进行commit
git diff --stage
git commit -m ""
切回上一个工作的分支
git checkout -
3.2 历史记录、索引
3.2.1 good commit
- Good Commits 对于保存代码库的历史是有帮助的
- 有助于:
- 调试、故障清除
- create release notes
- code review
- 回滚
- 将代码和问题或ticket联系
- 如果你的代码没有做太多事情,使用git commit -m
- 如果你的代码包含了非常复杂的想法,一行的message是不够的,你需要在message当中添加描述性正文,git 在默认情况下69个字符截断变成...
一行标题
正文,描述你的做法,不是翻译代码
3.2.2 git log
- 显示repository存储历史记录的基本命令
git log
commit 1e2801f2dbf8f2792e7c90a5aaa9d2c3ad863a5e (**HEAD ->** **feature**, **master**)
Author: yangyihang <yangyihang5@163.com>
Date: Thu Feb 10 21:04:55 2022 +0800
Initial commit
git log --since="<yesterday | 2 weeks age>"
log文件重命名或移动的历史
git log --name-status --follow --<file>
筛查满足特定表达式的commit message
git log --grep <regexp>
git log --grep=mail --author=nina --since=2.weeks
Diff-Filter A 添加 D 删除 M 修改 R 重命名
git log --diff-filter = R --stat
- 上标 referencing commits
- ^ or ^n
- no args == ^1: the first parent commit
- n: the n^th parent commit
- ~ or ~n
- no args == ~1: the first commit back, following 1st parent
- n: number of commits back, following only 1st parent
- ^ 和 ~ 可以结合使用
- ^ or ^n
3.2.3 git show
git show <commit>
show文件的change
git show <commit> --stat
查看某个commit当中的file
git show <commit>:<file>
3.2.4 diff
- show differrece:
- 不同commit之间的
- 暂存区和仓库之间的
- 工作区的内容
unstaged changes 下一个commit可能发生的changes
git diff
staged changes 下一个commit必然发生的changes
git diff -staged
A与B分支之间的diff,展示B上的修改
git diff A B
git diff A..B
list 已经与master合并的分支 准备清理掉
git branch --merged master
list 没有与master合并的分支 也许可以合并
git branch --no-merged master
强制切换分支 覆盖修改
git checkout -f <branch>
3.3 撤销修改
3.3.1 git checkout
git checkout 能够恢复工作树文件或者切换分支
- git checkout发生了什么
- HEAD指针指向另外一个分支
- copy 从stage中 commit的快照到暂存区
- 将分支的内容从暂存区更新工作区
注意:以下均为破坏性操作,可能会覆盖工作区和暂存区的文件,并不会警告
- git checkout --file
- 使用当前暂存区的文件版本 替换 工作区的文件, 暂存区的是最近一次commit的快照(包括add)
-
git checkout -- <file_path>
- 同样的使用暂存区的版本覆盖工作区
-
git checkout < commit > --file
- 将暂存区更新为该commit
- 将工作区更新为暂存区内容
-
git checkout < commit > -- <file_path>
- 工作区和暂存区都发生变化
-
git checkout < deleting_commit >^ -- <file_path>
-
恢复删除的文件
-
在某个commit我们删除了文件,找到它的parent commit,使用这个commit进行恢复
-
3.3.2 git clean
git clean
- 清理工作区,只会删除untracked files, 那些在工作区但是没有add到暂存区的内容
- 无法撤销
- --dry-run, list将要被删除的内容
- -d 清理文件的同时也会清理目录
3.3.3 git reset
- 根据参数决定指向什么命令
- 默认git reset == git reset -mixed
- 和checkout相比,checkout移动头指针,但是分支的指向是没有变化的,reset会移头指针,也会让branch的指向发生变化
- 携带路径参数,git reset则不会修改头指针,只是修改文件
get reset --soft HEAD~
不经常使用 仅仅是HEAD的指向改变了,当前的branch跟着变化 相当于HEAD之前的指向变成了dangling
HEAD~相当于 HEAD~1 HEAD指向的commit的parent commit 中的第一个
默认不带参数相当于 --mixed HEAD的指向变化,branch跟着变化 并且暂存区变化 但是工作区是不变的
get reset --mixed HEAD~
HEAD指向变化 branch跟着变化 暂存区变化 工作区也和暂存区一致
破坏性操作 不能撤销 理论上只要最后的commit还能找到可以恢复原状
get reset --hard HEAD~
- soft
- mixed
- hard
HEAD指向不变 HEAD对应的commit的内容从仓库覆盖到暂存区 工作区保持不变
git reset <file-path>
也可以指定一个commit进行暂存区文件的覆盖
git reset <commit> -- <file-path>
如何撤销reset
- git 保存了HEAD之前的值 称为ORIG_HEAD
- 在进行reset或者merge等操作的时候,HEAD的值都会保存在ORIG_HEAD
git reset ORIG_HEAD
3.3.4 git revert
- 安全重置
- git revert会创建一个commit引导一个和特定commit相反的更改
- 原始提交会保存在repository里,不会改变历史记录
- 希望撤销一个已经公开的commit
get revert <commit-sha1>
3.4 rewrite history
3.4.1 git amend
- 简单快捷的对之间的commit修改的方式
- 将git add的内容添加到上一次commit当中
- SHA1会改变
git commit --amend
3.4.2 rebase
- 开发分支和master越走越远,不希望history里面存在混乱的merge
- 可以从master把最近的修改拉下来,将我们的分支的commits的parent commit修改为master此时的commit
- rebase = 给commit一个新的partent(base commit)
rebase做了什么
- 将HEAD从开发分支切到master上面
- 在master最新的commit的基础上,添加开发分支上最后一个commit的修改
- 此时可能出现合并冲突,修复即可
交互式变基(这部分不咋懂)
- 变基过程中commit会回放,commit允许edited、removed、combined、re-ordered、inserted,直到HEAD重置到copy的commit上
- 开始后会打开编辑器,提供默认的,也就是原来的commit过程,然后你可以修改这些commit过程,改变顺序、修改信息等等
<commit_to_fix>^表示当前分支的第一个parent commit,原来的master
git rebase -i <commit_to_fix>^
3.4.3 abort
--abort 安全选项 取消变基
git rebase --abort
- 使用前备份当前branch
git branch my_branch_backup
- 如果rebase成功但是你搞错了
git reset my_branch_backup --hard
四、github
4.1 forks and repositories
4.1.1 github && git
- git
- 开源版本控制软件
- 可以在本地运作的工具、工作流
- github
- 开发人员协作的关键
- 建立在git之上的工具
- Repository hosting 仓库托管
- Browse code 浏览代码
- issues
- pull requests
- forks
4.1.2 remotes
- remote是存储在非本地的一个其他地方的git repository
- origin 的git对于你clone的服务器提供的默认名
- 通过一个URL进行clone remote repository时,会fetch整个repository,将其拷贝到本地的.git目录下
- 对于remote可以有权限的控制,Read/Write 或者 Read Only
查看设置了哪些remote
git remote -v
4.1.3 fork
- 存储在GitHub账户当中的一个repository的copy,比如一个其他人的仓库,fork将其变成自己的
- 对于自身来说没有了权限的限制
- 通过pull request将自己的fork和original project进行merge
- 当你在fork进行work的时候,如果其他更改merge到了source repository上
- this branch is commits behind pallets: master
- 如果希望跟上original project, 需要设置upstream
- upstream是创建fork的时候的base repository,默认情况下没有设置
git remote add upstream <URL>
4.1.4 workflow
Tracking branches
- 将branch绑定到upstream branch
- 之后就可以不带参数进行pull | pull 操作
to checkout a remote branch, with tracking
git checkout -t origin/feature
第一次将分支推送到远程
tell git which branch to track the first time you push
git push -u origin feature
展示本地分支的upstream, 带有ahead或者behind的commit数量信息
git branch --vv
4.1.5 fetch
- without merging or pulling
- 让本地的repository 和remote保持一致
- fecth 会将server上的修改都pull down
- 但是不会修改本地的repository
4.1.6 pull
- 从remote repository 上 pull down所有修改到本地的repository,然后merge到本地分支上
- git pull = git fetch && git merge
- 如果upstream上有新的修改,就相当于做了一个merge commit
- 如果没有,就相当于fast-forword
4.1.7 push
- 将本地的修改推送到remote repository上
- git只会允许在不造成conflict的修改的push, 如果remote repository发生其他修改,需要先pull,再update本地的副本,然后才可以push
查看哪些commits还没有push到upstream上
git cherry -v
fetch,update local branch to copy upstream branch, then replay any commits you mode via rebase
git pull --rebase
- git 不会自动将本地的tags push到remote上
git push <tag-name>
git push --tags
4.1.8 为开源项目做贡献的建议:pull request
- 打开PR前
- 保证commit history干净整洁,有需要则进行rebase
- 测试代码
- pull upstream的修改(倾向于rebase)
- 检查根目录下CONTRIBUTING文件
- 打开PR后
- 正文描述更改
- 链接可能修复的相关问题
- 检查维护者的评论
4.2 破坏性操作
4.2.1 local 破坏性操作
git checkout -- <file>
暂存区的文件会被覆盖
git reset --hard
暂存区和工作区的修改都将被覆盖
git stash --include-untracked
保存工作区的修改到stash
4.2.2 remote 破坏性操作
这些操作可以重写history
- rebase
- amend
- reset
如果你的代码是共享的,不要使用以下方式push
git push -f
4.2.3 恢复丢失的内容
- ORIG_HEAD
- ORIG_HEAD会保存reset或者merge之前的HEAD
- 检查备份
- github
- coworker
撤销之前的merge
--merge 保留当前还没有commit的change
git reset --merge ORIG_HEAD
查找到不被引用的commit
- git 会默认情况下保存这些不被引用的commit两周
- 查看rough log,会存在相关记录
HEAD@{2} 代表HEAD在两步之前的值
git reflog