"每一次提交都是对历史的记录,每一次提交都是对未来的承诺。"
在这一章中,我们将学习如何创建提交,管理提交历史,让Git成为你的时间机器!
通过本章学习,你将能够:
- 🎯 理解提交对象的结构和组成
- 📝 实现提交创建功能
- 🔗 管理提交链和提交历史
- 🛠️ 实现
commit和log命令 - 🎉 掌握提交信息的最佳实践
提交就像时间机器的一个快照,记录了某个时刻项目的完整状态:
📸 提交对象
├── 🔑 提交ID (SHA1哈希)
├── 👤 作者信息
├── 📅 提交时间
├── 💬 提交信息
├── 🔗 父提交引用
└── 📁 文件树引用
- 提交ID:基于内容生成的唯一标识符
- 作者信息:谁创建的提交
- 提交时间:何时创建的提交
- 提交信息:描述这次更改的内容
- 父提交:指向之前的提交(形成链)
- 文件树:指向当前文件状态的快照
提交链就像一条时间线:
A ← B ← C ← D ← E
│ │ │ │ │
│ │ │ │ └── 最新提交 (HEAD)
│ │ │ └── 父提交
│ │ └── 祖父提交
│ └── 曾祖父提交
└── 初始提交 (没有父提交)
首先,让我们在internal/git/models.go中定义提交相关的结构:
package git
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
// Commit 提交对象
type Commit struct {
ID string `json:"id"`
TreeID string `json:"tree_id"`
ParentID string `json:"parent_id"`
Author Author `json:"author"`
Committer Author `json:"committer"`
Message string `json:"message"`
CreatedAt time.Time `json:"created_at"`
}
// Author 作者信息
type Author struct {
Name string `json:"name"`
Email string `json:"email"`
Time time.Time `json:"time"`
}
// Tree 文件树对象
type Tree struct {
ID string `json:"id"`
Entries []TreeEntry `json:"entries"`
CreatedAt time.Time `json:"created_at"`
}
// TreeEntry 文件树条目
type TreeEntry struct {
Path string `json:"path"`
Hash string `json:"hash"`
Size int64 `json:"size"`
IsDir bool `json:"is_dir"`
Mode string `json:"mode"`
}
// NewCommit 创建新的提交
func NewCommit(parentID, treeID, message string, author Author) *Commit {
commit := &Commit{
TreeID: treeID,
ParentID: parentID,
Author: author,
Committer: author,
Message: message,
CreatedAt: time.Now(),
}
// 生成提交ID
commit.ID = commit.generateID()
return commit
}
// generateID 生成提交ID
func (c *Commit) generateID() string {
// 创建提交内容的字符串表示
content := fmt.Sprintf("tree %s\n", c.TreeID)
if c.ParentID != "" {
content += fmt.Sprintf("parent %s\n", c.ParentID)
}
content += fmt.Sprintf("author %s <%s> %d %s\n",
c.Author.Name, c.Author.Email, c.Author.Time.Unix(), c.Author.Time.Format("-0700"))
content += fmt.Sprintf("committer %s <%s> %d %s\n",
c.Committer.Name, c.Committer.Email, c.Committer.Time.Unix(), c.Committer.Time.Format("-0700"))
content += "\n" + c.Message + "\n"
// 计算SHA1哈希
hash := sha1.Sum([]byte(content))
return hex.EncodeToString(hash[:])
}
// Save 保存提交到对象数据库
func (c *Commit) Save(repoPath string) error {
// 创建对象目录
objectDir := filepath.Join(repoPath, ".git", "objects", c.ID[:2])
if err := os.MkdirAll(objectDir, 0755); err != nil {
return fmt.Errorf("创建对象目录失败: %v", err)
}
// 保存提交对象
objectPath := filepath.Join(objectDir, c.ID[2:])
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return fmt.Errorf("序列化提交失败: %v", err)
}
return os.WriteFile(objectPath, data, 0644)
}
// Load 从对象数据库加载提交
func LoadCommit(repoPath, commitID string) (*Commit, error) {
objectPath := filepath.Join(repoPath, ".git", "objects", commitID[:2], commitID[2:])
data, err := os.ReadFile(objectPath)
if err != nil {
return nil, fmt.Errorf("读取提交对象失败: %v", err)
}
var commit Commit
if err := json.Unmarshal(data, &commit); err != nil {
return nil, fmt.Errorf("反序列化提交失败: %v", err)
}
return &commit, nil
}
// NewTree 创建新的文件树
func NewTree(entries []TreeEntry) *Tree {
tree := &Tree{
Entries: entries,
CreatedAt: time.Now(),
}
// 生成树ID
tree.ID = tree.generateID()
return tree
}
// generateID 生成树ID
func (t *Tree) generateID() string {
// 创建树内容的字符串表示
var content strings.Builder
for _, entry := range t.Entries {
mode := "100644" // 普通文件
if entry.IsDir {
mode = "40000" // 目录
}
content.WriteString(fmt.Sprintf("%s %s\t%s\n", mode, entry.Hash, entry.Path))
}
// 计算SHA1哈希
hash := sha1.Sum([]byte(content.String()))
return hex.EncodeToString(hash[:])
}
// Save 保存树到对象数据库
func (t *Tree) Save(repoPath string) error {
// 创建对象目录
objectDir := filepath.Join(repoPath, ".git", "objects", t.ID[:2])
if err := os.MkdirAll(objectDir, 0755); err != nil {
return fmt.Errorf("创建对象目录失败: %v", err)
}
// 保存树对象
objectPath := filepath.Join(objectDir, t.ID[2:])
data, err := json.MarshalIndent(t, "", " ")
if err != nil {
return fmt.Errorf("序列化树失败: %v", err)
}
return os.WriteFile(objectPath, data, 0644)
}在internal/git/repository.go中添加提交相关功能:
// CreateCommit 创建新的提交
func (r *Repository) CreateCommit(message string) (*Commit, error) {
// 1. 检查暂存区是否有文件
if len(r.StagingArea.Files) == 0 {
return nil, fmt.Errorf("暂存区为空,没有文件可以提交")
}
// 2. 创建文件树
tree, err := r.createTreeFromStaging()
if err != nil {
return nil, fmt.Errorf("创建文件树失败: %v", err)
}
// 3. 保存文件树
if err := tree.Save(r.Path); err != nil {
return nil, fmt.Errorf("保存文件树失败: %v", err)
}
// 4. 获取当前分支的最新提交
parentID := ""
if head, err := r.getHEAD(); err == nil {
parentID = head
}
// 5. 创建作者信息
author := Author{
Name: r.Config.UserName,
Email: r.Config.UserEmail,
Time: time.Now(),
}
// 6. 创建提交
commit := NewCommit(parentID, tree.ID, message, author)
// 7. 保存提交
if err := commit.Save(r.Path); err != nil {
return nil, fmt.Errorf("保存提交失败: %v", err)
}
// 8. 更新HEAD指向新提交
if err := r.updateHEAD(commit.ID); err != nil {
return nil, fmt.Errorf("更新HEAD失败: %v", err)
}
// 9. 清空暂存区
r.StagingArea.Clear()
return commit, nil
}
// createTreeFromStaging 从暂存区创建文件树
func (r *Repository) createTreeFromStaging() (*Tree, error) {
var entries []TreeEntry
for path, fileInfo := range r.StagingArea.Files {
entry := TreeEntry{
Path: path,
Hash: fileInfo.Hash,
Size: fileInfo.Size,
IsDir: fileInfo.IsDirectory,
Mode: "100644", // 普通文件权限
}
entries = append(entries, entry)
}
return NewTree(entries), nil
}
// getHEAD 获取当前HEAD指向的提交
func (r *Repository) getHEAD() (string, error) {
headPath := filepath.Join(r.Path, ".git", "HEAD")
data, err := os.ReadFile(headPath)
if err != nil {
return "", err
}
// 解析HEAD内容
headContent := strings.TrimSpace(string(data))
if strings.HasPrefix(headContent, "ref: ") {
// HEAD指向分支
refPath := strings.TrimPrefix(headContent, "ref: ")
refFile := filepath.Join(r.Path, ".git", refPath)
refData, err := os.ReadFile(refFile)
if err != nil {
return "", err
}
return strings.TrimSpace(string(refData)), nil
} else {
// HEAD直接指向提交
return headContent, nil
}
}
// updateHEAD 更新HEAD指向指定提交
func (r *Repository) updateHEAD(commitID string) error {
// 更新分支引用
refPath := filepath.Join(r.Path, ".git", "refs", "heads", r.CurrentBranch)
if err := os.MkdirAll(filepath.Dir(refPath), 0755); err != nil {
return err
}
return os.WriteFile(refPath, []byte(commitID), 0644)
}
// GetCommitHistory 获取提交历史
func (r *Repository) GetCommitHistory(limit int) ([]*Commit, error) {
var commits []*Commit
// 从HEAD开始遍历
currentID, err := r.getHEAD()
if err != nil {
return commits, nil // 没有提交历史
}
count := 0
for currentID != "" && count < limit {
commit, err := LoadCommit(r.Path, currentID)
if err != nil {
return nil, fmt.Errorf("加载提交失败: %v", err)
}
commits = append(commits, commit)
currentID = commit.ParentID
count++
}
return commits, nil
}
// GetCommit 获取指定提交
func (r *Repository) GetCommit(commitID string) (*Commit, error) {
return LoadCommit(r.Path, commitID)
}更新cmd/commit.go:
package cmd
import (
"fmt"
"strings"
"cit/internal/git"
"github.com/spf13/cobra"
)
var (
message string
all bool
)
var commitCmd = &cobra.Command{
Use: "commit",
Short: "创建新的提交",
Long: `创建新的提交,将暂存区的更改保存到仓库历史中。
提交信息应该简洁明了地描述这次更改的内容。
使用 -m 参数提供提交信息,或使用 -a 参数自动添加所有修改的文件。
示例:
cit commit -m "添加新功能" # 提交暂存区的文件
cit commit -a -m "修复bug" # 自动添加并提交所有修改
cit commit -m "更新文档" # 提交暂存区的文件`,
RunE: func(cmd *cobra.Command, args []string) error {
// 查找仓库
repo, err := git.FindRepository(".")
if err != nil {
return fmt.Errorf("未找到Git仓库: %v", err)
}
// 检查提交信息
if message == "" {
return fmt.Errorf("必须提供提交信息,使用 -m 参数")
}
// 如果使用 -a 参数,自动添加所有修改的文件
if all {
// 获取状态
status, err := repo.GetStatus()
if err != nil {
return fmt.Errorf("获取状态失败: %v", err)
}
// 添加所有修改的文件
for _, file := range status.Modified {
if err := repo.AddFile(file); err != nil {
return fmt.Errorf("添加文件失败 %s: %v", file, err)
}
fmt.Printf("✅ 已添加: %s\n", file)
}
}
// 创建提交
commit, err := repo.CreateCommit(message)
if err != nil {
return fmt.Errorf("创建提交失败: %v", err)
}
// 显示提交信息
fmt.Printf("🎉 提交成功!\n")
fmt.Printf("📝 提交ID: %s\n", commit.ID[:8])
fmt.Printf("👤 作者: %s <%s>\n", commit.Author.Name, commit.Author.Email)
fmt.Printf("📅 时间: %s\n", commit.CreatedAt.Format("2006-01-02 15:04:05"))
fmt.Printf("💬 信息: %s\n", commit.Message)
return nil
},
}
func init() {
commitCmd.Flags().StringVarP(&message, "message", "m", "", "提交信息")
commitCmd.Flags().BoolVarP(&all, "all", "a", false, "自动添加所有修改的文件")
commitCmd.MarkFlagRequired("message")
rootCmd.AddCommand(commitCmd)
}更新cmd/log.go:
package cmd
import (
"fmt"
"strings"
"cit/internal/git"
"github.com/spf13/cobra"
)
var (
oneline bool
graph bool
limit int
)
var logCmd = &cobra.Command{
Use: "log",
Short: "显示提交历史",
Long: `显示提交历史,按时间倒序排列。
可以查看提交的详细信息,包括提交ID、作者、时间和提交信息。
使用不同的选项可以自定义显示格式。
示例:
cit log # 显示完整提交历史
cit log --oneline # 单行显示
cit log -n 5 # 只显示最近5个提交
cit log --graph # 显示提交图`,
RunE: func(cmd *cobra.Command, args []string) error {
// 查找仓库
repo, err := git.FindRepository(".")
if err != nil {
return fmt.Errorf("未找到Git仓库: %v", err)
}
// 获取提交历史
commits, err := repo.GetCommitHistory(limit)
if err != nil {
return fmt.Errorf("获取提交历史失败: %v", err)
}
if len(commits) == 0 {
fmt.Println("📝 还没有任何提交")
return nil
}
// 显示提交历史
for i, commit := range commits {
if oneline {
// 单行格式
fmt.Printf("%s %s\n", commit.ID[:8], commit.Message)
} else {
// 详细格式
if i > 0 {
fmt.Println()
}
if graph {
fmt.Printf("* ")
}
fmt.Printf("📝 提交: %s\n", commit.ID)
fmt.Printf("👤 作者: %s <%s>\n", commit.Author.Name, commit.Author.Email)
fmt.Printf("📅 时间: %s\n", commit.Author.Time.Format("2006-01-02 15:04:05"))
fmt.Printf("💬 信息: %s\n", commit.Message)
if commit.ParentID != "" {
fmt.Printf("🔗 父提交: %s\n", commit.ParentID[:8])
}
}
}
return nil
},
}
func init() {
logCmd.Flags().BoolVarP(&oneline, "oneline", "", false, "单行显示")
logCmd.Flags().BoolVarP(&graph, "graph", "", false, "显示提交图")
logCmd.Flags().IntVarP(&limit, "limit", "n", 0, "限制显示的提交数量")
rootCmd.AddCommand(logCmd)
}# 1. 创建测试仓库
mkdir commit-test
cd commit-test
cit init
# 2. 创建文件
echo "Hello, World!" > hello.txt
echo "package main" > main.go
# 3. 添加文件到暂存区
cit add hello.txt main.go
# 4. 创建第一个提交
cit commit -m "初始提交:添加Hello World程序"
# 5. 查看提交历史
cit log# 1. 修改文件
echo "func main() {" >> main.go
echo " fmt.Println(\"Hello, World!\")" >> main.go
echo "}" >> main.go
# 2. 查看状态
cit status
# 3. 自动添加并提交
cit commit -a -m "添加main函数"
# 4. 再次修改
echo "// 这是一个Go程序" > main.go
echo "package main" >> main.go
echo "" >> main.go
echo "import \"fmt\"" >> main.go
echo "" >> main.go
echo "func main() {" >> main.go
echo " fmt.Println(\"Hello, World!\")" >> main.go
echo "}" >> main.go
# 5. 提交更改
cit add main.go
cit commit -m "重构代码:添加import语句"
# 6. 查看详细历史
cit log --oneline-
为什么提交ID是唯一的?
- 提示:想想提交ID是如何生成的?
-
提交链的作用是什么?
- 提示:为什么每个提交都要指向父提交?
-
如何撤销一个提交?
- 提示:想想时间机器可以回到过去吗?
Git有四种基本对象类型:
Git对象类型:
├── 📝 Blob (文件内容)
├── 🌳 Tree (目录结构)
├── 📋 Commit (提交信息)
└── 🏷️ Tag (标签)
提交信息格式:
<类型>(<范围>): <简短描述>
<详细描述>
<页脚>
示例:
feat(auth): 添加用户登录功能
- 实现用户名密码登录
- 添加JWT token验证
- 集成Redis缓存
Closes #123
提交图示例:
* a1b2c3d4 最新提交
* e5f6g7h8 父提交
* i9j0k1l2 祖父提交
* m3n4o5p6 初始提交
恭喜!你已经成功实现了提交系统。让我们回顾一下学到的内容:
- ✅ 提交对象创建和管理
- ✅ 文件树对象系统
- ✅
commit命令实现 - ✅
log命令实现 - ✅ 提交历史遍历
- ✅ HEAD引用管理
- 提交对象的结构
- 文件树的作用
- 提交链的形成
- 提交信息的规范
在下一章中,我们将学习分支系统,包括:
- 分支创建和管理
- 分支切换
- 分支合并
- 冲突解决
💡 小贴士:每次提交都应该有明确的目的和清晰的描述。好的提交信息就像给未来的自己写的一封信,让代码历史变得有意义!
准备好继续下一章的冒险了吗?让我们一起去探索分支系统的奥秘! 🚀