Administrator
发布于 2026-03-03 / 5 阅读
0
0

利用git实现版本控制项目运行

一、Git的概论

什么是版本控制?

版本控制的定义

版本控制系统(Version Control System,VCS) 是一种记录文件内容变化,以便将来查阅特定版本修订情况的系统。它能够:

  • 记录历史:保存文件的每一次修改记录

  • 追溯变更:查看谁在什么时候修改了什么内容

  • 版本回退:随时恢复到之前的任意版本

  • 协作开发:多人同时对同一项目进行修改

例如:从实际场景来说

假设你是一名安全研究员,正在编写一份漏洞分析报告。随着工作的推进,你的文件可能会变成这样:

漏洞分析报告.doc
漏洞分析报告_修改版.doc
漏洞分析报告_最终版.doc
漏洞分析报告_最终版2.doc
漏洞分析报告_打死也不改了.doc
漏洞分析报告_真的最终版.doc

这种情况是不是很熟悉?这就是没有版本控制带来的混乱

Git 的核心特点

1. 分布式架构

每个开发者的本地都有一个完整的版本库,包含项目的全部历史记录。这意味着:

  • 几乎所有操作都在本地完成,速度极快

  • 即使没有网络连接也能正常工作

  • 任何一个副本都可以作为备份

2. 数据完整性保证

Git 使用 SHA-1 哈希算法对所有数据进行校验。每一个文件、每一次提交都有一个唯一的 40 位十六进制哈希值:

e83c5163316f89bfbde7d9ab23ca2e25604af290

这个特性对于安全工作者来说尤为重要——任何对历史记录的篡改都会导致哈希值变化,从而被发现

3. 只添加数据

Git 的操作几乎只往数据库中添加数据,很难让 Git 执行任何不可逆的操作。这意味着:

  • 已提交的数据很难丢失

  • 可以放心地进行各种实验

  • 历史记录具有可追溯性

4. 三种状态

Git 管理的文件有三种状态:

状态

英文

说明

已修改

Modified

文件已被修改,但还没有保存到数据库中

已暂存

Staged

对已修改的文件做了标记,使之包含在下次提交的快照中

已提交

Committed

数据已经安全地保存在本地数据库中

这三种状态对应 Git 项目的三个工作区域:

工作目录(Working Directory)
    |
    | git add
    v
暂存区域(Staging Area)
    |
    | git commit
    v
Git 仓库(Repository)

二、安装Git

windows系统安装

方法一:官方安装包(推荐)

  1. 访问 Git 官方网站:https://git-scm.com/

  2. 点击 "Download for Windows" 下载安装包

  3. 运行安装程序,按照向导完成安装

方法二:使用包管理器(windows)

如果你使用 Chocolatey 包管理器:

choco install git

如果你使用 winget:

winget install Git.Git

macOS 系统安装

方法一:Xcode Command Line Tools

打开终端,输入:

git --version

如果系统没有安装 Git,会自动提示安装 Xcode Command Line Tools。

方法二:Homebrew(推荐)

如果你已经安装了 Homebrew:

brew install git

方法三:官方安装包

访问 https://git-scm.com/download/mac 下载安装包。


Linux 系统安装

Debian/Ubuntu 系列

sudo apt update
sudo apt install git

RHEL/CentOS/Fedora 系列

# CentOS/RHEL 7
sudo yum install git

# CentOS/RHEL 8+ / Fedora
sudo dnf install git

Arch Linux

sudo pacman -S git

验证安装

安装完成后,打开终端(Windows 用户打开 Git Bash),输入以下命令验证:

git --version

如果看到类似以下输出,说明安装成功:

git version 2.43.0

三、Git基本操作(实验步骤)

1. Git初使配置

安装完成后,在开始使用 Git 之前,需要进行一些基本配置

配置用户信息

Git 要求每次提交都必须包含用户信息,这对于追踪代码变更非常重要。

# 设置用户名
git config --global user.name "你的名字"

# 设置邮箱
git config --global user.email "your.email@example.com"

注意:这里的用户名和邮箱会记录在每一次提交中,是公开可见的。在安全工作中,这些信息可能被用于追踪代码作者

配置级别说明

Git 的配置分为三个级别:

级别

参数

配置文件位置

作用范围

系统级

--system

/etc/gitconfig

所有用户

用户级

--global

~/.gitconfig

当前用户

仓库级

--local

.git/config

当前仓库

优先级从低到高:系统级 < 用户级 < 仓库级

其他常用配置

配置默认编辑器

# 使用 Vim
git config --global core.editor vim

# 使用 VS Code
git config --global core.editor "code --wait"

# 使用 Nano
git config --global core.editor nano

配置默认分支名称

git config --global init.defaultBranch main

配置命令别名

git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.lg "log --oneline --graph --all"

配置颜色输出

git config --global color.ui auto

查看配置信息

查看所有配置:

git config --list

查看特定配置项:

git config user.name
git config user.email

查看配置项的来源:

git config --list --show-origin

步骤1:初始化新仓库,并找到克隆路径

# 创建项目目录
mkdir -p secscan
cd secscan

# 初始化 Git 仓库
git init

执行后,你会看到类似这样的输出:

Initialized empty Git repository in /home/mario/secscan/.git/

此时,Git 会在当前目录下创建一个名为 .git 的隐藏目录,这个目录包含了 Git 仓库的所有元数据。后面会详细讲解这个目录的结构

验证仓库创建成功

# 查看生成的 .git 目录
ls -la

输出示例:

total 12
drwxrwxr-x 3 kali kali 4096 Jan 26 10:00 .
drwxrwxr-x 3 kali kali 4096 Jan 26 10:00 ..
drwxrwxr-x 7 kali kali 4096 Jan 26 10:00 .git

看到 .git 目录,说明仓库初始化成功。

github上找到克隆项目

打开github找到nmpa复制源代码:https://github.com/nmap/nmap

步骤2:克隆源代码

# 将 nmap 源码库完整克隆到指定的本地目录 /Users/mario/secscan
git clone https://github.com/nmap/nmap.git /Users/mario/secscan

结果图

原理解析:

这个操作完成了 “远程开源项目本地化” 的核心步骤,可以脱离网络或

  • 但改不了核心部件

  • git clone 命令(对应去车企拿全套改装图纸):你拿到所有设计图,能自己改发动机、加配件,甚至看懂这车为啥这么设计

  • 指定 /Users/mario/secscan(对应把图纸放到你车库的 “改装车专区” 抽屉):方便你后续找图纸、和其他改装资料放一起

总结:

  • 这个命令的核心就是:把 nmap 的 “全套设计图纸” 完整搬到你电脑指定的文件夹里,不是只拿个 “成品”

  • 这么做的好处:能自己改 nmap、适配自己的电脑、学习它的设计逻辑,比只用现成的安装包灵活得多

  • 指定路径只是为了让 “图纸” 放得更规整,方便你后续找

步骤3:文件状态与生命周期

文件4种状态

未跟踪(Untracked)
    |
    | git add
    v
已暂存(Staged)
    |
    | git commit
    v
未修改(Unmodified)
    |
    | 编辑文件
    v
已修改(Modified)
    |
    | git add
    v
已暂存(Staged)
    ...

状态说明

状态

说明

Untracked

新创建的文件,Git 还不知道它的存在

Staged

文件已被添加到暂存区,等待提交

Unmodified

文件已提交,且自上次提交后没有修改

Modified

文件已被修改,但还没有暂存

举例:

# 确保在项目目录中
cd secscan

# 创建 README 文件
cat > README.md << 'EOF'
# SecScan - 端口扫描器

一个用于内部安全评估的端口扫描工具。

## 功能

- TCP 端口扫描
- 服务识别
- 结果导出

## 作者

安全工具开发组
EOF

#查看文件状态
git status

结果图

简洁模式

git status -s

输出:

?? README.md

?? 表示未跟踪的文件。简洁模式的常见标记:

M file.txt        # 已修改,未暂存
M  file.txt        # 已修改,已暂存
MM file.txt        # 已暂存后又被修改
A  file.txt        # 新添加到暂存区
?? file.txt        # 未跟踪

步骤4:把文件提交到缓存状态

源代码:常用的 git add 用法

# 添加单个文件
git add filename.txt

# 添加多个文件
git add file1.txt file2.txt

# 添加当前目录下的所有文件
git add .

# 添加所有已跟踪文件的修改(不包括新文件)
git add -u

# 添加所有变更(包括新文件、修改、删除)
git add -A

# 提交(还是提交在本地,-m是为了方便了解提交的内容,类似注释)
git commit -m "mario添加本项目描述"

结果图

常用的 git commit 用法

# 直接在命令行输入提交信息
git commit -m "提交信息"

# 打开编辑器输入详细的提交信息
git commit

# 跳过暂存区,直接提交所有已跟踪文件的修改
git commit -am "提交信息"

修改最后一次提交

# 修改提交信息
git commit --amend -m "新的提交信息"

# 添加遗漏的文件到上次提交
git add forgotten-file.txt
git commit --amend --no-edi

注意--amend 会改变提交的哈希值,如果已经推送到远程仓库,不要使用这个命令

步骤5:使用VS Code查看git

结果图:

源代码:创建主程序文件

#!/usr/bin/env python3
"""
SecScan - 端口扫描器
"""
import socket
import argparse
from datetime import datetime

def scan_port(host, port, timeout=1):
    """扫描单个端口"""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((host, port))
        sock.close()
        return result == 0
    except:
        return False

def scan_range(host, start_port, end_port):
    """扫描端口范围"""
    open_ports = []
    for port in range(start_port, end_port + 1):
        if scan_port(host, port):
            open_ports.append(port)
            print(f"[+] Port {port} is open")
    return open_ports

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="SecScan - 端口扫描器")
    parser.add_argument("host", help="目标主机")
    parser.add_argument("-p", "--ports", default="1-1000", help="端口范围")
    args = parser.parse_args()
    
    print(f"[*] Scanning {args.host}...")
    print(f"[*] Time: {datetime.now()}")

创建敏感信息配置文件

"""
SecScan 配置文件
"""
# 扫描代理服务器配置
PROXY_HOST = "192.168.1.100"
PROXY_PORT = 8080
PROXY_USER = "scanner"
PROXY_PASSWORD = "Sc@nner2024!Secret"  # 内部代理密码

# API 密钥(用于结果上报)
API_KEY = "sk-secscan-a]1b2c3d4e5f6g7h8i9j0"
API_SECRET = "secret-xxxxxxxxxxxxxxxxxxxx"

# 数据库配置(存储扫描结果)
DB_HOST = "192.168.1.50"
DB_USER = "secscan"
DB_PASSWORD = "DB@Pass2024!"
DB_NAME = "scan_results"

结果图:

步骤7:修改文件并查看差异

源代码

#修改文件,使用环境变量
cat > config.py << 'EOF'
"""
SecScan 配置文件
警告:请勿将敏感信息写入此文件!
"""
import os

# 扫描代理服务器配置(从环境变量读取)
PROXY_HOST = os.environ.get("PROXY_HOST", "localhost")
PROXY_PORT = int(os.environ.get("PROXY_PORT", "8080"))
PROXY_USER = os.environ.get("PROXY_USER", "")
PROXY_PASSWORD = os.environ.get("PROXY_PASSWORD", "")

# API 密钥
API_KEY = os.environ.get("API_KEY", "")
API_SECRET = os.environ.get("API_SECRET", "")

# 数据库配置
DB_HOST = os.environ.get("DB_HOST", "localhost")
DB_USER = os.environ.get("DB_USER", "")
DB_PASSWORD = os.environ.get("DB_PASSWORD", "")
DB_NAME = os.environ.get("DB_NAME", "scan_results")
EOF

# 查看差异
git diff config.py

结果图:

提交修复

# 添加修改
git add config.py

# 提交
git commit -m "安全修复:移除硬编码密码,改用环境变量"

查看提交历史

git log --oneline

# 输出
xxxxxxx 安全修复:移除硬编码密码,改用环境变量
xxxxxxx 添加扫描器核心代码和配置文件
xxxxxxx 初始化项目:添加 README
## 重要警告:虽然最新版本的配置文件不再包含密码,但密码仍然存在于 Git 历史中!

# 验证:从历史中查看旧版本的配置文件
git show HEAD~1:config.py
## 这将显示包含密码的旧版本!这就是为什么单纯删除敏感信息并不能真正解决问题

步骤8:撤销内容

源代码

# 模拟误操作
echo "jjdhjkcnkjadchdsccjdhcksj" >> config.py

# 查看状态
git status -s

# 撤销修改(恢复到上次提交的状态)
git restore config.py

# 验证
git status -s

结果图

步骤9:撤销暂存区的文件

源代码

# 假设添加了不该添加的文件
echo "test" > test.txt

# 缓存
git add test.txt

# 查看状态
git status -s

# 把文件指定从暂存区拿出来
git restore --staged test.txt

# 再次查看状态
git status -s

# 删掉文件
rm test.txt

结果图

步骤10:重置

撤销提交

方式一:git reset(改变历史)

模式

工作目录

暂存区

仓库

--soft

保留

保留

回退

--mixed

保留

清空

回退

--hard

清空

清空

回退

# 软重置(但会保留工作路径,缓存、仓库都保留)
git reset --soft HEAD~1

# 工作目录缓存都会清空的重置
git reset --hard HEAD~1

# 删文件夹
git rm -r 文件夹名

# 从工作目录和暂存区删除
git rm 文件名

# 只从暂存区删除(保留工作目录中的文件)
git rm --cached 文件名

# 强制删除已修改的文件
git rm -f 文件名
# 删除目录
git rm -r 目录名/

方式二:git revert(不改变历史)

# 创建一个新提交来撤销指定提交
git revert HEAD

git revert 不会删除历史记录,更安全

步骤11:重命名

# 改名
git mv 旧文件名 新文件名

步骤12:忽略文件:防止敏感信息提交

为了防止以后再次误提交敏感文件,我们需要创建 .gitignore 文件

为 SecScan 项目创建 .gitignore

cd secscan

# 创建 .gitignore 文件
cat > .gitignore << 'EOF'
# 敏感配置文件
.env
.env.*
secrets.yml
credentials.json

# 本地配置(包含密码)
config.local.py

# 扫描结果(可能包含敏感信息)
results/
*.json
*.csv

# Python
__pycache__/
*.py[cod]
*.egg-info/
venv/

# 日志
*.log
logs/

# IDE
.idea/
.vscode/
*.swp

# 操作系统
.DS_Store
Thumbs.db
EOF

# 查看状态
git status -s

输出:

?? .gitignore
# 提交 .gitignore
git add .gitignore
git commit -m "添加 .gitignore:防止敏感文件提交"

.gitignore 语法规则

语法

说明

#

注释

*

匹配任意字符(不包括 /)

?

匹配单个字符

**

匹配任意目录

/ 开头

只匹配根目录

/ 结尾

只匹配目录

!

取反(不忽略)

全局忽略文件

# 创建全局忽略文件
git config --global core.excludesfile ~/.gitignore_global

已跟踪文件的忽略

如果文件已经被跟踪,添加到 .gitignore 不会生效。需要先从暂存区移除:

git rm --cached filename.txt

安全提示:在安全审计中,检查 .gitignore 文件可以发现项目中可能存在的敏感文件类型

四、git分支管理(实验步骤)

什么是分支

在 Git 中,分支本质上是指向某个提交对象的可移动指针。

想象一下,你正在写一本书。主线剧情已经写好了,但你突然有了一个新想法,想尝试一个不同的结局。你不想破坏已有的内容,于是你复制了一份手稿,在副本上进行修改。这个副本就相当于一个分支。

在 Git 中,这个过程更加轻量级——创建分支不需要复制任何文件,只是创建一个新的指针

分支的内部原理

当你执行 git commit 时,Git 会创建一个提交对象,这个对象包含:

  • 指向暂存内容快照的指针

  • 作者信息和提交信息

  • 指向父提交的指针(首次提交没有父提交,合并提交有多个父提交)

提交对象结构:

commit 1a2b3c4d
├── tree(指向目录树)
├── parent(指向父提交)
├── author
├── committer
└── message

分支就是一个指向这些提交对象的指针。默认分支名为 main(或旧版本的 master

HEAD 指针

HEAD 是一个特殊的指针,指向当前所在的分支。可以把它理解为"你现在在哪里"的标记

用 “看书” 比喻 HEAD 指针

把 Git 仓库想象成一本厚厚的书(每个提交版本是书的一页):

  • 书的目录(refs/heads/):记录了各章节(分支)的起始页码,比如「main 章节」从第 100 页开始,「feat/login 章节」从第 120 页开始;

  • HEAD 指针:就是你看书时夹的书签 ——

  • 正常看书(在分支上):书签夹在「main 章节」的目录页,意思是 “我现在在 main 分支,看的是这个分支最新的内容”;

  • 翻到某一页(分离头指针):书签直接夹在第 110 页,意思是 “我现在直接看第 110 页的内容,和任何章节(分支)都无关”

代码块

# 查看 HEAD 指向:
cat .git/HEAD

# 输出示例:
ref: refs/heads/main

创建分支开发新功能

现在让我们为 SecScan 添加 UDP 扫描功能

1.查看当前分支

# 进入项目目录
cd ~/git-security-labs/user-management-system

# 查看当前分支
git branch

结果图

2.创建并切换到新分支

# 创建并切换到 feature-udp 分支
git switch -c feature-udp

# 查看分支
git branch

结果图

3.在新分支上开发

# 添加 UDP 扫描功能
cat >> secscan.py << 'EOF'

def scan_udp_port(host, port, timeout=1):
    """扫描 UDP 端口"""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.settimeout(timeout)
        sock.sendto(b'', (host, port))
        try:
            data, addr = sock.recvfrom(1024)
            return True
        except socket.timeout:
            return True  # UDP 无响应可能表示端口开放
    except:
        return False
    finally:
        sock.close()
EOF

# 查看修改
git diff
## [feature-udp xxxxxxx] feature: 添加 UDP 端口扫描功能
## 1 file changed, 15 insertions(+)

# 提交
git add secscan.py
git commit -m "feature: 添加 UDP 端口扫描功能"

查看分支历史

# 查看所有分支的提交历史
git log --oneline --graph --all

结果图

4切换分支与并行开发

换回 main 分支

# 切换回 main
git switch main

# 查看当前分支
git branch

创建另一个功能分支

git switch -c feature-service-detect

# 添加服务识别功能
cat >> secscan.py << 'EOF'

# 常见服务端口映射
SERVICE_PORTS = {
    21: 'FTP',
    22: 'SSH',
    23: 'Telnet',
    25: 'SMTP',
    53: 'DNS',
    80: 'HTTP',
    443: 'HTTPS',
    3306: 'MySQL',
    3389: 'RDP',
    6379: 'Redis',
}

def identify_service(port):
    """根据端口识别服务"""
    return SERVICE_PORTS.get(port, 'Unknown')
EOF

# 提交
git add secscan.py
git commit -m "feature: 添加服务识别功能"

结果图

查看分支状态

git log --oneline --graph --all

# 输出:
## * xxxxxxx (HEAD -> feature-service-detect) feature: 添加服务识别功能
## | * xxxxxxx (feature-udp) feature: 添加 UDP 端口扫描功能
## |/
## * xxxxxxx (main) 添加 .gitignore:防止敏感文件提交
## ...

现在有两个并行的功能分支!

常用分支命令速查

命令

说明

git branch

查看分支

git switch -c <name>

创建并切换分支

git switch <name>

切换分支

git branch -d <name>

删除分支

git branch -m <new>

重命名分支


合并分支

服务识别功能开发完成,现在要合并到主分支。

1.合并 feature-service-detect 分支

# 切换到 main 分支
git switch main

# 合并 feature-service-detect
git merge feature-service-detect -m "合并服务识别功能"

输出:

Updating xxxxxxx..xxxxxxx
Fast-forward
 secscan.py | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

这是一个「快进合并」(Fast-forward),因为 main 分支没有新的提交。

2.合并 feature-udp 分支

# 合并 feature-udp
git merge feature-udp -m "合并 UDP 扫描功能"

输出:

Auto-merging secscan.py
CONFLICT (content): Merge conflict in secscan.py
Automatic merge failed; fix conflicts and then commit the result.

冲突了! 因为两个分支都修改了 secscan.py 文件。

3.解决合并冲突

# 查看冲突文件
cat secscan.py | grep -A 10 "<<<<<<"

冲突标记示例:

<<<<<<< HEAD
# 常见服务端口映射
SERVICE_PORTS = {
...
=======
def scan_udp_port(host, port, timeout=1):
...
>>>>>>> feature-udp

解决冲突:我们需要保留两边的代码,删除冲突标记。

# 手动编辑解决冲突(保留两边的功能)
# 这里我们用脚本模拟解决冲突
cat > secscan.py << 'EOF'
#!/usr/bin/env python3
"""
SecScan - 端口扫描器
作者: 安全工具开发组
"""
import socket
import argparse
from datetime import datetime

def scan_port(host, port, timeout=1):
    """扫描 TCP 端口"""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((host, port))
        sock.close()
        return result == 0
    except:
        return False

def scan_udp_port(host, port, timeout=1):
    """扫描 UDP 端口"""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.settimeout(timeout)
        sock.sendto(b'', (host, port))
        try:
            data, addr = sock.recvfrom(1024)
            return True
        except socket.timeout:
            return True
    except:
        return False
    finally:
        sock.close()

# 常见服务端口映射
SERVICE_PORTS = {
    21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP',
    53: 'DNS', 80: 'HTTP', 443: 'HTTPS', 3306: 'MySQL',
    3389: 'RDP', 6379: 'Redis',
}

def identify_service(port):
    """根据端口识别服务"""
    return SERVICE_PORTS.get(port, 'Unknown')

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="SecScan - 端口扫描器")
    parser.add_argument("host", help="目标主机")
    args = parser.parse_args()
    print(f"[*] Scanning {args.host}...")
EOF

# 标记冲突已解决
git add secscan.py

# 提交合并
git commit -m "合并 UDP 扫描功能,解决冲突"

4.查看合并后的历史

git log --oneline --graph --all

输出:

*   xxxxxxx (HEAD -> main) 合并 UDP 扫描功能,解决冲突
|\  
| * xxxxxxx (feature-udp) feature: 添加 UDP 端口扫描功能
* | xxxxxxx 合并服务识别功能
|/  
* xxxxxxx (feature-service-detect) feature: 添加服务识别功能
...

5.删除已合并的分支

# 删除已合并的功能分支
git branch -d feature-udp
git branch -d feature-service-detect

# 查看分支
git branch

输出:

* main

功能分支已清理完毕。


储藏工作(Stash)

组长突然说:「有个紧急 bug 需要修复,你先放下手头的工作。」

但是你正在开发新功能,代码还没写完,不想提交。这时可以用 git stash 保存工作进度。

1 模拟场景

cd ~/security-tools/secscan

# 开始开发新功能(未完成)
echo '
# TODO: 添加结果导出功能
def export_results(results, filename):
    pass  # 未完成
' >> secscan.py

# 查看状态
git status -s

输出:

M secscan.py

2 储藏当前工作

# 储藏工作进度
git stash push -m "未完成的结果导出功能"

# 查看状态
git status -s

输出为空,工作目录已清空。

# 查看储藏列表
git stash list

输出:

stash@{0}: On main: 未完成的结果导出功能

3 修复紧急 bug

# 现在可以安全地修复 bug
git switch -c hotfix-timeout

# 修复超时问题
cat > secscan.py << 'EOF'
#!/usr/bin/env python3
"""
SecScan - 端口扫描器
作者: 安全工具开发组
"""
import socket
import argparse
from datetime import datetime

DEFAULT_TIMEOUT = 2  # 修复:增加默认超时时间

def scan_port(host, port, timeout=DEFAULT_TIMEOUT):
    """扫描 TCP 端口"""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((host, port))
        sock.close()
        return result == 0
    except:
        return False

def scan_udp_port(host, port, timeout=DEFAULT_TIMEOUT):
    """扫描 UDP 端口"""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.settimeout(timeout)
        sock.sendto(b'', (host, port))
        try:
            data, addr = sock.recvfrom(1024)
            return True
        except socket.timeout:
            return True
    except:
        return False
    finally:
        sock.close()

SERVICE_PORTS = {
    21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP',
    53: 'DNS', 80: 'HTTP', 443: 'HTTPS', 3306: 'MySQL',
    3389: 'RDP', 6379: 'Redis',
}

def identify_service(port):
    return SERVICE_PORTS.get(port, 'Unknown')

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="SecScan")
    parser.add_argument("host", help="目标主机")
    parser.add_argument("-t", "--timeout", type=int, default=DEFAULT_TIMEOUT)
    args = parser.parse_args()
    print(f"[*] Scanning {args.host}...")
EOF

git add secscan.py
git commit -m "hotfix: 修复超时时间问题"

# 合并到 main
git switch main
git merge hotfix-timeout -m "合并超时修复"
git branch -d hotfix-timeout

4 恢复储藏的工作

# 查看储藏
git stash list

# 恢复储藏并删除记录
git stash pop

# 查看状态
git status -s

输出:

M secscan.py

之前未完成的工作已恢复!

常用 stash 命令

命令

说明

git stash push -m "msg"

储藏并添加描述

git stash list

查看储藏列表

git stash pop

恢复并删除储藏

git stash apply

恢复但保留储藏

git stash drop

删除储藏


变基操作(高级)

1 什么是变基

变基(Rebase)是另一种整合分支的方式。它会将一个分支的提交"移植"到另一个分支上

git checkout feature
git rebase main

2 变基 vs 合并

特性

合并(Merge)

变基(Rebase)

历史记录

保留完整历史,包括分支结构

线性历史,更简洁

提交哈希

不改变原有提交

创建新的提交(哈希值改变)

冲突解决

一次性解决所有冲突

可能需要多次解决冲突

适用场景

公共分支、需要保留历史

个人分支、整理提交历史

3 交互式变基

交互式变基允许你修改、合并、删除或重新排序提交:

git rebase -i HEAD~3

这会打开编辑器,显示最近 3 次提交:

pick abc1234 第一次提交
pick def5678 第二次提交
pick ghi9012 第三次提交

# Commands:
# p, pick = 使用提交
# r, reword = 使用提交,但修改提交信息
# e, edit = 使用提交,但停下来修改
# s, squash = 使用提交,但合并到前一个提交
# f, fixup = 类似 squash,但丢弃提交信息
# d, drop = 删除提交

常见用途

  • 合并多个小提交为一个

  • 修改历史提交信息

  • 删除错误的提交

  • 重新排序提交

警告:不要对已推送到远程的提交进行变基,这会导致历史记录不一致。


分支管理策略

1 Git Flow 工作流

Git Flow 是一种广泛使用的分支管理模型:

main(生产分支)
  |
  +-- hotfix(紧急修复)
  |
develop(开发分支)
  |
  +-- feature(功能分支)
  |
  +-- release(发布分支)

分支说明

分支

用途

生命周期

main

生产环境代码

永久

develop

开发主线

永久

feature/*

新功能开发

临时

release/*

发布准备

临时

hotfix/*

紧急修复

临时

2 GitHub Flow

GitHub Flow 是一种更简单的工作流:

  1. 从 main 创建分支

  2. 在分支上进行开发

  3. 提交 Pull Request

  4. 代码审查

  5. 合并到 main

  6. 部署

3 安全团队的分支策略建议

对于安全团队,建议采用以下策略:

main(稳定版本)
  |
  +-- audit/项目名(代码审计分支)
  |
  +-- poc/漏洞编号(PoC 开发分支)
  |
  +-- report/日期(报告编写分支)

Git分支管理小结

在本章中,我们通过继续开发 SecScan 项目,学习了 Git 分支管理:

  1. 创建分支:为 UDP 扫描和服务识别功能创建独立分支

  2. 并行开发:同时开发多个功能

  3. 合并分支:快进合并和三方合并

  4. 解决冲突:处理合并时的代码冲突

  5. 储藏工作:临时保存未完成的工作

  6. 分支策略:了解安全团队的分支管理模型

当前项目状态

SecScan 现在具备:

  • TCP 端口扫描

  • UDP 端口扫描

  • 服务识别

  • 可配置超时时间

五、Git内部文件结构

Git 的核心是一个内容寻址文件系统,所有仓库的元数据和对象都存储在 .git 目录中,我们平时执行的 git addgit commit 等操作本质上都是在修改这个目录里的内容

1 核心目录结构

.git/
├── HEAD                 # 指向当前分支的指针
├── config               # 仓库级配置文件
├── description          # GitWeb 使用的描述文件
├── hooks/               # 钩子脚本目录
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   └── ...
├── index                # 暂存区(二进制文件)
├── info/                # 额外信息
│   └── exclude          # 本地忽略规则
├── logs/                # 引用日志
│   ├── HEAD
│   └── refs/
├── objects/             # 对象数据库(核心)
│   ├── info/
│   ├── pack/
│   └── [0-9a-f][0-9a-f]/
├── refs/                # 引用(分支、标签等)
│   ├── heads/           # 本地分支
│   ├── remotes/         # 远程分支
│   └── tags/            # 标签
├── COMMIT_EDITMSG       # 最后一次提交的信息
├── FETCH_HEAD           # 最后一次 fetch 的信息
├── ORIG_HEAD            # 危险操作前的 HEAD 备份
└── packed-refs          # 打包的引用

.git/objects/— 原始档案柜

这是 Git 的对象数据库,存储着所有文件内容、提交信息、目录结构等核心数据

存储的对象类型

  • Blob 对象:存储单个文件的内容,不包含文件名等元信息。

  • Tree 对象:存储目录结构,记录文件和子目录的名称、权限,以及对应的 Blob/Tree 对象哈希值。

  • Commit 对象:存储一次提交的元数据,包括作者、提交者、提交信息、指向的 Tree 对象哈希值,以及父提交的哈希值。

  • Tag 对象:存储标签信息,通常指向一个 Commit 对象,用于标记重要版本。

  • 存储方式:对象以 SHA-1 哈希值命名,前两位作为子目录名,后 38 位作为文件名,避免单目录文件过多导致性能问题

举例:

  • 🎯 比喻:公司档案柜,所有代码版本都是带唯一编号的档案袋

  • ✨ 作用:存所有历史版本的原始文件、提交记录,永不删除旧档案

  • 💡 记忆点:你能回滚版本、看历史记录,全靠它存着所有旧档案

🔹.git/refs/— 快捷方式台账

这个目录存储引用(References),也就是指向 Commit 对象的指针,用来简化对长哈希值的记忆

  • refs/heads/:存储所有本地分支,每个分支文件的内容就是该分支当前指向的 Commit 哈希值

  • refs/tags/:存储所有标签,指向对应的 Commit 或 Tag 对象

  • refs/remotes/:存储远程仓库的分支引用,记录本地最后一次同步时远程分支的状态

举例:

  • 🎯 比喻:档案柜编号记不住?用台账把 “好记的名字” 和 “档案编号” 绑定

  • ✨ 作用:

  • refs/heads/:分支台账,每个分支名对应一个档案编号

  • refs/tags/:版本标签台账,v1.0 对应正式版档案编号

  • 💡 记忆点:分支不是复制代码,只是台账里的一行编号

🔹 .git/HEAD — 当前位置便利贴

这是一个符号引用文件,内容是当前工作区所在的分支或提交

  • 如果在分支上(如 main),内容是 ref: refs/heads/main

  • 如果处于 “分离头指针” 状态(直接指向某个 Commit),内容就是该 Commit 的哈希值

举例:

  • 🎯 比喻:你现在在哪操作,就把便利贴贴在哪

  • ✨ 作用:

  • 正常状态:贴在分支台账页(比如 ref: refs/heads/main

  • 分离头指针:直接贴在档案袋上(一串哈希值)

  • 💡 记忆点:切换分支就是 “撕旧便利贴,贴新的”

🔹 .git/index — 提交待办清单

这是 Git 的暂存区(Stage/Index),是一个二进制文件,记录了下一次提交时要包含的文件信息(文件名、哈希值、时间戳等)

  • git add 命令就是将文件的当前状态写入这个索引文件

  • git commit 则是根据这个索引文件生成新的 Tree 和 Commit 对象

举例:

  • 🎯 比喻:提交前先写清单,要存档的文件都列上

  • ✨ 作用:临时记录下一次提交要包含的文件

  • 💡 记忆点:git add 是写清单,git commit 是按清单打包存档

🔹 .git/config — 仓库设置登记本

存储当前仓库的配置信息,包括用户信息、远程仓库地址、分支跟踪关系等

  • 可以通过 git config 命令修改,优先级高于全局配置(~/.gitconfig

举例:

  • 🎯 比喻:办公间的登记本,记着这个仓库的专属配置

  • ✨ 作用:存远程地址、仓库专属的用户名 / 邮箱等

  • 💡 记忆点:git config 命令就是改这个登记本


2 安全视角的重要性

从安全角度来看,.git 目录包含了:

  • 完整的代码历史:包括所有曾经提交过的文件

  • 敏感信息:可能包含被删除但仍存在于历史中的密码、密钥

  • 开发者信息:用户名、邮箱等

  • 服务器信息:远程仓库地址

常见安全问题

  1. Web 服务器暴露 .git 目录

  2. 敏感信息被提交到版本历史

  3. 通过 .git 目录重建完整源代码

3为什么Git采用内容寻址存储

Git 采用内容寻址存储,主要是为了实现高效、可靠、去重的版本管理

🛡️ 确保内容不可篡改

  • Git 对每个文件或提交的内容计算唯一的 SHA-1 哈希值,作为它的 “身份证号”

  • 如果文件内容哪怕只改一个字符,哈希值就会完全不同,Git 立刻就能发现内容被篡改

  • 这相当于给所有历史版本加了 “防伪标签”,保证代码历史的完整性

🧹 自动去重,节省空间

  • 多个分支或版本里的相同文件,Git 只会存一份,因为它们的哈希值相同

  • 比如你在不同分支都用到同一个配置文件,Git 不会重复存储,而是用同一个哈希值指向它

  • 这让仓库体积比复制整个项目的传统方式小得多

⚡ 快速对比与合并

  • 内容寻址让 Git 能通过哈希值快速判断两个文件是否相同,不用逐行比较

  • 合并分支时,Git 只需对比哈希值就能定位差异,大幅提升合并效率

🔗 天然支持分布式协作

  • 每个开发者的本地仓库都有完整的内容寻址数据库,不需要依赖中央服务器

  • 多人协作时,通过哈希值就能确认双方的内容是否一致,避免冲突和数据丢失

  • 即使离线也能正常提交、分支、合并,联网后再同步即可

📜 完整的可追溯性

  • 每个提交的哈希值都包含父提交的哈希值,形成一条完整的 “哈希链”

  • 你可以顺着这条链追溯任何版本的来源,轻松定位问题引入的时间点

  • 这对排查 Bug、审计代码变更至关重要


4.四种基础对象类型

Git 所有版本内容都靠Blob、Tree、Commit、Tag 四大基础对象存储,全是内容寻址(用内容算唯一哈希当 ID),存在.git/objects/里,记清「谁存什么、怎么关联」就够了!

🔹 1. Blob 对象 - 只存「文件内容本身」的小文件

  • 全称:Binary Large Object(二进制大对象)

  • 比如:你有个test.py文件,内容是print(123),Git 生成的 Blob 只存print(123)这行内容,不知道它叫test.py,也不知道它在src文件夹里

  • 关键特点:内容相同,Blob 就相同比如两个文件夹里的config.py内容一样,Git 只存 1 个 Blob,省空间)

  • 哈希特点:内容相同的文件,不管名字 / 路径在哪,哈希值完全一样(Git 去重的核心,相同内容只存一个 Blob)

  • 关联操作:执行git add 文件名,Git 就会把文件内容转成 Blob 对象存起来

Blod对象代码及结果

创建 blob 对象

# 将内容写入对象数据库
echo "Hello, Git" | git hash-object -w --stdin

输出:

b7aec520dec0a7516c18eb4c68b64ae1eb9b5a5e

查看 blob 对象:

git cat-file -p b7aec520dec0a7516c18eb4c68b64ae1eb9b5a5e

输出:

Hello, Git

查看对象类型:

git cat-file -t b7aec520dec0a7516c18eb4c68b64ae1eb9b5a5e

输出:

blob

Blob 对象的存储位置:

.git/objects/b7/aec520dec0a7516c18eb4c68b64ae1eb9b5a5e

对象以哈希值的前两位作为目录名,后 38 位作为文件名


🔹 2. Tree 对象 - 给 Blob 贴「名字 + 位置」的标签,还管文件夹结构

  • 存什么:目录结构信息,包含「当前目录下的文件名 / 子目录名 + 对应 Blob / 子 Tree 的哈希 ID + 文件权限」

  • 核心作用:补全 Blob 的缺陷,专门记录 **“哪个 Blob 对应哪个文件名”“哪个文件在哪个文件夹”,还会记录文件权限,最终拼成整个项目的目录树

  • 层级关系:根目录有一个根 Tree,子目录对应子 Tree,最终所有 Tree 串联成完整的项目目录树

Tree对象代码及结果

查看 tree 对象

git cat-file -p main^{tree}

输出示例:

100644 blob a906cb2a4a904a152e80877d4088654daad0c859    README.md
100644 blob 8f94139338f9404f26296befa88755fc2598c289    config.js
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0    src

字段说明

字段

说明

100644

文件模式(普通文件)

100755

可执行文件

040000

目录

120000

符号链接

blob/tree

对象类型

SHA-1

对象哈希值

文件名

文件或目录名


🔹 3. Commit 对象 - 给整个项目拍「带说明的快照」

  • 核心作用:当你执行git commit时,Git 会先给当前所有文件生成 Tree(根目录的总 Tree),然后 Commit 对象就像快照的 “说明书”,把这次快照的所有信息打包

  • Commit 里存的核心信息(全是关键):

(1)指向根 Tree 的哈希(通过这个 Tree,能还原这次提交的完整项目目录 + 所有文件);

(2)指向父 Commit 的哈希(上一次快照的说明书,串起所有历史版本,能回滚、看git log);

(3)提交人、时间、提交备注(就是你-m写的话,比如 “新增登录功能”)

你用git log看到的每一行记录,就是一个 Commit 对象!

  • 关联操作:执行git commit,Git 会先生成当前工作区的根 Tree,再基于根 Tree 创建 Commit 对象

Commit对象代码及结果

查看 commit 对象

git cat-file -p HEAD

输出示例:

tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
parent 1a2b3c4d5e6f7890abcdef1234567890abcdef12
author mario <mario@123.top> 1706234567 +0800
committer mario <mario@123.top> 1706234567 +0800

修复安全漏洞

字段说明:

字段

说明

tree

指向项目根目录的 tree 对象

parent

父提交(可以有多个,合并提交)

author

代码的原始作者

committer

执行提交的人

时间戳

Unix 时间戳 + 时区

🔹 4. Tag 对象 - 版本的 “永久书签”

  • 核心作用Commit 对象是哈希值(一串乱码,比如 a1b2c3),记不住,Tag 就是给某个重要的 Commit起个好记的名字,比如v1.0v2.1-beta,方便快速找到

  • 两种类型

(1)轻量标签:仅存 “标签名 + 目标 Commit 哈希”(简单快捷,git tag v1.0);

(2)附注标签:存完整信息(含说明,git tag -a v1.0 -m "v1.0正式版",推荐打正式版本用)

  • 关联操作git tag 标签名 提交哈希给指定版本打标签),git checkout 标签名切换到标签版本)

Tag对象代码及结果

附注标签(Annotated Tag)会创建一个 tag 对象:

git cat-file -p v1.0

输出示例:

object 1a2b3c4d5e6f7890abcdef1234567890abcdef12
type commit
tag v1.0
tagger evalEvil <evalevil@example.com> 1706234567 +0800

版本 1.0 发布

5.对象存储机制

Git 把四大基础对象(Blob/Tree/Commit/Tag)存到 .git/objects/ 里,全程围绕内容寻址+分块压缩+哈希命名,既保证唯一性、又省空间,还能快速查找

1.完整的存储流程

2.手动验证对象哈希

# 计算文件的 Git 哈希值
git hash-object filename.txt

# 不写入数据库,只计算哈希
echo "Hello, Git" | git hash-object --stdin

使用 Python 验证

import hashlib
import zlib

content = b"Hello, Git\n"
header = f"blob {len(content)}\0".encode()
store = header + content
sha1 = hashlib.sha1(store).hexdigest()
print(sha1)  # b7aec520dec0a7516c18eb4c68b64ae1eb9b5a5e

3.松散对象与打包对象

松散对象(Loose Objects)

每个对象单独存储为一个文件,位于 .git/objects/[前2位]/[后38位]

打包对象(Packed Objects)

为了节省空间,Git 会定期将松散对象打包成 .pack 文件:

.git/objects/pack/
├── pack-abc123...def.idx    # 索引文件
└── pack-abc123...def.pack   # 打包文件

手动打包

git gc

查看打包内容

git verify-pack -v .git/objects/pack/pack-*.idx

6.引用系统

Git 引用系统想象成你手机里的「联系人通讯录」—— 哈希值是一串难记的手机号(比如 138xxxx1234),引用就是联系人名字(比如「张三」),核心就是用 “好记的名字” 代替 “难记的哈希号”,所有操作都是在 “查通讯录、改通讯录、标记当前要联系的人”,所有引用相关的文件,都在项目 .git 文件夹里

类核心引用:就是通讯录的 3 个「分组」

Git 把不同用途的 “名字 - 哈希映射” 分成 3 个分组,全在 .git/refs/ 文件夹下,就像通讯录里的「家人、同事、客户」分组,分工明确,找起来方便

1. 分支引用:.git/refs/heads/ → 通讯录「常用好友组」

  • 作用给分支名(比如 main、dev)绑定「该分支最新版本的哈希值」

  • 类比常用好友组里,「张三」对应手机号 138xxxx1234,「李四」对应 139xxxx5678

  • 实际样子

(1)有个 dev 分支,就会有个文件 .git/refs/heads/dev

(2)这个文件里只有一行字:比如a823f80(dev 分支最新版本的哈希值)

  • 关键操作你在 dev 分支执行git commit提交代码,Git 会自动更新这个文件 —— 把旧哈希删掉,换成新生成的版本哈希,就像 “张三换手机号了,通讯录里直接更新号码”

分支引用代码块

分支存储在 .git/refs/heads/ 目录下:

cat .git/refs/heads/main

输出:

1a2b3c4d5e6f7890abcdef1234567890abcdef12

每个分支文件只包含一个 40 位的 SHA-1 哈希值,指向该分支的最新提交

2. 标签引用:.git/refs/tags/ → 通讯录「重要联系人组」

  • 作用给标签名(比如 v1.0、v2.1)绑定「重要版本的哈希值」(一般是正式版)

  • 类比重要联系人组里,「客户王总」对应 136xxxx9999,一旦存好就不随便改

  • 实际样子

(1)打了个 v1.0 标签,就会有个文件 .git/refs/tags/v1.0

(2)文件里只写一行:正式版的哈希值,比如b945g71

  • 关键特点静态不变化—— 创建后不会自动改哈希,除非你手动删了重打,就像重要联系人的手机号一般不会变,适合标记固定的正式版本

标签引用代码块

标签存储在 .git/refs/tags/ 目录下:

cat .git/refs/tags/v1.0

3. 远程引用:.git/refs/remotes/ → 通讯录「微信好友组(缓存版)」

  • 作用记录「你最后一次和远程仓库(比如 GitHub 的 origin)同步时,远程分支的哈希值」

  • 类比你把微信好友的手机号存到本地通讯录,这个通讯录就是 “缓存版”—— 好友换手机号,你不手动更新,本地就还是旧号码

  • 实际样子

(1)远程仓库叫 origin,有个 main 分支,就会有文件 .git/refs/remotes/origin/main

(2)文件里写着:你上次git pull/fetch时,远程 origin/main 分支的哈希值

  • 关键特点:不会自动更新, 只有执行git fetch/pull/push,Git 才会去远程仓库查最新哈希,然后更新这个文件,就像 “手动刷新微信好友资料,更新本地通讯录”

远程引用代码块

远程分支存储在 .git/refs/remotes/ 目录下:

.git/refs/remotes/
└── origin/
    ├── HEAD
    └── main

HEAD 指针:就是通讯录里「星标当前联系人」

HEAD 不是上面的 “分组文件”,但却是引用系统的核心关键—— 它的作用就是标记 “你现在正在操作哪个引用(哪个名字)”,就像你在通讯录里给「张三」标星,告诉自己 “现在要和张三聊天,所有操作都针对他”

HEAD代码块:

HEAD 文件指向当前分支:

cat .git/HEAD

输出:

ref: refs/heads/dev

如果处于分离 HEAD 状态(直接指向某个提交):

1a2b3c4d5e6f7890abcdef1234567890abcdef12

1. HEAD 的核心样子:永远只写一句话

日常 99% 的情况,HEAD 文件(.git/HEAD)里的内容都是:ref: refs/heads/分支名比如:ref: refs/heads/dev → 意思是「我现在标星的是 dev 分支,所有操作都基于 dev」

2. 切换分支的本质:就是「改星标」

执行git checkout dev(切换到 dev 分支),Git 干的唯一事就是修改 HEAD 文件:把里面的内容从原来的ref: refs/heads/main,改成ref: refs/heads/dev—— 就像把星标从「张三」移到「李四」,告诉 Git “我现在要操作李四了”

3. Git 怎么找到最终版本?(星标→查分组→拿号码)

当你标星 dev 分支后,Git 会按这个步骤找对应版本,一步都不会错:HEAD(标星dev) → 去refs/heads/找 dev 文件 → 拿到里面的哈希值 → 用哈希找到对应的版本内容。类比:星标张三 → 去常用好友组找张三 → 拿到他的手机号 → 用手机号打电话

refs/logs/ 日志:就是通讯录的「修改记录」

你每次修改通讯录(比如更新张三手机号、把星标从张三移到李四、刷新远程好友号码),都会自动记一笔记录 —— 这就是 .git/refs/logs/ 的作用,记录所有引用和 HEAD 的每一次变化

  • 类比:通讯录里的「修改日志」:2026.1.28 把张三手机号从 138xxxx1234 改成 138xxxx4321;2026.1.28 星标从张三切换到李四

  • 关键命令:git reflog → 就是直接查看这份修改日志,哪怕你误删了分支、误切换了版本,只要看这份日志,就能找到之前的 “名字” 和 “哈希号”,把版本恢复回来 —— 相当于 “通讯录删了张三,看修改日志找到他原来的手机号,重新加回来”

你每次修改通讯录(比如更新张三手机号、把星标从张三移到李四、刷新远程好友号码),都会自动记一笔记录 —— 这就是 .git/refs/logs/ 的作用,记录所有引用和 HEAD 的每一次变化

  • 类比:通讯录里的「修改日志」:2026.1.28 把张三手机号从 138xxxx1234 改成 138xxxx4321;2026.1.28 星标从张三切换到李四

  • 关键命令:git reflog → 就是直接查看这份修改日志,哪怕你误删了分支、误切换了版本,只要看这份日志,就能找到之前的 “名字” 和 “哈希号”,把版本恢复回来 —— 相当于 “通讯录删了张三,看修改日志找到他原来的手机号,重新加回来”

引用日志代码块(Reflog)

Git 会记录引用的变更历史,存储在 .git/logs/ 目录下:

cat .git/logs/HEAD

输出示例:

0000000... 1a2b3c4... evalEvil <email> 1706234567 +0800	commit (initial): 初始提交
1a2b3c4... 2b3c4d5... evalEvil <email> 1706234600 +0800	commit: 添加功能

查看引用日志

git reflog

安全提示:即使使用 git reset --hard 删除了提交,通过 reflog 仍然可以找回。这对于数据取证非常有用。


7 暂存区(Index)

1 index 文件

暂存区的数据存储在 .git/index 文件中,这是一个二进制文件。

查看暂存区内容

git ls-files --stage

输出示例:

100644 a906cb2a4a904a152e80877d4088654daad0c859 0	README.md
100644 8f94139338f9404f26296befa88755fc2598c289 0	config.js

字段说明

字段

说明

100644

文件模式

SHA-1

文件内容的哈希值

0

暂存槽位(用于合并冲突)

文件名

文件路径

2 暂存槽位

在合并冲突时,index 会使用多个槽位:

槽位

说明

0

正常状态

1

共同祖先版本

2

当前分支版本(ours)

3

合并分支版本(theirs)


8 配置文件

1 仓库配置

.git/config 文件存储仓库级配置:

[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
[remote "origin"]
    url = https://github.com/username/repo.git
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
    remote = origin
    merge = refs/heads/main
[user]
    name = evalEvil
    email = evalevil@example.com

安全提示:配置文件可能包含敏感信息,如:

  • 远程仓库地址(可能暴露内部系统)

  • 用户身份信息

  • 代理配置

2 info/exclude

.git/info/exclude 文件用于本地忽略规则,不会被提交到仓库:

# 本地忽略规则
my-local-notes.txt
*.local

9 钩子脚本

1 钩子系统的设计原理

为什么 Git 需要钩子系统?

Git 的钩子系统是一种事件驱动的扩展机制,允许用户在 Git 操作的关键节点插入自定义逻辑。这种设计体现了 Unix 哲学:

设计原则

在钩子系统中的体现

可组合性

钩子是独立的脚本,可以用任何语言编写

透明性

钩子的存在不影响 Git 的核心功能

可选性

钩子默认不启用,用户按需配置

钩子的执行机制

  • 如果钩子不存在:直接完成提交

  • 如果退出码为 0:继续提交流程

  • 如果退出码非 0:中止提交,显示错误

为什么钩子不会被提交到仓库?

.git/hooks/ 目录位于 .git/ 内部,而 .git/ 目录本身不会被版本控制。这是一个安全设计

  • 防止恶意仓库通过钩子执行任意代码

  • 允许不同开发者使用不同的本地钩子

  • 服务器端钩子和客户端钩子可以独立配置

团队共享钩子的最佳实践

# 将钩子脚本放在项目目录中
mkdir -p .githooks
cp pre-commit .githooks/

# 配置 Git 使用自定义钩子目录
git config core.hooksPath .githooks

2 hooks 目录

.git/hooks/ 目录包含 Git 钩子脚本:

.git/hooks/
├── applypatch-msg.sample
├── commit-msg.sample
├── post-update.sample
├── pre-applypatch.sample
├── pre-commit.sample
├── pre-push.sample
├── pre-rebase.sample
├── prepare-commit-msg.sample
└── update.sample

启用钩子:去掉 .sample 后缀并添加执行权限。

3 常用钩子

钩子

触发时机

用途

pre-commit

提交前

代码检查、格式化

commit-msg

编辑提交信息后

验证提交信息格式

pre-push

推送前

运行测试

post-receive

服务器接收推送后

自动部署

4 安全相关的钩子应用

pre-commit 钩子示例(检查敏感信息):

#!/bin/bash
# .git/hooks/pre-commit

# 检查是否包含敏感信息
if git diff --cached | grep -iE "(password|secret|api_key|private_key)" > /dev/null; then
    echo "警告:检测到可能的敏感信息!"
    echo "请确认是否要提交这些内容。"
    exit 1
fi

exit 0

10 从 .git 目录恢复源代码

这是安全工作中的重要技能——当发现暴露的 .git 目录时,如何恢复完整源代码。

1 基本恢复方法

如果有完整的 .git 目录:

# 进入包含 .git 的目录
cd /path/to/exposed/.git/..

# 恢复工作目录
git checkout .

2 从对象数据库恢复

如果只有 objects 目录:

# 查看所有对象
find .git/objects -type f | while read file; do
    hash=$(echo $file | sed 's/.*objects\///' | tr -d '/')
    echo "=== $hash ==="
    git cat-file -t $hash 2>/dev/null
    git cat-file -p $hash 2>/dev/null
done

3 常用取证命令

# 列出所有提交
git log --all --oneline

# 列出所有分支(包括远程)
git branch -a

# 列出所有标签
git tag

# 查看所有引用
git show-ref

# 查看 reflog
git reflog

# 搜索历史中的敏感信息
git log -p -S "password"
git log -p -S "api_key"
git log -p --all -- "*.env"

# 查看被删除的文件
git log --all --full-history -- "**/deleted-file.txt"

# 恢复被删除的文件
git checkout <commit-hash> -- path/to/file

4 使用工具自动化

GitTools(用于从暴露的 .git 目录下载和恢复):

GitTools(用于从暴露的 .git 目录下载和恢复):

# 下载暴露的 .git 目录
./gitdumper.sh https://target.com/.git/ /tmp/git-dump

# 恢复源代码
./extractor.sh /tmp/git-dump /tmp/source

truffleHog(搜索敏感信息):

trufflehog git file://./repo

11 Git内部文件结构小结

本章我们深入学习了:

  1. .git 目录结构:了解每个文件和目录的作用

  2. Git 对象模型:blob、tree、commit、tag 四种对象

  3. 对象存储机制:哈希计算、压缩存储、打包

  4. 引用系统:HEAD、分支、标签、reflog

  5. 暂存区:index 文件的结构

  6. 配置与钩子:配置文件和钩子脚本

  7. 源代码恢复:从 .git 目录恢复完整源代码

这些知识是进行 Git 相关安全工作的基础


评论