基本理解

  1. 工作区。 编辑文件的地方

  2. 暂存区。 通过git add将文件(.表示将所有文件)添加进暂存区,同时将内容添加进Objects,暂存区只有index并指向Objects

  3. 版本库。 通过git commit -m 备注将暂存区的index同步到版本库。

  4. 远程仓库。 常见的有Gitee、GitHub,可以申请一个仓库,且这个仓库在本地通过git clone git remote add origin 远程地址后默认会有一个origin指向这个远程仓库,这就是一个约定的名称,没有什么其他含义。

  5. commit的本质是将版本库中的索引同步暂存区的索引。

  6. HEAD是当前分支commit的指针

  7. branch分支,master为主分支由git默认创建,一般Master是稳定版,要测试或者添加新功能时就会使用新的分支,分支之间互不影响,这就很好的隔离了代码。开发完后再merge到master

  8. 几个状态,unstaged,没有写入暂存区。 modify 修改状态,写入暂存区后变为stage状。 unmodify没有修改。commit添加到暂存区后等待提交的状态


需要理解以上概念才能更深一步使用

本地仓库.git文件存储了相关的objects,结构如下:

使用前

git config --global user.name 姓名
git config --global user.email 邮箱
# 配置下你的账号、邮箱,后面的commit都会带上这个标识

常用命令

初始化

git init # 初始化git,生成一个.git的隐藏文件。或者通过clone和远程仓库直接完成关联
git remote add <remote> 远程仓库地址 # 和远程仓库关联,默认是用origin指代远程仓库,类似别名。如果不希望每次都要重新输入账号密码,可以在远程仓库上生成一个公钥

查看状态

git status # 查看文件状态
git log # 查看提交日志,reset后,后面的commit就不会显示了
git log --graph --pretty=oneline --abbrev-commit # 格式化查看log
git reflog # 查看git操作日志,这里可以恢复reset掉的commit
git show [commit_id] # 查看某次提交的内容
git rev-parse <branch> # 查看分支最新commit的hash值

查看区别

git diff # 查看工作区和暂存区的区别
git diff --cached # 查看暂存区和版本库的区别
git diff HEAD # 查看工作区和版本库的区别

add和回滚

git add filename / . # 添加到暂存区,并将内容添加Objects
git checkout filename / . # 将暂存区的内容恢复到工作区,如果已经add了,这里就没有效果了,需要先reset
git restore filename # 对于没有add的内容可以直接恢复
git restore --staged filename # 对于已经add了的文件,可以取消stage变为unstage,且不会改变工作区的内容。取消的是最近一次的add。
git rm --cached filename # 删除暂存区中的文件,这和restore --staged不同,rm是删,restore是移除最近的一次add,对于untracked文件,只能rm来移除add

commit和回滚

git commit -m <desc> 提交说明 # 将暂存区的内容提交到版本库,实际就是同步index
git reset [--soft | --mixed | --hard] <commit_id>
# 将HEAD指针移动"到"指定的commit, HEAD^和HEAD~1都表示前一个commit,或者具体的commit_id。
# --soft只回滚代码库, 工作区和暂存区不改变。
# --mixed默认选项,回滚代码库和暂存库,工作区不变,使用后git status 变为unstage状态。
# --hard 慎用!,全部同步修改,会覆盖工作区。reset回滚时会删得回滚后面的commit
git revert HEAD # 对比reset,revert相对更温和,保留所有commit,并再做一次提交。需要注意的是这里的HEAD是相对当前的前一次,和reset有区别

克隆和pull

git clone 地址 # 克隆一个项目
git pull [--rebase] # 拉取一个分支的内容并merge | rebase到当前分支,省略后面的origin brachName的前提是当前操作的分支和远程有关联
git fetch # 拉取但是不merge,可以手动merge
git merge | rebase # 合并分支,有多种模式

分支

git branch <branch> # 创建一个新的分支
git switch <branch>  # 切换到分支,后面同理
git switch -c <branch> [commit_id] # 创建并切换
git switch -c <branch> <origin/branch> # 创建本地分支,并和远程分支关联,可能需要先git fetch远程分支到本地
git switch <remote>/<branch> # 创建和远程仓库上的同名分支并同步内容
git branch -d <branch> # 删除分支,对于没有合并的无法删除
git branch -D <branch> # 删除分支,强制
git push <remote> --delete <branch> # 删除远程分支
git push <remote> <branch> # 创建远程分支
git push -u <remote> <branch> # 创建远程分支并和当前分支关联
git branch --set-upstream-to=<remote>/<branch> <branch> # 将远程分支和本地分支关联
git branch -m <branch> <new_branch> # 修改分支名称
git branch -vv # 查看分支track关联情况

分支一但和远程关联track后就可以直接pull push

worktree

worktree的作用是对同一个仓库进行并行管理。

典型场景:

在一个$GIT_DIR下要想同时编辑多个分支,通常是git stash,再switch

但是worktree却可以轻松实现,且要方便的多,就好像在不同的仓库中一样,不需要切换分支。

基本使用:

git worktree add <path> [-b] <branch> # 创建一个工作目录,并切换到其中, -b创建分支
git worktree list # 查看工作目录
git worktree remove <path> # 删除工作目录。一个分支同时只能在一个worktree中作为工作分支(当前分支)

这和复制工作目录是完全不一样的,worktree共享同一个.git,在任意一个worktree中的应用,在其他worktree中也会生效。

pull之前查看变化

git pull = git fetch + git merge | rebase

git log --name-status -2 [filename/path] # 查看最近2次提交修改的文件名称
git log -p -2 [filename/path] # 查看最近2次提交了什么具体更新

git fetch之后可以通过

git log --name-status <remote>/<branch>...<branch> [<filename/path>] # 查看fetch之后的<branch>分支和当前本地版本库的<branch>分支之间的差异,名称上的
git log -p <remote>/<branch>...<branch> [<filename/path>] # 同上,查看具体的内容差异
git diff <branch>...<remote>/<branch> [<filename/path>] # 同上

或者git pull之后查看commitId之间的差异

git diff <commit_id>...<commit_id> [<filename/path>]

标签

git tag <tag> # 为当前分支最后一次commit 打标签
git tag -a <tag> -m <tag_desc> # 添加标签时带上描述
git tag -a <tag> -m <tag_desc> commitId # 给指定的commit打上tag
git push <remote> <tag> # 推送到远程仓库
git push <remote> --tags # 一次推送所有不再远程仓库的tags
git tag -d <tag> # 删除tag
git push <remote> --delete <tag> # 删除远程tag
git show <tag> # 查看标签信息
git tag # 查看所有标签,按字母顺序

提交冲突

如果对同一个分支,比如dev分支,多人开发,很可能产生提交冲突,即origin上面的commit比你本地的新

这个时候需要先pull,解决冲突(和分支合并解决方式相同),然后add commit

注意:

慎用push --force,这会忽略冲突强制覆盖

建议使用push --force-with-lease,避免覆盖别人提交的更新,这并不是多此一举,比如你使用amend,rebase,squash等操作,就需要这种强制推送。

常见的冲突

  1. merge可以直接ff的,不会出现冲突,除非使用-no-ff,否则也不会产生merge commit
  2. merge无法直接ff,git能够识别自动解决冲突,此时会产生一个merge commit
  3. merge无法直接ff,git无法自动解决,手动解决冲突,再addcommit
  4. rebase出现的冲突(rebase虽然是提交重放,但是同样也有冲突,比如commit的上下文被其他commit所修改),同样需要解决冲突,然后addrebase --continue,修改提交信息等,相当于重新提交
  5. revert出现冲突,同样的add,continue,commit等等

核心

提交,冲突,解决冲突,add ,再提交

分支合并

本地分支之间的合并,本地和远程分支的合并。

merge方式

分为快速合并和普通合并,快速合并不会产生新的合并commit

快速合并是指当当前分支的commit都在另一个分支时,即另一个分支是基于当前分支的最后一次提交实现的,会默认使用快速合并模式

可以通过--no-ff强制不使用快速合并,此时就算可以ff也会有一条merge commit,保留Merge commit的好处是便于审计和回滚。

# revert回滚相比reset的好处是可以保留原始提交
git revert -m 1 <merge_commit_sha> # -m 用来表示以谁为主, 1表示当前分支,2表示被合并分支,如果只是针对普通的commit revert就不需要指定

合并冲突:merge时会提示哪里有冲突,冲突文件上也会有标记,需要手动修改解决冲突,然后add commit

git merge -ff <branch>          # 快速合并,默认参数,如果快速合并,删除被合并的分支,那么整个分支信息就会丢掉
git merge -ff-only <branch>     # 只有快速合并的情况才合并
git merge --no-ff       # 不使用快速合并,合并时会产生新的提交需要用-m描述下,此时就算删掉了被合并的分支,主分支上也会有合作记录
git merge -n <branch>     # 合并分支,不会在合并后显示合并前后的不同状态
git merge -stat <branch>  # 合并分支,合并结束后显示合并前后的不同状态
git merge -e <branch>     # 合并分支,合并前调用编辑器,可自行编写commit

merge方式合并逻辑:

比如:master分支(蓝色)合并dev分支(绿色)

实际就是将分支冲突的部分,单独做了一次提交。

过程:

  1. git switch master
  2. git merge dev
  3. 解决冲突
  4. git add
  5. git commit (head *)

最终的master commit graph就是这样:

graph LR
linkStyle default fill:none,stroke:#333,stroke-width:1px;
classDef common fill:#fff,color:#fff,stroke:#333,r: 22;
classDef dev fill:#4ed1a1,color:#fff,stroke:#333,r: 22;;
classDef master fill:#b3e3ff,color:#fff,stroke:#333,r: 22;;
a(( )) --> b(( )) --> c(( ))
c --> d1(( )) --> d2(( ))
c --> m1(( )) --> m2(( ))
d2 --> h((*))
m2 --> h((*))
class a,b,c common
class d1,d2 dev
class m1,m2,h master

rebase方式

Tip

更整洁干净的提交树,但是可能改变历史,需要force push,正常下应优先使用rebase方式。

git rebase <branch> # 和merge最大的区别是,merge会保留所有分支的提交记录(除非使用--squash消除被合并分支记录),但是rebase为了让合并树看着更整洁,会取消某些提交生成新的提交,即在最后的合并树上是看不到原来的某些提交的,这对于回滚有较大难度
git rebase --continue # 遇到合并冲突会每次显示一次冲突,处理完后再继续
git rebase --abort # 放弃所有的冲突处理,恢复rebase前的情况
git rebase --skip # 跳过当前的补丁,处理下一个补丁,不建议使用,补丁部分的commit会丢失

rebase方式合并逻辑:

比如:master分支(蓝色)合并dev分支(绿色)。

实际开发中如果采用rebase方法,都是先在dev分支中rebase master,然后再回到master中通过fast-forward merge dev,因为要确保master分支的清晰。

git rebase branchName

本质上就是当前分支基于指定brachName的最新commit,“重新播放”一遍提交。所以切换为brachName后可以进行快速合并。

过程:

  1. git switch dev
  2. git rebase master
  3. 解决冲突
  4. git add
  5. git rebase --continue
  6. 循环3-5直至解决所有冲突,并退出rebase环境
  7. git switch master
  8. git merge dev

合并前:

graph LR
linkStyle default fill:none,stroke:#333,stroke-width:1px;
classDef common fill:#fff,color:#fff,stroke:#333,r: 22;
classDef dev fill:#4ed1a1,color:#fff,stroke:#333,r: 22;;
classDef master fill:#b3e3ff,color:#fff,stroke:#333,r: 22;;
a(( )) --> b(( )) --> c((A ))
c --> d1((E )) --> d2((F ))
c --> m1((C )) --> m2((D ))
class a,b,c common
class d1,d2 dev
class m1,m2,h master

devrebase master:

graph LR

linkStyle default fill:none,stroke:#333,stroke-width:1px;

classDef common fill:#fff,color:#333,stroke:#333,r: 22
classDef dev fill:#4ed1a1,color:#333,stroke:#333,r: 22;
classDef master fill:#b3e3ff,color:#333,stroke:#333,r: 22;
classDef hidden display: none

a(( )) --> b(( )) --> c((A ))
c --- other(( ))
c --> m1((C )) --> m2((D ))
m2 --> d1((E')) --> d2((F'))
class a,b,c common
class d1,d2 dev
class m1,m2,h master
class other hidden

操作过程就像先找到base,然后将base后面的commit都直接移动到master顶部,但是不是直接移过去,而是为每个dev commit生成一个对应的new commit

简单说就是将当前branchbase后面的commit重新提交到rebase指定的branch后面。

**原理:**在dev中git rebase master,首先寻找共同的祖先A,然后把HEAD指向master最后的提交D,然后把dev分支A后面的提交逐个合并,这个过程可以理解为基于D重新提交了一遍E,F,重新提交时,其他内容不变(时间,作者,注释等),只有commit hash重新生成了。中间如果产生了冲突,就需要解决,然后使用git rebase --continue继续提交。

最后在mastermerge dev:

graph LR

linkStyle default fill:none,stroke:#333,stroke-width:1px;

classDef common fill:#fff,color:#333,stroke:#333,r: 22
classDef dev fill:#4ed1a1,color:#333,stroke:#333,r: 22;
classDef master fill:#b3e3ff,color:#333,stroke:#333,r: 22;

a(( )) --> b(( )) --> c(( A))
c --> m1((C )) --> m2((D ))
m2 --> d1((E' )) --> d2(( F'))
class a,b,c common
class d1,d2 dev
class m1,m2,h master

自此分支合并结束,commit会非常清爽。

本地pull远程分支进行合并时,也可以使用rebasegit pull --rebase === git fetch git rebase

合并多个commit

可以使用rebase合并多个commit

git rebase -i [start-commitId] end-commitId # 省略start,表示Head,当前最新一个commit,end-commitId表示合并到这个id之后的那个commit。
# 比如有1->2->3->4这4个commit,git rebase 4 1 实际合并的是4 3 2这3个,即不包括基线commit
git rebase -i HEAD~3 # 这种模式使用的较多,就是合并最新的3次commit
# 使用命令后,会提示你如何合并,比如可以把4,3合并到2,也可以2,3合并到4,只要把想要丢弃的commit,由pick改成s(squash)即可,最后根据提示改下commit -m 的内容即可

rebase编辑的内容,就是git执行的顺序,比如1234,如果要把4合并到2,可以:

pick 1
pick 2
squash 4  # 移动4到2的后面
pick 3

git会先pick 1,也就是原样保留1,然后pick 2,然后将4的提交合并到2上,最后pick 3,中间如果有冲突,就会提示冲突,需要处理冲突,然后rebase --continue继续执行。

编辑中间某个提交

比如1->2->3->4,现在想要编辑3,可以git rebase -i HEAD~2,出现:

pick 3
pick 4

需要将3的pick改成edit,即可编辑3所在的commit,此时可以修改文件,然后git addgit commit --amend,完成编辑,然后继续git rebase --continue 提交后面的4。

另一个例子,比如想把3拆分为3’和3”2个提交:

git rebase -i HEAD~2

将3改位edit,保存

此时进入3这个commit的状态,使用git reset HEAD^移除3的index和commit,但是保留了工作区,此时就可以正常的add,commit

改完git reabse --continue后面的commit

暂存未commit的更改

临时有新的紧急任务需要切换到其他分支,但是当前工作又没有完成时,又不想此时提交,但是不提交又切换不了

可以使用stash暂存更改,切回来后恢复即可

git stash # 暂存
git stash push -m <desc> # 暂存并提交
git stash apply [--index] <stash_id> # 恢复一个stash,--index表示同时恢复暂存区
git stash pop [--index] HEAD # 也是恢复,但是apply恢复后stash list中会保留,pop则会删除。同时这里的HEAD指向当前的前一个,和reset中的HEAD有区别
git stash list # 查看stash列表

注意:可以有多个stash,比如当前分支stash,切换到另一个分支又stash,再切回来通过stash apply恢复的是最后一次,并不一定是当前分支的,所有一定要明确要恢复的id

恢复stash apply/pop造成的修改

如果在某个分支上错误的执行的stash apply/pop应用了当前stash stack中的修改,可以直接使用git reset --merge,git merge --abort来取消

如何恢复pop掉的stash?

git fsck --no-reflog # 找到悬空的commit
git stash show -p <commit-hash> # 查看内容
git stash apply <commit-hash> # 恢复

把提交的commit提交到另一个branch

git cherry-pick <commit_id> # 可能会出现冲突

cherry-pick有个典型的使用场景,比如在feature分支上做了一个不相关的提交A,此时就可以switch到dev,然后cherry-pick这个commit,commitId变成A'

然后回到feature分支上,使用rebase dev,此时由于AA'提交的内容完全一样,会直接变成A',并且不会出现冲突,就好像是dev上开发完了,然后合并到feature上一样。

分批提交

场景: 一个文件一批修改,多次提交。

如果能明确显示多个hunk,那很简单,多次add(如果编辑器支持,比如neovim的GitSigns插件就可以),然后提交即可。

如果不能明确显示多个hunk或者插件不支持,就需要用到下面的方法:

  1. 针对可以多个hunk的文件

    git add -p <file> # 显示多个hunk,并且可以指定每个hunk的提交信息
     
  2. 针对不能多个hunk的文件

    git add -e <file> # 手动编辑处理,直接删掉本次不add的内容即可,这并不会删掉工作区的内容,只是删掉了本次add的内容
    # git add -p <file> 后也可以通过e命令进入edit模式。

还一种情况,如果文件是untracked的,那git add -p | -e是没有效果的,因为index中没有这个文件。

此时可以:

  1. git add -N <file> 将文件添加到暂存区,但是内容不add,也就是只添加了track。然后可以按照上面的方法来add。
  2. git add <file> 正常add,然后使用git reset -p <file>来恢复,也就是逆操作,但是这种操作涉及到patch文件的元信息修改,所以不推荐。

恢复删除的提交

借助git reflog查看所有的操作记录,找到想要恢复的commit,然后git reset --hard <commit_id>即可。

注意

这是一个危险操作,reflog是你本地的操作记录,不是remote仓库的,所以可能会引起意外的情况,同时—hard也是危险操作,会丢失当前工作区。

ignore tracked文件

对于已经在暂存区的文件ignore是不会生效的,需要删除暂存区的内容,再提交即可

git rm -f --cached <file> # 删除暂存区索引

另:

git rm <file> # 删除工作区和index的内容
# 等效于
rm <file>
git add <file>

ours and theirs

在从index中checkout,产生冲突时可以使用git checkout --oursgit checkout --theirs来快速选择合并冲突的内容。

正常情况下的ourstheirs就是常规意义上的ourstheirs

只有一个例外rebase

git rebasegit pull --rebase中,这里的ours指rebase的分支,而不是当前的工作分支

因为rebase的分支被当作主要分支,也就是树干,而当前分支会被当作要被合并的分支,你的视角是在树干那。

man git-checkout

Note that during git rebase and git pull —rebase, ours and theirs may appear swapped; —ours gives the version from the branch the changes are rebased onto, while —theirs gives the version from the branch that holds your work that is being rebased.

This is because rebase is used in a workflow that treats the history at the remote as the shared canonical one, and treats the work done on the branch you are rebasing as the third-party work to be integrated, and you are temporarily assuming the role of the keeper of the canonical history during the rebase. As the keeper of the canonical history, you need to view the history from the remote as ours (i.e. “our shared canonical history”), while what you did on your side branch as theirs (i.e. “one contributor’s work on top of it”).