前言
学习Git,从底层开始了解git的命令语句所执行的操作。看完本文,希望你能够了解Git,“实践是检验真理的唯一标准”,掌握Git,需要你在日常工作中,不断深化其命令以及底层逻辑。
版本控制系统
版本控制系统是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。版本控制系统不仅可以应用于软件源代码的文本文件,而且可以对任何类型的文件进行版本控制。
版本控制系统发展可以分为三个阶段:
- 本地版本控制系统
- 集中式版本控制系统
- 分布式版本控制系统
本地版本控制系统
RCS,现今许多计算机系统上都还看得到它的踪影。 RCS 的工作原理是在硬盘上保存补丁集(补丁是指文件修订前后的变化);通过应用所有的补丁,可以重新计算出各个版本的文件内容。
集中版本控制系统
集中化的版本控制系统(Centralized Version Control Systems,简称 CVCS)应运而生。 这类系统,诸如 CVS
、Subversion
以及 Perforce
等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。
以上两者都存在的致命性问题:
只要整个项目的历史记录被保存在单一位置,就有丢失所有历史更新记录的风险。
分布式版本控制系统
分布式版本控制系统(Distributed Version Control System,简称 DVCS)
客户端并不只提取最新版本的文件快照, 而是把代码仓库完整地镜像下来,包括完整的历史记录。 这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。 因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。
Git使用
Repository
版本库又名仓库,一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。
初始化仓库 —— git init
创建目录
pwd
命令用于显示当前目录。
初始化仓库
通过git init
命令把这个目录变成 Git 可以管理的仓库
多了一个.git
的目录,这个目录是 Git 来跟踪管理版本库的,如果你没有看到 .git 目录,那是因为这个目录默认是隐藏的,用ls -ah
命令就可以看到了。
克隆现有的仓库 —— git clone
git clone
git clone
命令,Git 克隆的是该 Git 仓库服务器上的几乎所有数据,而不是仅仅复制完成你的工作所需要文件。
执行 git clone
命令的时候,默认配置下远程 Git 仓库中的每一个文件的每一个版本都将被拉取下来。
克隆仓库的命令是 git clone <url>
在当前目录下创建一个名为 你拉取项目的名字
的目录,并在这个目录下初始化一个 .git
文件夹, 从远程仓库拉取下所有数据放入 .git
文件夹,然后从中读取最新版本的文件的拷贝。 如果你进入到这个新建的 你拉取项目的名字
文件夹,你会发现所有的项目文件已经在里面了,准备就绪等待后续的开发和使用。
在
clone
时出现Timeout
情况时,可以设置Git代理(此时需开启魔法)git config --global http.proxy "127.0.0.1:7890"
然后继续
clone
自定义本地仓库名称
通过额外的参数指定新的目录名git clone <url> 自定义仓库名
这会执行与上一条命令相同的操作,但目标目录名变为了 自定义仓库名
。
Git 支持多种数据传输协议。 上面的例子使用的是 https://
协议,不过你也可以使用 git://
协议或者使用 SSH
传输协议,比如 user@server:path/to/repo.git
。
编辑并添加文件
在已经准备好的 Git 仓库中编辑一个readme.txt
文件,内容如下:
bash Git is a version control system. Git is free software.
接下来,我们可以通过2个命令将刚创建好的readme.txt
添加到Git仓库
-
用命令
git add
告诉 Git,把文件添加到仓库:$ git add readme.txt
执行上面的命令,没有任何显示,说明添加成功
提交变动到仓库
用命令git commit
告诉 Git,把文件提交到仓库:
$ git commit -m "wrote a readme file" [master (root-commit) 50ed06b]
git commit
命令,-m
后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。
git commit
命令执行成功后会告诉你:
- 1 file changed:1个文件被改动(我们新添加的readme.txt文件)
- 2 insertions:插入了两行内容(readme.txt有两行内容)
为什么 Git 添加文件需要add,commit
一共两步呢?因为commit
可以一次提交很多文件,所以你可以多次add
不同的文件,比如:
$ git add file1.txt
$ git add file2.txt file3.txt
$ git commit -m "add 3 files."
查看Git仓库当前转态变化
成功地添加并提交了一个readme.txt
文件,接下来让我们继续修改readme.txt
文件,改成如下内容:
Git is a distributed version control system. Git is free software.
查看 git status 结果
git status
命令可以让我们时刻掌握仓库当前的状态,上面的命令输出告诉我们,readme.txt
被修改过了,但还没有准备提交的修改。
比较变动
虽然 Git 告诉我们readme.txt
被修改了,但并没有告诉我们具体修改的内容是什么,假如刚好是上周修改的,等到周一来班时,已经记不清上次怎么修改的readme.txt
,这个时候我们就需要用git diff
这个命令查看相较于上一次暂存都修改了些什么内容了
运行git diff
命令,如:git diff readme.txt
git diff
顾名思义就是查看 difference,显示的格式正是 Unix 通用的 diff
格式,可以从上面的输出看到,我们在第一行添加了一个distributed
单词。
综合操作
知道了对readme.txt
作了什么修改后,再把它提交到仓库就放心多了,提交修改和提交新文件是一样的两步
git add
,如:git add readme.txt
- 在执行第二步之前,再次查看仓库转态
git status
,git status
告诉我们,将要被提交的修改包括readme.txt
,下一步,就可以放心地提交了
- 在执行第二步之前,再次查看仓库转态
git commit
,如:git commit -m "add distributed"
- 提交后,我们再用
git status
命令看看仓库的当前状态
- 提交后,我们再用
查看日志
如果嫌输出信息太多,看得眼花缭乱的,可以试试加上--pretty=oneline
参数:
git log --pretty=oneline
git lg
命令,就可以更清楚地看到提交历史的时间线
Git回退
假设我们需要将 readme.txt
回退到上一个版本,也就是 wrote a readme file
的这个版本,
首先,Git 必须知道当前版本是哪个版本,在 Git 中,用HEAD
表示当前版本,也就是最新的提交e55063a
,上一个版本就是HEAD^
,上上一个版本就是HEAD^^
,当然往上100个版本写100个^
比较容易数不过来,所以写成HEAD~100
现在,我们要把当前版本add distributed
回退到上一个版本wrote a readme file
,就可以使用git reset
命令:
$ git reset --hard HEAD^
现在让我们看看readme.txt
的内容是不是版本wrote a readme file
:
$ cat readme.txt
Git is a version control system. Git is free software.
果然还原到最初wrote a readme file
这个版本了。
Git 的版本回退速度非常快,因为 Git 在内部有个指向当前版本的HEAD
指针,当你回退版本的时候,Git 仅仅是把HEAD
从指向add distributed
Git重置
git reflog
现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的commit id
怎么办?
好在 Git 提供了一个命令git reflog
用来记录你的每一次命令,当你用git reset --hard HEAD^
回退到wrote a readme file
版本时,再想恢复到add distributed
,就可以通过git reflog
命令找到add distributed
的commit id
。
git reflog
$ git reflog
从上面的输出可以看到,add distributed
的commit id
是e55063a
,现在,我们就可以通过 git reset --hard e55063a
切换到最新的版本上了。
工作区和暂存区
Git 和其他版本控制系统如 SVN 的一个不同之处就是有暂存区的概念
工作区(Working Directory)
就是你在电脑里能看到的目录,比如我的learngit文件夹就是一个工作区
版本库(Repository)
工作区有一个隐藏目录.git
,这个不算工作区,而是 Git 的版本库。
Git 的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)
的暂存区,还有 Git 为我们自动创建的第一个分支master,以及指向 master
的一个指针叫HEAD
。
前面讲了我们把文件往 Git 版本库里添加的时候,是分两步执行的:
- 第一步是用
git add
把文件添加进去,实际上就是把文件修改添加到暂存区; - 第二步是用
git commit
提交更改,实际上就是把暂存区的所有内容提交到当前分支。
因为我们创建 Git 版本库时,Git自动为我们创建了唯一一个master
分支,所以,现在,git commit
就是往 master
分支上提交更改。
git add
命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行git commit
就可以一次性把暂存区的所有修改提交到分支。
一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是“干净”的
版本管理
Git 处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。 与许多其它版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。
撤销修改(git commit --amend)
有时候我们提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了。 此时,可以运行带有 --amend 选项的提交命令来重新提交:git commit --amend
$ git commit --amend
这个命令会将暂存区中的文件提交。 如果自上次提交以来你还未做任何修改(例如,在上次提交后马上执行了此命令), 那么快照会保持不变,而你所修改的只是提交信息。
文本编辑器启动后,可以看到之前的提交信息。 编辑后保存会覆盖原来的提交信息。
例如,你提交后发现忘记了暂存某些需要的修改,可以像下面这样操作:
$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend
最终你只会有一个提交——第二次提交将代替第一次提交的结果。
当你在修补最后的提交时,并不是通过用改进后的提交 原位替换掉旧有提交的方式来修复的, 理解这一点非常重要。从效果上来说,就像是旧有的提交从未存在过一样,它并不会出现在仓库的历史中。
修补提交最明显的价值是可以稍微改进你最后的提交,而不会让“啊,忘了添加一个文件”或者 “小修补,修正笔误”这种提交信息弄乱你的仓库历史。
取消暂存的文件(git reset)
接下来我们看看如何操作暂存区和工作目录中已修改的文件。 这些命令在修改文件状态的同时,也会提示如何撤消操作。例如,你已经修改了两个文件并且想要将它们作为两次独立的修改提交, 但是却意外地输入 git add *
暂存了它们两个。如何只取消暂存两个中的一个呢? git status
命令提示了你:
$ git add *
$ git status
On branch master Changes to be committed: (use "git reset HEAD ..." to unstage)
renamed: LICENSE -> LICENSE.md
modified: readme.txt
在 “Changes to be committed” 文字正下方,提示使用 git reset HEAD <file>...
来取消暂存。 所以,我们可以这样来取消暂存 readme.txt
文件:git reset HEAD
$ git reset HEAD readme.txt Unstaged changes after reset: M readme.txt
$ git status
这个命令有点儿奇怪,但是起作用了。 readme.txt
文件已经是修改未暂存的状态了。
git reset
确实是个危险的命令,如果加上了--hard
选项则更是如此。 然而在上述场景中,工作目录中的文件尚未修改,因此相对安全一些。
撤消对文件的修改(git --checkout)
git checkout – file
$ git checkout -- readme.txt
$ git status
请务必记得
git checkout -- <file>
是一个危险的命令。 你对那个文件在本地的任何修改都会消失——Git 会用最近提交的版本覆盖掉它。 除非你确实清楚不想要对那个文件的本地修改了,否则请不要使用这个命令。
删除文件
在 Git 中,删除也是一个修改操作,添加一个新文件test.txt
到 Git 并且提交
$ git add test.txt
$ git commit -m "add test.txt"
一般情况下,你通常直接在文件管理器中把没用的文件删了,或者用rm命令删了:$ rm test.txt
,这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status命令会立刻告诉你哪些文件被删除了。
现在你有两个选择,
-
一是确实要从版本库中删除该文件,那就用命令
git rm
删掉,并且git commit
$ git rm test.txt rm 'test.txt'
现在,文件就从版本库中被删除了。
小提示:先手动删除文件,然后使用git rm 和git add效果是一样的。
-
另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本
$ git checkout -- test.txt
git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
注意:从来没有被添加到版本库就被删除的文件,是无法恢复的!
-
场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令
git checkout -- file
-
场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令
git reset HEAD <file>
,就回到了场景1,第二步按场景1操作 -
场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,可以用命令
git reset --hard commit_id
,不过前提是没有推送到远程库
分支管理
Git 保存的不是文件的变化或者差异,而是一系列不同时刻的快照
在进行提交操作时,Git 会保存一个提交对象(commit object)。 知道了 Git 保存数据的方式,我们可以很自然的想到——该提交对象会包含一个指向暂存内容快照的指针。 但不仅仅是这样,该提交对象还包含了作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。
首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象, 而由多个分支合并产生的提交对象有多个父对象,
Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master。 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master 分支。 master 分支会在每次提交时自动向前移动。
创建、合并、删除分支
Git 会把仓库中的每次提交串成一条时间线,这条时间线就是一个分支。在 Git
里,每个仓库都会有一个主分支,即master
分支。HEAD
严格来说不是指向提交,而是指向master
,master
才是指向提交的,所以,HEAD
指向的就是当前分支。
一开始的时候,master
分支是一条线,Git 用master
指向最新的提交,再用HEAD
指向master
,就能确定当前分支,以及当前分支的提交点。
每次提交,master
分支都会向前移动一步,这样,随着你不断提交,master
分支的线也越来越长。
-
当我们创建新的分支,例如
dev
时,Git 新建了一个指针叫dev
,指向master
相同的提交,再把HEAD
指向dev
,就表示当前分支在dev
上。Git 创建一个分支很快,因为除了增加一个dev
指针,改改HEAD
的指向,工作区的文件都没有任何变化。不过,从现在开始,对工作区的修改和提交就是针对dev
分支了,比如新提交一次后,dev
指针往前移动一步,而master
指针不变。 -
假如我们在
dev
上的工作完成了,就可以把dev
合并到master
上。Git 怎么合并呢?最简单的方法,就是直接把master
指向dev
的当前提交,就完成了合并。所以 Git 合并分支也很快!就改改指针,工作区内容也不变 -
合并完分支后,甚至可以删除
dev
分支。删除dev
分支就是把dev
指针给删掉,删掉后,我们就剩下了一条master
分支
例
- 创建
dev
分支,切换到dev
分支:
$ git checkout -b dev
# git checkout命令加上-b参数表示创建并切换,相当于以下两条命令
$ git branch dev
$ git checkout dev
- 查看当前分支(列出所有分支)
$ git branch
- 在
dev
分支上正常提交,比如对readme.txt
做个修改,加上一行:Creating a new branch is quick.
,然后提交
$ git add readme.txt
$ git commit -m "branch test"
dev
分支的工作完成,我们就可以切换回master
分支
$ git checkout master
-
切换回
master
分支后,再查看一个readme.txt
文件,刚才添加的内容不见了!因为那个提交是在dev
分支上,而master
分支此刻的提交点并没有变 -
把
dev
分支的工作成果合并到master
分支上$ git merge dev
git merge
命令用于合并指定分支到当前分支。合并后,再查看readme.txt
的内容,就可以看到,和dev分支的最新提交是完全一样的。注意到上面的
Fast-forward
信息,Git 告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。 -
合并完成后,就可以放心地删除
dev
分支了$ git branch -d dev
-
删除后,查看
branch
,就只剩下master
分支了$ git branch
因为创建、合并和删除分支非常快,所以 Git 鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master
分支上工作效果是一样的,但过程更安全
git switch
切换分支使用git checkout <branch>
,而 Git 中撤销修改则是git checkout -- <file>
,同一个命令,有两种作用,确实有点令人迷惑
切换分支这个动作,用switch
更科学。因此,最新版本的 Git 提供了新的git switch
命令来切换分支
-
创建并切换到新的
dev
分支,可以使用$ git switch -c dev
-
直接切换到已有的
master
分支,可以使用$ git switch master
git switch
命令,比git checkout
要更容易理解。
分支管理策略
通常,合并分支时,如果可能,Git会用Fast forward
模式,但这种模式下,删除分支后,会丢掉分支信息。
如果要强制禁用Fast forward
(--no-ff
)模式,Git 就会在merge
时生成一个新的commit
,这样,从分支历史上就可以看出分支信息
例
-
创建并切换dev分支
$ git switch -c dev
-
修改
readme.txt
文件,并提交一个新的commit
$ git add readme.txt $ git commit -m "add merge"
-
切换回
master
$ git switch master
-
合并
dev
分支,请注意--no-ff
参数,表示禁用Fast forward
$ git merge --no-ff -m "merge with no-ff" dev # 因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去
-
合并后,用
git log
看看分支历史$ git log --graph --pretty=oneline --abbrev-commit * # 可以看到,不使用Fast forward模式
分支策略
按照基本原则进行分支管理
master
分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活- 干活都在
dev
分支上,也就是说,dev
分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev
分支合并到master
上,并在master
分支发布1.0版本
你和你的小伙伴们每个人都在dev
分支上干活(即在dev
分支上继续分支),每个人都有自己的分支,时不时地往dev
分支上合并就可以了
master
和 dev
两个常用分支外,我们还会有其他类型的分支使用策略:
- bug 分支
- feature 分支
bug分支
在开发过程中,bug 就像家常便饭一样。有了 bug 就需要修复,在 Git 中,由于分支是如此的强大,所以,每个 bug 都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
例
当你接到一个修复一个代号101的 bug 的任务时,很自然地,你想创建一个分支 issue-101
来修复它,但是,等等,当前正在dev
上进行的工作还没有提交。并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该 bug,怎么办?
git stash
Git还提供了一个stash
功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作
$ git stash
现在,用git status
查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复 bug。
-
确定要在哪个分支上修复 bug,假定需要在
master
分支上修复,就从master
创建临时分支$ git checkout master $ git checkout -b issue-101
-
现在修复bug,将你的bug进行修改后,然后提交
$ git add readme.txt $ git commit -m "fix bug 101"
-
修复完成后,切换到
master
分支,并完成合并,最后删除issue-101
分支$ git switch master $ git merge --no-ff -m "merged bug fix 101" issue-101
-
回到
dev
分支干活$ git switch dev $ git status # 工作区是干净的,刚才的工作现场存到哪去了
-
查看刚才的工作场
$ git stash list
-
工作现场还在,Git 把
stash
内容存在某个地方了,但是需要恢复一下,有两个办法-
用
git stash apply
恢复,但是恢复后,stash
内容并不删除,你需要用git stash drop
来删除 -
用
git stash pop
,恢复的同时把stash
内容也删了$ git stash pop
-
-
再用
git stash list
查看,就看不到任何stash
内容了$ git stash list
-
可以多次
stash
,恢复的时候,先用git stash list
查看,然后恢复指定的stash
,用命令$ git stash apply stash@{0}
在master
分支上修复了bug
后,我们要想一想,dev
分支是早期从master
分支分出来的,所以,这个bug
其实在当前dev
分支上也存在。
那怎么在dev
分支上修复同样的bug
?重复操作一次,提交不就行了?在 Git 中还有比这更简单的方法可以实现。
同样的 bug,要在dev
上修复,我们只需要把8842ff5 fix bug 101
这个提交所做的修改“复制”到dev
分支。注意:我们只想复制8842ff5 fix bug 101
这个提交所做的修改,并不是把整个master
分支merge
过来。
git cherry-pick
为了方便操作,Git 专门提供了一个cherry-pick
命令,让我们能复制一个特定的提交到当前分支
$ git branch
$ git cherry-pick 8842ff5
Git 自动给dev
分支做了一次提交,注意这次提交的commit
是0944c8c
,它并不同于master
的8842ff5
,因为这两个commit
只是改动相同,但确实是两个不同的commit
。用git cherry-pick
,我们就不需要在dev
分支上手动再把修 bug 的过程重复一遍。
既然可以在master
分支上修复bug
后,在dev
分支上可以“重放”这个修复过程,那么直接在dev
分支上修复 bug,然后在master
分支上“重放”行不行?当然可以,不过你仍然需要git stash
命令保存现场,才能从dev
分支切换到master
分支。
feature分支
新的功能不断添加进来
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature
分支,在上面开发,完成后,合并,最后,删除该feature
分支。
例
接到了一个新任务:开发代号为Vulcan
的新功能。
-
准备开发
$ git switch -c feature-vulcan
-
开发完后
$ git add vulcan.md $ git status $ git commit -m "add feature vulcan"
-
切回
dev
,准备合并$ git switch dev # feature分支和bug分支是类似的,合并,然后删除
-
在回到分支后,开发的新功能需取消,进行销毁分支
$ git branch -d feature-vulcan # 会报错,是因为feature-vulcan分支没有被合并,如果删除,将丢失修改,如果强行删除,使用-D参数 $ git branch -D feature-vulcan
Git解决冲突
-
准备新的
feature1
分支,继续新分支开发$ git switch -c feature1
修改
readme.txt
最后一行,改为:Creating a new branch is quick AND simple.
-
在
feature1
分支上提交$ git add readme.txt $ git commit -m "AND simple"
-
切换到
master
分支$ git switch master
在master分支上把
readme.txt
文件的最后一行改为:Creating a new branch is quick & simple.
-
在
master
分支上提交$ git add readme.txt $ git commit -m "& simple"
现在,master
分支和feature1
分支各自都分别有新的提交
这种情况下,Git 无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突
$ git merge feature1
Git发出警告,readme.txt
文件存在冲突,必须手动解决冲突后再提交,git status
可以列出冲突文件
$ git status
查看readme.txt
的内容,Git 用<<<<<<<,=======,>>>>>>>
标记出不同分支的内容,我们修改如下后保存Creating a new branch is quick and simple.
再次提交
$ git add readme.txt
$ git commit -m "conflict fixed"
用带参数的git log也可以看到分支的合并情况
$ git log --graph --pretty=oneline --abbrev-commit *
删除feature1
分支:
$ git branch -d feature1
git cherry-pick
上面在进行bug
分支时,曾用到了git cherry-pick
。
对于多分支的代码库,将代码从一个分支转移到另一个分支是常见需求。
这时分两种情况。
- 需要另一个分支的所有代码变动,那么就采用合并
git merge
。 - 需要部分代码变动(某几个提交),这时可以采用
Cherry pick
。
用法
git cherry-pick
命令的作用,就是将指定的提交commit
应用于其他分支。
$ git cherry-pick <commitHash>
# 会将指定的提交`commitHash`,应用于当前分支。这会在当前分支产生一个新的提交,当然它们的哈希值会不一样
配置项
git cherry-pick
命令的常用配置项如下。
-e,--edit
: 打开外部编辑器,编辑提交信息。-n,--no-commit
: 只更新工作区和暂存区,不产生新的提交。-x
: 在提交信息的末尾追加一行cherry picked from commit ...
,方便以后查到这个提交是如何产生的。-s,--signoff
: 在提交信息的末尾追加一行操作者的签名,表示是谁进行了这个操作。-m parent-number,--mainline parent-number
: 如果原始提交是一个合并节点,来自于两个分支的合并,那么Cherry pick
默认将失败,因为它不知道应该采用哪个分支的代码变动。-m
:配置项告诉 Git,应该采用哪个分支的变动。它的参数parent-number
是一个从1开始的整数,代表原始提交的父分支编号。git cherry-pick -m 1 <commitHash>
表示,Cherry pick
采用提交commitHash
来自编号1的父分支的变动。一般来说,1号父分支是接受变动的分支,2号父分支是作为变动来源的分支。
例
代码仓库有master
和feature
两个分支
Master: a - b - c -d
Feature: e - f - g
a,b,c,d,e,f,g是分支上提交的
-
将提交
f
应用到master
分支$ git checkout master $ git cherry-pick f
-
代码库变为了
Master: a - b - c - d - f
Feature: e - f - g
master
分支的末尾增加了一个提交f
git cherry-pick
命令的参数,不一定是提交的哈希值,分支名也是可以的,表示转移该分支的最新提交.
一个提交
将feature
分支的最近一次提交,转移到当前分支
$ git cherry-pick feature
多个提交
$ git cherry-pick <HashA> <HashB>
# 将 `A` 和 `B` 两个提交应用到当前分支。这会在当前分支生成两个对应的新提交
转移**一系列
**的连续提交,可以使用下面的简便语法
$ git cherry-pick A..B
# 转移从 A 到 B 的所有提交。它们必须按照正确的顺序放置:提交 A 必须早于提交 B,否则命令将失败,但不会报错。
# 使用上面的命令,提交 A 将不会包含在 Cherry pick 中。如果要包含提交 A,可以使用下面的语法
$ git cherry-pick A^..B
代码冲突
如果操作过程中发生代码冲突,Cherry pick
会停下来,让用户决定如何继续操作。
-
–continue
用户解决代码冲突后,第一步将修改的文件重新加入暂存区(git add .
),第二步使用下面的命令,让Cherry pick
过程继续执行。$ git cherry-pick --continue
-
–abort
发生代码冲突后,放弃合并,回到操作前的样子 -
–quit
发生代码冲突后,退出Cherry pick
,但是不回到操作前的样子。
转移到另一个代码库
Cherry pick
也支持转移另一个代码库的提交,方法是先将该库加为远程仓库。
-
添加了一个远程仓库
target
。$ git remote add target git://gitUrl
-
将远程代码抓取到本地。
$ git fetch target
-
检查一下要从远程仓库转移的提交,获取它的哈希值。
$ git log target/master
-
使用
git cherry-pick
命令转移提交。$ git cherry-pick <commitHash>
多人协作
远程分支
当你从远程仓库克隆时,实际上 Git 自动把本地的master
分支和远程的master
分支对应起来了,并且,远程仓库的默认名称是origin
。
-
查看远程库的信息,用
git remote
$ git remote origin
# 用git remote -v显示更详细的信息 $ git remote -v origin # 显示了可以抓取和推送的origin的地址。如果没有推送权限,就看不到push的地址。
推送分支
推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git 就会把该分支推送到远程库对应的远程分支上
$ git push origin master
# 要推送其他分支,比如dev,就改成
$ git push origin dev
并不是一定要把本地分支往远程推送,根据分支需求来决定是否需要推送到远程
master
分支是主分支,因此要时刻与远程同步;dev
分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;bug
分支只用于在本地修复bug
,就没必要推到远程了;feature
分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
抓取分支
多人协作时,大家都会往master
和dev
分支上推送各自的修改。
例
-
当从远程库
clone
时,默认情况下,只能看到本地的master
分支。可以用git branch
命令看看$ git branch
-
要在
dev
分支上开发,就必须创建远程origin
的dev
分支到本地,创建本地dev
分支$ git checkout -b dev origin/dev
-
在
dev
上继续修改,然后,时不时地把dev
分支push
到远程$ git add env.txt $ git commit -m "add env" $ git push origin dev To <仓库地址>
-
上面是一个人向,
origin/dev
分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送$ git add env.txt $ git commit -m "Update env.txt" $ git push origin dev
推送失败,因为这个人的最新提交和你试图推送的提交有冲突
-
先用
git pull
把最新的提交从origin/dev
抓下来,然后,在本地合并,解决冲突,再推送$ git pull
git pull
也失败了,原因是没有指定本地dev
分支与远程origin/dev
分支的链接,根据提示,设置dev
和origin/dev
的链接$ git branch --set-upstream-to=origin/dev dev
再
pull
:$ git pull
git pull
成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再push
$ git commit -m "fix env conflict" $ git push origin dev
工作模式
多人协作的工作模式:
- 试图用
git push origin <branch-name>
推送自己的修改; - 如果推送失败,则因为远程分支比你的本地更新,需要先用
git pull
试图合并; - 如果合并有冲突,则解决冲突,并在本地提交;
- 没有冲突或者解决掉冲突后,再用
git push origin <branch-name>
推送就能成功!
如果git pull
提示no tracking information
,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to <branch-name> origin/<branch-name>
git rebase
多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后push的童鞋不得不先pull,在本地合并,然后才能push成功。
每次合并再push后,查看分支情况
$ git log --graph --pretty=oneline --abbrev-commit
会发现,分支看上去很乱。
针对上述问题,Git有一种称为**rebase
**的操作,有人把它翻译成“变基”。
-
假设,在和远程分支同步后,我们对
hello.py
这个文件做了两次提交。用git log命令看看$ git log --graph --pretty=oneline --abbrev-commit *
注意到Git用(HEAD -> master)和(origin/master)标识出当前分支的HEAD和远程origin的位置分别是
582d922 add author
和d1be385 init hello
,本地分支比远程分支快两个提交。 -
尝试推送本地分支
$ git push origin master To <仓库地址>
推送失败,说明有人先于你推送了远程分支,按照经验,先
pull
:$ git pull
再用git status看看状态
$ git status
加上刚才合并的提交,现在我们本地分支比远程分支超前3个提交
用
git log
看看$ git log --graph --pretty=oneline --abbrev-commit *
提交历史分叉,现在把本地分支
pull
到远程,会显的分支很乱 -
git rebase
$ git rebase
用
git log
看看$ git log --graph --pretty=oneline --abbrev-commit *
push
把本地分支推送到远程$ git push origin master
git log
查看$ git log --graph --pretty=oneline --abbrev-commit *
原本分叉的提交现在变成一条直线了!这种神奇的操作是怎么实现的?
我们注意观察,发现Git把我们本地的提交“挪动”了位置,放到了
f005ed4 (origin/master) set exit=1
之后,这样,整个提交历史就成了一条直线。rebase
操作前后,最终的提交内容是一致的,但是,我们本地的commit
修改内容已经变化了,它们的修改不再基于d1be385 init hello
,而是基于f005ed4 (origin/master) set exit=1
,但最后的提交7e61ed4
内容是一致的。这就是
rebase
操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。
-
rebase
操作可以把本地未push的分叉提交历史整理成直线 -
rebase
的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比
Git标签
发布一个版本时,我们通常先在版本库中打一个标签(tag
),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。
Git 的标签虽然是版本库的快照,但其实它就是指向某个commit
的指针(分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。
tag
就是一个让人容易记住的有意义的名字,它跟某个commit
绑在一起
创建标签
-
切换到需要打标签的分支上
$ git branch $ git checkout master
-
git tag <name>
就可以打一个新标签$ git tag v1.0
-
默认标签是打在最新提交的
commit
上的。有时候,如果忘了打标签方法是找到历史提交的commit id
,然后打上就可以$ git log --pretty=oneline --abbrev-commit
找到对应的
commit id
,敲入命令:$ git tag v0.9 <commit id>
-
查看标签
$ git tag
标签不是按时间顺序列出,而是按字母排序的。可以用
git show
查看标签信息$ git show # 查看具体某个标签信息 $ gitshow <tagname>
-
创建带有说明的标签,用
-a
指定标签名,-m
指定说明文字$ git tag -a v0.1 -m "version 0.1 released"
注意:标签总是和某个
commit
挂钩。如果这个commit
既出现在master
分支,又出现在dev
分支,那么在这两个分支上都可以看到这个标签。
标签操作
-
如果标签打错了,也可以删除
$ git tag -d v0.1
因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
-
推送某个标签到远程,使用命令
git push origin <tagname>
$ git push origin v1.0
一次性推送全部尚未推送到远程的本地标签
$ git push origin --tags
-
如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除
$ git tag -d <tagname>
然后,从远程删除。删除命令也是
push
,但是格式如下$ git push origin :refs/tags/<tagname>
-
命令
git tag -a <tagname> -m 'messages'
可以创建一个带附注的标签 -
命令
git tag -s <tagname> -m 'messages'
可以创建一个带gpg
签名的标签
总结
在日常工作中git少不了,所以掌握这门核心技术,是非常有必要的。
虽说,大模型(LLMs)不断飞速发展,他们展现的基础能力,能超过基础人员的认知,但是在逻辑、思维能力上LLms是达不到,因此,开发人员,更应该扎实基础,在此基础上,不断提高个人的认知以及构思能力。近期,在不断的重温基础知识,厚积才能薄发。
以上是我个人在学习过程中的记录所学,希望对正在一起学习的小伙伴有所帮助!!!
如果对你有帮助,希望你能一键三连【关注、点赞、收藏】!!!
参考链接
- https://www.git-tower.com/learn/?utm_source=Tower+Blog&utm_medium=cheat+sheet+pdf&utm_content=german+version&utm_campaign=Tower+website
- https://rogerdudler.github.io/git-guide/
- https://www.git-scm.com/