Git从原理到实践

背景

如果有一个软件,不但能自动帮我记录每次文件的改动,还可以让同事协作编辑,这样就不用自己管理一堆类似的文件了,也不需要把文件传来传去。如果想查看某次改动,只需要在软件里瞄一眼就可以,岂不是很方便?

这个软件用起来就应该像这个样子,能记录每次文件的改动:

版本 用户 说明 日期
1 张三 删除了软件服务条款5 7/12 10:38
2 张三 增加了License人数限制 7/12 18:09
3 李四 财务部门调整了合同金额 7/13 9:51
4 张三 延长了免费升级周期 7/14 15:17

那么它就是Git

到了 2002 年,Linux 系统已经发展了十年了,代码库之大让 Linus 很难继续通过手工方式管理了,社区的弟兄们也对这种方式表达了强烈不满,于是 Linus 选择了一个商业的版本控制系统 BitKeeper,BitKeeper 的东家 BitMover 公司出于人道主义精神,授权 Linux 社区免费使用这个版本控制系统。

安定团结的大好局面在 2005 年就被打破了,原因是 Linux 社区牛人聚集,不免沾染了一些梁山好汉的江湖习气。开发 Samba 的 Andrew 试图破解 BitKeeper 的协议(这么干的其实也不只他一个),被 BitMover 公司发现了(监控工作做得不错!),于是 BitMover 公司怒了,要收回 Linux 社区的免费使用权。

Linus 可以向 BitMover 公司道个歉,保证以后严格管教弟兄们,嗯,这是不可能的。实际情况是这样的:

Linus 花了两周时间自己用 C 写了一个分布式版本控制系统,这就是 Git!一个月之内,Linux 系统的源码已经由 Git 管理了!牛是怎么定义的呢?大家可以体会一下。

常用命令

git init 初始化仓库

git add <file> 添加文件到缓冲区,git add .添加所有文件到缓冲区(Stage)

git commit 提交代码到本地版本库,此时会生成版本的提交hash

image-20240602080854175

git status 查看状态,可以查看当前所处的分支,当前的修改内容和提交到版本库里的内容

image-20240602081304965

git diff 可以让你看到你更改的内容

image-20240602091847345

git log 查看commit的版本,参数--pretty=oneline会只显示版本和commit

image-20240602091859951

git rest --hard hash or head^会进行版本号的回退可以选用版本的hash值或者HEAD^代表回退上一版本,

git reflog 记录你的版本操作,

git checkout -- file 可以丢弃工作区的修改。把 readme.txt 文件在工作区的修改全部撤销,这里有两种情况:

  • 一种是 readme.txt 自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
  • 一种是 readme.txt 已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。

git checkout file不加横杠就是创建分支

git reset HEAD file 撤销存储在缓冲区里的内容,HEAD 代表最新版本,也可以使用hash。 unstage是撤销缓存区中的内容

image-20240602093448608

将你本地删除的文件在git上也删除,然后再提交,现在,文件就从版本库中被删除了。

1
2
git rm test.txt
git commit -m "remove test.txt"

git checkout -- test.txt 是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。

远程库需要把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到 GitHub 仓库。

1
git remote add origin git@github.com:michaelliao/learngit.git

git push -u origin main把本地库的所有内容推送到远程库上,-u参数,Git 不但会把本地的 master 分支内容推送的远程新的 master 分支,还会把本地的 master 分支和远程的 master 分支关联起来

创建分支就是创建了一个指针,他可以独立于master进行提交和更改

img

1
2
3
4
git checkout -b dev #创建并切换分支
#等于下边两条指令
git branch dev #创建分支
git checkout dev #切换分支

git merge dev 可以将你对分支的提交的修改提交到master上,这里的dev是把dev合并到当前分支,当你同时修改一个文件时才会冲突,修改两个文件不会冲突。然后再提交到缓冲区,在提交版本

image-20240602100519014

git branch -d dev 删除分支

git branch 查看分支

当出现如下修改时不能自动的合并分支

img

image-20240602102003297

需要我们去手动解决冲突后再提交。git status 也可以告诉我们冲突的文件:

可以看到合并的过程

1
git log --graph --pretty=oneline --abbrev-commit

image-20240602142812506

git merge --no-ff -m "merge with no-ff" dev会以提交的方式合并分支

git stash 可以保存缓冲区的内容而不用提交

git stash list 可以查看工作现场的列表

git stash apply 恢复保存起来的现场

git stash drop 删除保存起来的现场

git stash apply stash@{0} 恢复指定现场

git branch -D feature-vulcan 强制删除分支

远端合并解决方案

分支策略

在实际开发中,我们应该按照几个基本原则进行分支管理:

首先,master 分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;

那在哪干活呢?干活都在 dev 分支上,也就是说,dev 分支是不稳定的,到某个时候,比如 1.0 版本发布时,再把 dev 分支合并到 master 上,在 master 分支发布 1.0 版本;

你和你的小伙伴们每个人都在 dev 分支上干活,每个人都有自己的分支,时不时地往 dev 分支上合并就可以了。

所以,团队合作的分支看起来就像这样:

image-20240602144803817

如果版本库领先自己一个版本应该如何操作和更改

应该git rebase

如何pull和push

git和pull 和是在和远程库操作,只是在拉取相应的分支并合并,与其他分支之间没有关系

git rebase 原理

chatgpt

git rebase 是一个强大的 Git 命令,用于将一个分支的提交历史重新应用到另一个分支之上。

基本工作流程

假设我们有两个分支,mainfeature,并且 feature 分支是从 main 分支分出来的,现在我们想要把 feature 分支的更改重新基于 main 最新的提交之上。我们可以使用 git rebase main 来实现这个目标。

内部指令解析

假设初始状态:

  • main 分支:A - B - C
  • feature 分支:A - B - C - D - E

执行 git rebase main

  1. 确定共同祖先提交:

    1
    git merge-base main feature

    假设共同祖先提交是 A

  2. 创建补丁并保存到临时文件:

    1
    git format-patch A..feature -o /tmp/patches

    这会在临时目录中创建从 Afeature 的补丁文件。

  3. 切换到main分支并重置feature分支:

    1
    2
    git checkout feature
    git reset --hard main

    现在 feature 分支的 HEAD 和 main 分支的 HEAD 是一致的。

  4. 应用补丁:

    1
    git am /tmp/patches/*.patch

    这一步会逐个应用补丁,并创建新的提交。应用过程中,如果有冲突,Git 会暂停并允许用户手动解决冲突。

其他常见操作

处理冲突

如果在应用补丁的过程中遇到冲突,Git 会暂停并提示用户解决冲突。用户解决冲突后,需要执行以下命令继续:

1
2
git add <resolved-files>
git rebase --continue

如果用户希望放弃变基,可以执行:

1
git rebase --abort

git rebase 和git pull 的替换

可以使用 git pullgit pushgit merge 来实现类似 git rebase 的效果。虽然这并不是完全相同的操作,但通过一些步骤,你可以手动完成类似的变基过程。

以下是一个示例,展示如何使用这些命令来实现 git rebase 的效果:

场景设置

假设有两个分支:

  • main 分支
  • feature 分支

我们希望将 feature 分支的更改重新基于 main 分支的最新提交之上。

步骤

1. 更新 main 分支

首先,确保 main 分支是最新的:

1
2
git checkout main
git pull origin main

这将把 main 分支拉到最新状态。

2. 更新 feature 分支

接下来,切换到 feature 分支并拉取最新的更改:

1
2
git checkout feature
git pull origin feature

这将确保 feature 分支也是最新的。

3. 创建临时分支

创建一个临时分支来保存 feature 分支的更改:

1
git checkout -b feature-temp

4. 将 feature 分支重置到 main 分支

切换回 feature 分支并将其重置到 main 分支的最新提交:

1
2
git checkout feature
git reset --hard main

现在 feature 分支的 HEAD 与 main 分支的 HEAD 相同。

5. 合并临时分支的更改

将临时分支的更改合并到 feature 分支:

1
git merge feature-temp

在这个过程中,如果有冲突,解决冲突并继续合并。

6. 推送更改

如果合并成功,将 feature 分支的更改推送到远程仓库:

1
git push origin feature --force

总结

通过这样述步骤,可以实现类似 git rebase 的效果总结步骤如下:

  1. 更新 main 分支和 feature 分支。
  2. 创建一个临时分支保存 feature 分支的更改。
  3. feature 分支重置到 main 分支。
  4. 合并临时分支的更改到 feature 分支。
  5. 推送更改到远程仓库。

hexo常用命令

https://www.jianshu.com/p/c14e70f5885d

参考

https://doc.yonyoucloud.com/doc/wiki/project/git-tutorial/index.html

单调栈

什么时候用单调栈呢?

通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为O(n)。

那么单调栈的原理是什么呢?为什么时间复杂度是O(n)就可以找到每一个元素的右边第一个比它大的元素位置呢?

单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。

更直白来说,就是用一个栈来记录我们遍历过的元素,因为我们遍历数组的时候,我们不知道之前都遍历了哪些元素,以至于遍历一个元素找不到是不是之前遍历过一个更小的,所以我们需要用一个容器(这里用单调栈)来记录我们遍历过的元素。

Read more

动态规划6

583. 两个字符串的删除操作

力扣题目链接

相比之前现在两个字符串都可以删除,

  1. 确定dp数组(dp table)以及下标的含义

dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数

这里和原来的匹配长度的dp定义不同,

Read more

动态规划5

300.最长递增子序列

力扣题目链接

本题要先有一个逻辑,就是我们怎么确定一个状态转移,如果我dp要取这个数那么之前的状态怎么找,所以很明显需要两轮遍历,

Read more

动态规划3

完全背包

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

代码上两者最大的不同就是遍历顺序

Read more

动态规划2

背包问题

01 背包

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

举一个例子:

背包最大重量为4。

物品为:

重量 价值
物品0 1 15
物品1 3 20
物品2 4 30

问背包能背的物品最大价值是多少?

Read more

动态规划1

所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的,

动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。

但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。

动态规划的解题步骤

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组
Read more

贪心算法1

贪心的本质是选择每一阶段的局部最优,从而达到全局最优

Read more