Git从底层到命令的综合【实践】

前言

学习Git,从底层开始了解git的命令语句所执行的操作。看完本文,希望你能够了解Git,“实践是检验真理的唯一标准”,掌握Git,需要你在日常工作中,不断深化其命令以及底层逻辑。

版本控制系统

版本控制系统是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。版本控制系统不仅可以应用于软件源代码的文本文件,而且可以对任何类型的文件进行版本控制。

版本控制系统发展可以分为三个阶段:

  • 本地版本控制系统
  • 集中式版本控制系统
  • 分布式版本控制系统

本地版本控制系统

RCS,现今许多计算机系统上都还看得到它的踪影。 RCS 的工作原理是在硬盘上保存补丁集(补丁是指文件修订前后的变化);通过应用所有的补丁,可以重新计算出各个版本的文件内容。

集中版本控制系统

集中化的版本控制系统(Centralized Version Control Systems,简称 CVCS)应运而生。 这类系统,诸如 CVSSubversion 以及 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作了什么修改后,再把它提交到仓库就放心多了,提交修改和提交新文件是一样的两步

  1. git add,如:git add readme.txt
    • 在执行第二步之前,再次查看仓库转态git statusgit status告诉我们,将要被提交的修改包括readme.txt,下一步,就可以放心地提交了
  2. 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 distributedcommit id

git reflog

$ git reflog

从上面的输出可以看到,add distributedcommit ide55063a,现在,我们就可以通过 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严格来说不是指向提交,而是指向mastermaster才是指向提交的,所以,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分支

  1. 创建dev分支,切换到dev分支:
$ git checkout -b dev

# git checkout命令加上-b参数表示创建并切换,相当于以下两条命令
$ git branch dev 
$ git checkout dev
  1. 查看当前分支(列出所有分支)
$ git branch
  1. dev分支上正常提交,比如对readme.txt做个修改,加上一行:Creating a new branch is quick.,然后提交
$ git add readme.txt
$ git commit -m "branch test"
  1. dev分支的工作完成,我们就可以切换回master分支
$ git checkout master
  1. 切换回master分支后,再查看一个readme.txt文件,刚才添加的内容不见了!因为那个提交是在dev分支上,而master分支此刻的提交点并没有变

  2. dev分支的工作成果合并到master分支上

    $ git merge dev
    

    git merge命令用于合并指定分支到当前分支。合并后,再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的。

    注意到上面的Fast-forward信息,Git 告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。

  3. 合并完成后,就可以放心地删除dev分支了

    $ git branch -d dev
    
  4. 删除后,查看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,这样,从分支历史上就可以看出分支信息

  1. 创建并切换dev分支

    $ git switch -c dev
    
  2. 修改readme.txt文件,并提交一个新的commit

    $ git add readme.txt
    $ git commit -m "add merge"
    
  3. 切换回master

    $ git switch master
    
  4. 合并dev分支,请注意--no-ff参数,表示禁用Fast forward

    $ git merge --no-ff -m "merge with no-ff" dev
    # 因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去
    
  5. 合并后,用git log看看分支历史

     $ git log --graph --pretty=oneline --abbrev-commit *
     
     # 可以看到,不使用Fast forward模式
    

分支策略

按照基本原则进行分支管理

  • master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活
  • 干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,并在master分支发布1.0版本
    你和你的小伙伴们每个人都在dev分支上干活(即在dev分支上继续分支),每个人都有自己的分支,时不时地往dev分支上合并就可以了

masterdev 两个常用分支外,我们还会有其他类型的分支使用策略:

  • 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分支做了一次提交,注意这次提交的commit0944c8c,它并不同于master8842ff5,因为这两个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号父分支是作为变动来源的分支。

代码仓库有masterfeature两个分支

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分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
抓取分支

多人协作时,大家都会往masterdev分支上推送各自的修改。

  • 当从远程库clone时,默认情况下,只能看到本地的master分支。可以用git branch命令看看

    $ git branch
    
  • 要在dev分支上开发,就必须创建远程origindev分支到本地,创建本地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分支的链接,根据提示,设置devorigin/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
    
工作模式

多人协作的工作模式:

  1. 试图用git push origin <branch-name>推送自己的修改;
  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
  3. 如果合并有冲突,则解决冲突,并在本地提交;
  4. 没有冲突或者解决掉冲突后,再用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 authord1be385 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/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

故事挺秃然

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值