跳转至

Git Tools

This part is mainly from ProGit Ch7

Tip

这部分随缘更新,用到的时候再学,学完顺便总结一下

最近在 skypilot 进行高强度开发,因此遇见很多git中曾经不重视的问题,借此机会学习并进行汇总

事实上,开发的过程中各种查缺补漏,到最后都是搞懂这张git指令集

只是,单纯花时间系统学没太大必要,用到再查,查完学会即可。

Stash and Clean

Examples

基础环境

Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# `more` branch is created by `main` git ls-tree --name-only main
1.py
2.py
3.py
4.py
  ~/Desktop/test   main ?1                                                                                                       14:59:33
❯ git ls-tree --name-only more
1.py
2.py
3.py
5.py
  ~/Desktop/test   main ?1                                                                                                       14:59:36
❯ git branch                  
* main
  more

使用场景

现在我在more分支下新建两个文件:bxhu_add_not_commit.pybxhu_not_add.py.

Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 git add bxhu_add_not_commit.py
❯ git status
On branch more
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   bxhu_add_not_commit.py

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    bxhu_not_add.py

为了更好的说明stash的特性,我特地设置了两个文件,一个被追踪但未被提交,一个压根没被追踪。

现在的问题是,我突然要回到main分支,新建一个6.py;但是,上述两个文件都是我瞎写的,我甚至不想给它们commit,目前没法切换分支 ToT

Legal but not good

其实“目前没法切换分支”不完全准确,事实上也可以切换,但是这样很不安全,不符合规范

如果在 more 分支中添加了新文件而没有提交,当您切换回 main 分支时,这些文件会暂时出现在 main 分支的工作区。这种情况发生的原因是:

  • 暂存文件 bxhu_add_not_commit.py
    • 该文件被 git add 暂存了但未提交。Git 会把暂存的更改带到切换后的分支上,因为暂存区的内容在 Git 看来是待定的,属于未完成的变更。
  • 未跟踪文件 bxhu_not_add.py
    • 未跟踪文件会一直停留在工作区中,跨分支切换时也会存在。Git 默认不会自动移除未跟踪的文件,除非通过 git clean 清理。

因此我在这里,最规范的做法是:git stash,暂存目前的变更进入缓冲区

From ProGit

Now you want to switch branches, but you don’t want to commit what you’ve been working on yet, so you’ll stash the changes. To push a new stash onto your stack, run git stash or git stash push:

现在我们进行stash操作

stash状态1:

Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 git status
On branch more
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   bxhu_add_not_commit.py

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    bxhu_not_add.py

❯ git stash save "bxhu_not_add.py is not tracked now"
Saved working directory and index state On more: bxhu_not_add.py is not tracked now

stash状态2:

Bash
1
2
3
4
5
6
7
8
 git add bxhu_not_add.py

❯ git stash save "track bxhu_not_add.py"
Saved working directory and index state On more: track bxhu_not_add.py

❯ git status
On branch more
nothing to commit, working tree clean

检查当前stash list:

Bash
1
2
3
 git stash list
stash@{0}: On more: track bxhu_not_add.py
stash@{1}: On more: bxhu_not_add.py is not tracked now

注意这里,stash@{0}是最新的stash,stash@{1}是之前的stash,以此类推...

现在假设我们==回到了 main 分支进行一系列操作,操作完,又回到 more 分支==,我们想要恢复之前的stash,那么我们可以使用git stash apply命令:

先回到比较老的状态(stash@{1})试试:

Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  ~/Desktop/test   more *2                                                                                                                                                   15:55:35
❯ git stash apply stash@{1}
On branch more
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   bxhu_add_not_commit.py

  ~/Desktop/test   more *2 +1                                                                                                                                                16:00:08
❯ git status
On branch more
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   bxhu_add_not_commit.py

再到最新的状态(stash@{0}):

Bash
1
2
3
4
5
6
 git stash apply stash@{0}
On branch more
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   bxhu_add_not_commit.py
    new file:   bxhu_not_add.py

注意这里,stash apply之后,它会自动显示当前的status,不需要再单独操作一次了

Summary

个人来看,stash 是一个“时间机器” for specific branch,它可以让我们在当前分支上,暂存当前的工作区,然后切换到其他分支,完成其他分支的工作,然后再切换回来,恢复之前的工作区。

这里总结一下常见的 stash 指令:

1) 保存修改

Bash
1
2
git stash # save current status (staged + unstaged)
git stash save "specific message for this item" # Strongly Recommend!!!
Unstaged and Staged
  • 默认情况下,git stash 只会保存已跟踪的文件修改 (staged)
  • 使用 git stash -u 可以同时保存未跟踪的文件 (staged + unstaged)

2) 恢复修改

Bash
1
2
git stash pop # back to "timepoint" and delete corresponding stash item
git stash apply # back to "timepoint" but keep stash item

3) 查看stash list

Bash
1
git stash list # PS: Stack Structure,  stash@{0} is the latest stash

4) 删除list条目

Bash
1
2
git stash drop stash@{n} # delete specific item in Stash List
git stash clear          # delete all items in Stach List

My Choice:

  • git stash save "msg"
  • git stash list
  • git stash apply stash@{n}
  • git stash drop stash@{n}

Rebase and Merge

事实上这是一个历史遗留的疑惑点,正好今天有时间系统整理一下:

This video is all you need :)

Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# stage 1 in main
git init
echo "init content" >> file.txt
git add file.txt
git commit -m "init file"

# stage 2 in feature
git checkout -b feature
echo "modified by feature branch" >> file.txt
git add file.txt
git commit -m "add new content in file"

# stage 3 in main
git checkout main
echo "modified by main branch" >> file.txt
git add file.txt
git commit -m "add new functionality in main"

现在 commit timeline 长这样:

alt text

Difference

1)如果采取merge方案:

Bash
1
2
git checkout main
git merge feature

alt text

2)如果采取rebase方案:

Bash
1
2
3
4
git checkout feature
git rebase main
git checkout main
git merge feature

中间态,在git rebase main之后

alt text

最终态:after git merge feature

alt text

Exception Process

在feature分支merge到main分支的过程中,会出现一些常见的conflict错误,这里我们分类来说明解决方法:

Merge Conflict

首先确保你已经修改了有冲突的文件(file.txt),删除了冲突标记(<<<<<<, =======, >>>>>>>)并保存了最终想要的内容。

这一步:

  • 如果在vim中,就是逐个对比<<<<<<, =======, >>>>>>>区间内的异同,手动修改;
  • 如果在vscode中,可以利用“冲突合并编辑器”,直接点击“接受当前更改”或“接受传入更改”来解决冲突,非常方便 :)

1) 将修改后的文件添加到暂存区:

Bash
1
git add file.txt

2) 提交合并结果:

Bash
1
git commit -m "Merge branch 'feature': resolve conflicts in file.txt"

PS, 如果在解决冲突过程中改变主意了,想要取消这次合并,可以使用:

Bash
1
git merge --abort

3) 如果之后需要推送到 remote repo,直接推送即可:

Bash
1
git push origin <branch-name>
检查状态的cmd
  • git status, 查看当前冲突状态
  • git diff, 查看具体的冲突内容

Rebase Conflict

首先确保你已经修改了有冲突的文件(file.txt),删除了冲突标记(<<<<<<, =======, >>>>>>>)并保存了最终想要的内容。

这里跟上面merge一模一样。

1) 将修改后的文件添加到暂存区:

Bash
1
git add file.txt

2) 继续 rebase 过程:

Bash
1
git rebase --continue
abort in rebase

在 rebase 过程中可能需要多次解决冲突

放弃这次 rebase,回到之前的状态 (最初始状态):

Bash
1
git rebase --abort

小心:这是回到最初始,而不是上一步状态!

3) rebase 完成后,如需推送到远程,要使用强制推送:

Bash
1
git push -f origin <branch-name>

Git-LFS

Git Large File Storage (LFS) 是一个 Git 的扩展,用于管理大型文件,如视频、音频、图像等。

git-lfs官网

git-lfs官方文档

它通过将大型文件存储在 Git 仓库之外的服务器上,并在 Git 仓库中存储指向这些文件的指针,从而解决了 Git 对大型文件的处理效率低下的问题。

Why Git-LFS

alt text

这张图实际上说清楚了git-lfs的设计理念,便是:

对于超级大的文件,我们实际上将其存储在git-lfs的远程库(第三方托管,有使用额度,超过则需要缴费),而在本地库中,我们只存储一个指向这个文件的指针,这样就可以大大减少本地库的大小,提高git的处理效率。

alt text

这是git-lfs官网的开篇图片,说的更加简明:

对于一般开发的code,常规来说文件肯定不会太大,我们使用git就足够了。

但是对于一些大型文件file.psd,比如.pdf / .mp4等媒体文件,我们就需要使用git-lfs来管理了。

How it works

alt text

Git LFS 通过将大文件替换为指针文件的方式来管理大文件:

  1. 在本地仓库中只保存指针文件,这些指针文件极小(通常小于 1KB)
  2. 实际的大文件内容存储在 Git LFS 服务器上
  3. 指针文件包含三个关键信息
    Text Only
    1
    2
    3
    version https://git-lfs.github.com/spec/v1
    oid sha256:[文件的唯一hash值]
    size [文件大小]
    

Process

  1. 添加文件时

    • 当执行 git add 命令时,Git LFS 会创建指针文件替换原文件内容
    • 实际文件内容被存储在本地 Git LFS 缓存中(.git/lfs/objects 目录)
  2. 推送到远程时

    • 当执行 git push 时,Git LFS 的 pre-push 钩子会被触发
    • 大文件内容会从本地 LFS 缓存直接传输到远程 Git LFS 存储服务器
    • 而==指针文件会被推送到常规的 Git 仓库中==
  3. 克隆和检出时

    • 克隆仓库时只会下载指针文件,不会下载大文件内容
    • 只有在 checkout 到具体文件时,才会从 LFS 服务器下载对应版本的实际文件内容

Storage

  1. 本地仓库

    • 只存储指针文件(约 132 字节)
    • 实际文件存在 .git/lfs/objects 目录下
    • 只保存当前工作所需的文件版本
  2. 远程仓库

    • Git 仓库中存储指针文件
    • LFS 服务器存储实际的大文件内容
    • 保存文件的所有历史版本

这种设计的优点是:

  • 显著减少 Git 仓库的体积
  • 加快克隆和拉取操作
  • 只下载实际需要的文件版本

缺点是:

  • 需要依赖网络访问 LFS 服务器
  • 本地仓库不再是完整的仓库副本,需要额外的 LFS 服务器支持

How to Use

在mac/linux上安装git-lfs非常简单,只需要在终端中输入:

Bash
1
git lfs install

此时,你会发现仓库里多了一个.gitattributes文件,这个文件用于配置哪些文件需要使用git-lfs来管理

Bash
1
2
ls -a
# we can find `.gitattributes` now

追踪需要的大文件,这里的文件可以是后缀名(群体),也可以是文件名(个体):

Bash
1
2
3
4
5
# track all .pdf files in this repo
git lfs track "*.pdf"

# track a specific file
git lfs track "bxhu-handsome.pdf"

此时检验一下.gitattributes文件,会发现多了一些内容:

Text Only
1
2
3
4
5
❯ cat .gitattributes
───────┬────────────────────────────────────────────────────────────────────────────────────────────
       │ File: .gitattributes
───────┼────────────────────────────────────────────────────────────────────────────────────────────
   1   │ *.pdf filter=lfs diff=lfs merge=lfs -text

.gitatributes文件自身也追踪一下:

Bash
1
git add .gitattributes
No Forward Compatibility!

Note that defining the file types Git LFS should track will not, by itself, convert any pre-existing files to Git LFS, such as files on other branches or in your prior commit history. To do that, use the git lfs migrate command, which has a range of options designed to suit various potential use cases.

如果我曾经有一些大文件了,但是那时候还没有启用git-lfs,则此时添加git-lfs后,不会自动前向追踪

我需要再额外使用 git lfs migrate 命令来将这些大文件转换为git-lfs的格式。

更加推荐的做法请参考:here

此时,就可以像往常一样,a-c-p了:

Bash
1
2
3
git add .
git commit -m "Add pdf with git-lfs"
git push origin main

TL;DR

Bash
1
2
3
4
5
git lfs track "*.pdf"
git add .gitattributes
git add .
git commit -m "Add pdf with git-lfs"
git push origin main