1. Concept
HEAD:HEAD是一个指向你正在工作中的本地分支的指针。简单来讲,就是你现在在哪儿,HEAD就指向哪儿。例如当前我们处于master分支,所以HEAD这个指针指向了master分支。
working tree:实际操作的目录被称为工作树,也就是工作区域。
Index:索引是工作树和仓库之间的一个暂存区域。在这个区域放置了你想要提交给仓库的文件,如工作树的代码通过右键TortoiseGit → add添加到索引中,通过commit 则将索引区域的文件提交到本地仓库中。
2. Submodule
为了降低代码维护成本,在YS8803项目开发中,希望将FEP / BEP / MEP / LLF 等工程共用同一份SDK代码,于是将原本存放在各个project下的SDK文件夹提取出来,单独建立一个SDK仓库,FEP / BEP / MEP / LLF 等工程包含并使用该SDK仓库。在此将FW_TLC称为主项目/主仓库,将SDK称为子项目/子仓库/子模块。
2.1. 将SDK提升为项目
以下内容是将旧的代码仓库结构调整为SDK独立仓库,并通过submodule方式将子项目链接到主项目的操作步骤,开发人员在日常开发中无需关注。
完成上述步骤后,本地FW_TLC文件夹下就只剩下SDK文件夹,然后再在该目录下将文件推送到服务器端的SDK仓库,就可以将原本在FW_TLC文件夹下的SDK文件夹推送到外层的SDK仓库,并且能保留之前的所有log。
2.2. 将SDK链接到主项目
(1)选择子仓库链接方式
SSH:授权访问,无需登录git账号及密码,需要向 Git 服务器提供 SSH 公钥。详见2.4节。
HTTPS:无授权访问,需要登录git账号及密码。
(2)将SDK仓库链接到FW_TLC仓库
FW_TLC文件夹内右键TortoiseGit -> Submodule Add,添加子仓库path。之后会在一级目录生成一个.gitmodules文件,该配置文件保存了项目 URL 与已经拉取的本地目录之间的映射。
2.3. 克隆含有子模块的项目
当你在克隆一个含有子模块的项目时,默认会包含该子模块目录,但其中还没有任何文件,需在主项目文件夹下右键TortoiseGit -> Submodule Update。
switch branch时需要在switch完成界面点击弹框右下角的 "Update Submodules"。
2.4. gitlab远程子仓库配置为SSH
在gitlab main path下原本是链接了一个http的子仓库SDK,但是由于要使用持续集成工具Jenkins检查merge的代码是否编译通过、是否单元测试通过等,因此需要将子仓库SDK的链接引用改为SSH的。改链接引用很容易,将git main path下的.gitmodules文件里的url改为SSH的就好。
但是遇到了一个问题:重新在本地Git Clone main path,右键点击Submodules update,提示要输入gitlab的密码,输入密码后还是弹出输入密码的框。此时需要配置下子仓库,
2.4.1. Flow
1. win+r输入cmd打开cmd运行窗口输入,git config --global user.name "你的gitlab用户名",回车,输入git config --global user.email "你的gitlab邮箱",回车;
gitlab用户名和gitlab邮箱可以通过右键TortoiseGit -> Setting -> Git看到;
2. 输入git config --global --list,回车,这一步知识看下前面的步骤有没有成功;
3. 输入ssh-keygen,连续3次回车,成功之后id_rsa,id_rsa.pub两个文件默认在C:\Users\username/.ssh目录下,cmd运行窗口也显示了文件路径。
ps:如果输入命令ssh-keygen回车显示“ssh-keygen 不是内部或外部命令,也不是可运行的程序xxx”,那么在本地main code文件夹下右键选择Git Bash Here,输入ssh-keygen就可以了,盲猜可能是之前环境变量没配好。
4. 打开id_rsa.pub,复制ssh key
法一:直接用UltraEdit打开id_rsa.pub,复制所有内容;
法二:在本地main code文件夹下右键选择Git Bash Here,输入cat ~/.ssh/id_rsa.pub,回车,
复制以ssh-rsa开头的这段ssh key;
5. 将ssh key填写到gitlab SSH Keys上。 如果之前已经为本台电脑设置过ssh key,会显示“Fingerprint has already been taken”,跳过此步骤就好。
6. 搜索栏搜putty,打开PuTTYgen(一般是TortoiseGit软件包自带的,如果搜索不到,可以安装软件包📎putty-0.70cn.7z,或者用windows11内置的OpenSSH客户端,其中包含了ssh-keygen工具) -> 点击load,选择.ssh/id_rsa -> 点击Save private key,保存ppk文件到.ssh目录下;
7. 本地新建文件夹load ssh path,输入gitlab的个人密码,成功clone sdk到本地;
8. clone main path到本地
(1)右键单击TortoiseGit → Git Clone,clone main path(http);
(2)将生成的.ppk加到Git putty key;
(3)在本地main仓库下右键pull;
(4)右键submodules update将sdk子仓库(ssh)拉取到本地。
2.4.2. Q&A
1. clone代码报错:error: cannot spawn C:\ProgramData\Microsoft\Windows\Start Menu\Programs\TortoiseGit: Permission denied
原因:之前安装过TortoiseGit,但是残留文件没有清除
解决方法:打开C:\ProgramData\Microsoft\Windows\Start Menu\Programs\TortoiseGit,找到其快捷键,打开文件所在位置,复制其路径,粘贴到下图所示的位置应用即可。
2. 配置了SSH仍要输入密钥
这种情况下,再应用一次ppk即可。
3. Push
这里主要介绍下强推,也就是使用本地分支的提交覆盖远端推送分支的提交。
因为版本不同,push的时候会弹出如下两个界面中的一个,图一勾选known changes 或者 unknown changes即为强推。图二勾选force 或者 force with lease即为强推。
图一选项known changes 等同于图二选项force with lease,图一选项unknown changes 等同于图二选项force。known changes / force with lease 比 unknown changes / force 更安全。因为,如果远端有其他人推送了新的提交,此时勾选了known changes / force with lease,那么推送将被拒绝。也就是说known changes / force with lease解决了本地仓库不够新时,依然覆盖了远端新仓库的问题。
如果你执意想要覆盖远端提交,只需要先 fetch 再push with known changes / force with lease,它也不会拒绝的。或者直接选择unknown changes / force。
4. Revert
在TortoiseGit中,"Revert"文件 相当于git checkout HEAD--文件名(或git checkout-REVISION--文件名),用于将文件重置为其最后(或特定)提交状态。而git的revert仅由日志提交对话框中的revert更改引用。
4.1. Undoing Changes which have been committed
如果您想撤消自上次commit后在文件中所做的更改,右键TortoiseGit → revert → 弹出的对话框内勾选要还原的文件 → 单击“确定”。
4.2. Reverting a whole commit
右键TortoiseGit → Show log → 选中要还原的提交,鼠标右击 → Revert change by this commit,此提交的所有更改都将被撤消,并创建一个需要手动提交的恢复提交。提交后会生成一个revert的log。
5. Reset
常见使用场景:
- 回退节点;
- 修改历史:将最近的几个commit合并为一个。
- 将最新的1个或几个commit的修改提交到另一个分支
5.1. 回退节点
Flow:右键TortoiseGit -> Show log -> 选中要回退的节点 -> 右键点击Reset "xxx" -> Reset Type选择Hard模式 -> 强推到远端仓库。
- Mixed:回退到指定节点,本地保留更改的文件,并报告未更新的内容。TortoiseGit中,Soft 模式 和 Mixed 基本无区别。
- Hard:同Abort Merge,重置工作树和索引(放弃所有本地更改),对工作树中跟踪文件的任何更改都将被丢弃。与TortoiseGit的revert或clean功能不同,Hard不使用Windows回收站,直接永久删除这些文件,未提交的更改可能会丢失!
上述Reset操作只能将本地仓库回退到之前的节点,并不能将远端节点回退。如果要将远端节点回退,在上述操作的基础上还需要强推,即在push时勾选known changes / unknown changes 或者 force / force with lease即可,区别详见 3. Push。
5.2. 将最新的1个或几个commit合并为一个
Flow:右键TortoiseGit -> Show log -> 选中要回退的节点 -> 右键点击Reset "xxx" -> Reset Type选择Mixed模式 -> commit 本地文件-> 强推(详见 3. Push)到远端仓库。
5.3. 将最新的1个或几个commit的修改提交到另一个分支
Flow:右键TortoiseGit -> Show log -> 选中要回退的节点 -> 右键点击Reset "xxx" -> Reset Type选择Mixed模式 -> Stash 本地文件 -> Switch想要提交的分支 -> Pop 文件 -> Commit & Push。
注:developer只能回退远端分支,不能回退远端main;
6. Revert VS Reset
TortoiseGit revert aimed to revert effects of previous commit. For example,
A <- B <- C
^ HEAD
If I found B I committed before is wrong, and I want to "undo" its change, revert B will cause:
A <- B <- C <- B'
^ HEAD
其中B' 与B中所做的改变相反。
Reset is more straight-forward, it is simply setting the HEAD to a certain commit,
A <- B <- C
^ HEAD
git-reset-ting to B will give you
A <- B <- C
^ HEAD
7. rebase
Rebase(变基)会改变/重写存储库的历史。
假设存在以下历史记录,且当前分支为"feature1"
A---B---C feature1
/
D---E---F---G main
执行 git rebase main,结果如下,feature1分支变基到main最新节点
A'--B'--C' feature1
/
D---E---F---G main
如果main分支包含所做的更改,那么该提交将被跳过,如下,
A---B---C feature1
/
D---E---A---F main
执行 git rebase main,结果如下
B'---C' feature1
/
D---E---A---F main
Squash:fold two or more commits into one,该提交与位于列表下方(ID较低)的前一个提交合并。
Skip:跳过该条记录
Edit:you can tell git rebase to stop after applying that commit, so that you can edit the files and/or the commit message, amend the commit, and continue rebasing
Force rebase:逐个重新播放所有变基的提交,这确保了变基分支的整个历史由新的提交组成。
Preserve merges:进行 rebase 操作时,默认情况下,它会将合并提交从待办任务列表中删除,并将变基后的提交放入一个单一的线性分支中。这样可以简化提交历史,使其更加线性和清晰。如果你勾选了 "Preserve merges" 选项,使用 rebase 进行变基时将尝试保留原始分支的合并结构。也就是说,它会尝试重新创建合并提交,以保持原有的分支关系。这对于那些希望保留更多分支信息和合并历史的情况非常有用。需要注意的是,保留合并提交可能会导致更复杂的提交历史,并且在解决合并冲突或手动修改时需要谨慎操作。因此,在使用 "Preserve merges" 选项时,请确保理解其影响,并根据具体情况进行决策。
常用使用场景:
1. 保持代码库整洁:通过使用 rebase 可以将开发分支中的多个小提交合并为更少、更有意义的提交,从而使提交历史更加清晰和易于理解。
eg:在Feature_Amber_For_Rebase分支下右键TortoiseGit -> show log -> 选中基础节点(要更改的节点的前一个节点) 右键点击 rebase“Feature_Amber_For_Rebase”onto this, 进行如下操作。完成之后需要push(勾选unknown changes)到远端仓库。
2. 与主分支同步:当主分支(例如 master)上有新的提交时,可以使用 rebase 将当前分支与主分支同步,以便在合并更改之前将自己的提交应用到最新的主分支上
eg:在Feature_Amber_For_Rebase分支下右键TortoiseGit -> rebase -> 选择要rebase的分支 -> Start Rebase即可;rebase之后show log就可以看到Feature_Amber_For_Rebase分支变基成功了。
8. Merge(with Submodule)
由于用Git Submodule方式将SDK作为子仓库链接到FW_TLC仓库中,并且Ci Jenkins只能使用SSH Clone的方式下载代码,.gitmodules里的path由HTTP改为了SSH,所以update submodules的时候SDK子仓库不会自动更新为main最新节点,而是load 对应的SHA节点的SDK子仓库代码,因此涉及到SDK的改动代码,在merge的时候SDK的代码改变是以一个仓库形式出现,解决冲突很麻烦,因此merge的步骤发生了如下改变,稍微繁琐一些,但总体思想不变,即将其看作两个独立的仓库进行管理。
8.1. Flow
以开发一个新需求为例,从FW_TLC Main的最新节点上创建一个FW_TLC branch进行模块开发。若此模块涉及SDK的修改,则创建FW_TLC branch对应的SDK branch;当功能开发完成并通过研发内测后,将FW_TLC branch merge到Main,此时需按如下步骤进行。如果此FW_TLC branch不涉及SDK的修改,则从步骤7开始。
SDK源分支
:Feature_FixRetry_SDK
SDK目标分支
:Develop_For_WTV0.6_SDK
FW源分支
:Feature_FixRetry
FW目标分支
:Develop_For_WTV0.6
1. pull 从远程仓库拉取最新版本到本地。
2. 在本地 Feature_FixRetry_SDK下右键TortoiseGit -> Merge,选择Develop_For_WTV0.6_SDK。
3. 如果有冲突的话,解决冲突,commit & push 到远程仓库。如果没有冲突,则直接push到远程仓库。
4. 在gitlab web端创建SDK的merge request,等待直至merge通过。
5. 在本地 Feature_FixRetry_SDK目录下 pull,然后switch到 Develop_For_WTV0.6_SDK ,在本地Feature_FixRetry 下commit & push SDK的引用修改。
6. 在gitlab上创建FW的merge request,如果显示冲突,则在本地 Feature_FixRetry_SDK下右键TortoiseGit -> Merge,选择Develop_For_WTV0.6,解决冲突(详见 7.2. Edit conflict),commit & push 到远程仓库。(web界面不需要close merge reuest,本地解决冲突commit & push后会自动刷新,再次进行merge冲突检测和冒烟测试)
8.2. Edit conflict
Merged file存在以下几种情况:
1. 所有的修改 Mine 是最新的
此时选中文件右键,点击 Resolve conflict using "HEAD"即可。
2. 所有的修改 Theirs 是最新的
此时选中文件右键,点击 Resolve conflict using "MERGE_HEAD"即可
3. 一部分修改 Mine是最新的,一部分修改 Theirs是最新的。
左上窗格显示了 "MERGE_HEAD" 是您要合并的修订文件的版本。当你想合并其他更改时,这个状态/版本通常也被称为“theirs”。
右上窗格显示了 "HEAD" 是您在开始合并之前存在于工作树中的文件(即,该文件符合本地存储库HEAD的最新提交状态),这个状态/版本通常也被称为“mine”。
底部窗格 "Merged" 是您试图解决冲突的输出文件。
此时可以直接编辑底部窗格。或者使用上方工具栏的Use 'theirs' text block 、Use 'mine' text block、Use 'mine' text block then 'theirs'、Use 'theirs' text block then 'mine'。
Tips:
在解决冲突的时候无需关注三窗格视图中"="、"+"、"-",只需看每个窗格中的行号。
8.3. Q&A
- gitlab web merge request发起后,Checking pipeline status一直转圈,且下方提示git merge blocked: all merge request dependencies must be merged or close。
解决方法:close and recreate merge request。
9. Rebase VS Merge
在开发中,我们常常会遇到这样的场景,我们基于主干分支所指向的 commit C1 创建了一个特性分支,并在特性分支上新增了C2、C3、 C4 三个提交。与此同时,主干分支上又合入了其他的提交。如果这时我们要将主干分支上的新引入的这些变更也整合进特性分支,大家通常会选用merge,其实还有rebase。
merge 和 Rebase 都具备整合分支间变更的能力,但二者的实现手段却大不相同。我们在特性分支上执行 Git merge master 时, Git 会与master最新节点、feature最新节点以及双方最近公共祖先对应的快照执行三路合并生成新的快照,并基于此快照创建一个连接特性分支和主干的合并节点,最后调整特性分支的指向。
可见, Git merge 总是在向前推进,提交历史并不会影响提交的原始状态,而 Git Rebase 整合变更的方式则是对提交历史进行重写。我们在特性分支上执行 Git Rebase master 时, Git 会从双方的最近公共祖先开始,将特性分支上每个提交对应的变更暂存起来,然后以主干分支所指向的提交为新起点,将暂存的变更按照顺序一一还原成。新提交 Rebase 完成后,特性分支的起始点就像是从 C1 迁移到了C6。需要特别说明的是,通常情况下,无论是 merge 还是Rebase,特性分支最终所指向的快照会完全相同。也就是说, Git merge 通过三路合并生成的 C7 与 Git Rebase 重建出的 C4 所对应的代码内容完全一样,但二者称构建出了迥异的提交历史。
此处为语雀视频卡片,点击链接查看:git merge VS rebase.mp4
开发者发起从特性分支到主干的merge request,希望通过评审后将特性分支合入主干。但是因为主干分支也合入了新提交,在merge request中显示特性分支和主干间存在冲突而无法合并。开发者通常有两种方法解决冲突问题,一种是将主干分支 merge 到特性分支,并在合并时解决冲突。这样做,特性分支中会引入一个合并提交,包含解决冲突所做的修改。另外一种方法是将特性分支 Rebase 到主干的最新提交,并在 Rebase 的过程中解决冲突。可见,使用 merge 会在特性分支中引入新提交,增加代码评审者的负担。而使用 Rebase 这部分孰优孰劣一目了然。在复杂的多人协作开发场景下,随着项目迭代的不断推进和工程复杂度的日益提高,rebase 往往会助力生成相对清爽的提交历史,进而方便工程历史的追溯以及缺陷排查。
此处为语雀视频卡片,点击链接查看:gitlab rebase.mp4
优点 | 缺点 | |
Merge 合并 |
|
|
Rebase 变基 |
|
|
综上所述,merge 适合保留完整的提交历史和追踪分支,并且在多人协作和公共分支上更加安全。而 rebase 则适用于创建整洁、线性的提交历史以提高可读性,并减少合并冲突的数量,但需要在谨慎使用并遵循团队协作规范的前提下执行。
10. Abort Merge
Merge:重置索引并且尝试重构merge之前的状态;
Mixed:保持工作树不变,重置索引,但不重置工作树(即保留更改的文件,但不标记为提交),并报告未更新的内容。
Hard:重置工作树和索引(放弃所有本地更改),对工作树中跟踪文件的任何更改都将被丢弃。与TortoiseGit的revert或clean功能不同,Hard不使用Windows回收站,直接永久删除这些文件,未提交的更改可能会丢失!
11. Stash changes
通常,当你在做项目的一部分时,事情处于一种混乱的状态,你想切换一下分支来做其他事情。问题是,你不想只做一半的工作,然后再回到这一点。那么可以通过Stash changes来解决。
Stash changes就是将当前未commit的修改存放在堆栈,需要使用时pop出来即可。
Stash change操作会获取工作目录的脏状态,即修改后的跟踪文件和暂存的更改,并将其保存在一堆未完成的更改中,你可以随时重新应用这些更改,即使在不同的分支上。
当你想记录工作目录和索引的当前状态,但又想返回一个干净的工作目录时,右键选择命令TortoiseGit → Stash changes将弹出一个对话框,你可以在其中选择输入此状态的消息:
- include untracked:将未跟踪文件也隐藏起来。
- --all:要隐藏所有文件,包括被忽略的文件和未跟踪的文件。
如果你需要应用Stash起来的更改,右键选择命令TortoiseGit → stash list → 选中要应用的修改 → 右键选择Stash Apply
- Stash List:提供了整个Stash堆栈的概览。你还可以删除并查看隐藏在那里的更改。
- Stash Apply:会将选中的那条Stash的更改应用到当前分支,应用后选中的Stash仍然在stash list中。
- Stash Pop:会将最新的那条Stash的更改应用到当前分支,并且会将此条stash从stash list中删除。
12. Clean up
要从工作树中删除未跟踪或忽略的文件,请使用TortoiseGit → Clean up。然后出现一个对话框,允许你从当前目录或整个工作树(取决于安装的git版本)开始,通过递归删除不受版本控制或被忽略的文件来清理工作树。
Remove all untracked files:这将删除所有未跟踪文件,包括Git忽略的文件。这是最干净的选择。
Remove non-ignore untracked files:这将删除未跟踪文件,但不包括Git忽略的文件。
Remove ignored files:只清除被忽略的文件这只删除Git忽略的文件
Remove untracked directories :删除未跟踪目录。
Do not use recycle bin:不要使用回收站,即直接永久删除这些文件。
Dry run:这只是给出了要删除的文件列表,但不执行任何删除。
Submodules :递归地清理子模块。
13. Ignore
编译后会产生一些不受版本控制的文件和文件夹。如用于存储可执行文件bin/、obj/、db、*table等等。每当提交更改时,TortoiseGit都会显示这些派生文件,这将填充提交对话框中的文件列表。当然,你可以关闭此显示,但可能会忘记添加新的源文件。
避免这些问题的最佳方法是将派生文件添加到项目的忽略列表中。这样,它们将永远不会出现在提交对话框中,但真正需要commit的源文件仍将被标记。
右键单击一个或多个未版本化的文件,并选择TortoiseGit→ add to ignore list,将出现一个子菜单,允许你选择按名称或扩展名忽略。如下图二所示,忽略对话框允许你选择忽略类型和忽略文件。
Ignore Type
- 仅忽略包含文件夹中的项目:仅忽略该文件夹中的选定pattern。
- 递归忽略项:忽略该文件夹和子文件夹中具有选定模式的项目。
Ignore File
- 存储库根目录中的.gitignore:在存储库根目录的.gitignore中写入忽略条目。这允许您将忽略列表与远程存储库同步。
- .gitignore位于项目的包含目录中:在包含项的目录中的.gitignore中写入忽略项。这允许您将忽略列表与远程存储库同步。
- .git/info/exclude:在存储库元数据中的.git/info/exclude中写入忽略条目。这允许您在本地存储忽略列表,但不能与远程存储库同步。
14. Amend
- 提交代码到远端仓库后,如果发现提交有遗漏或者提交有误,可以使用如下方法修改最后一条提交信息。
修改后,右键commit -> 选中Amend Last Commit -> 修改代码 or log -> 点击commit -> push的时候选中unknown changes。
15. Cherry Pick
Cherry-pick,即挑选一个需要的commit 进行操作。它可以将在源分支A上的commit修改移植到目标分支B。
(1)pull or fetch,确保本地仓库含有源分支A和目标分支B的最新内容。
(2)本地仓库切到目标分支B -> 右键TortoiseGit show log -> 点击右上角的目标分支,选中源分支A -> 选中所需的commit -> 右键,点击Cherry pick this comment -> 如果需要多个commit,点击add添加 -> 点击continue,即可在目标分支的log里看到刚刚cherry pick的commit。
add “cherry picked from”:在提交的时候log下方会自动生成一句(cherry picked from commit 40871bfa6ca0dfab64357b7feb18b1abbc3c05ec)
此处也可以右键选择edit、squash等操作,以修改log和commit。
16. Q&A
情形1:代码clone到本地,simstudio编译fail,但报错很奇怪,看起来代码没有问题,
情形2:编译pass,但是运行的时候断点没触发,表面看起来运行的不是这份代码生成的dll,但是remove、 rebuild工程都不行
以上两种情况很可能是文件的行尾造成的,不同操作系统使用不同的换行符表示行尾,例如Windows使用回车换行符(CRLF,\r\n),而Linux、Mac OS使用换行符(LF,\n)。代码行尾符的变化可能由于编辑器或者git的行尾符默认设置导致的。
配置git的行尾符默认设置,如果AutoCrLf为false(表示Git不会自动转换换行符),点击Edit global.gitconfig改为true,ture代表Git会自动将换行符转换为操作系统默认的换行符,如下图所示:
如果还存在行尾符不是PC行尾符的情况,可以使用git bash的unix2dos命令。
查看行尾符:使用beyonce compare打开文件,点击规则,选中比较行结尾即可。