Skip to content

Latest commit

 

History

History
684 lines (546 loc) · 17.6 KB

07-commit-system.md

File metadata and controls

684 lines (546 loc) · 17.6 KB

第7章:提交系统 📝

"每一次提交都是对历史的记录,每一次提交都是对未来的承诺。"

在这一章中,我们将学习如何创建提交,管理提交历史,让Git成为你的时间机器!

📚 本章学习目标

通过本章学习,你将能够:

  • 🎯 理解提交对象的结构和组成
  • 📝 实现提交创建功能
  • 🔗 管理提交链和提交历史
  • 🛠️ 实现commit和log命令
  • 🎉 掌握提交信息的最佳实践

🧠 核心概念

什么是提交(Commit)?

提交就像时间机器的一个快照,记录了某个时刻项目的完整状态:

📸 提交对象
├── 🔑 提交ID (SHA1哈希)
├── 👤 作者信息
├── 📅 提交时间
├── 💬 提交信息
├── 🔗 父提交引用
└── 📁 文件树引用

提交的组成部分

  1. 提交ID:基于内容生成的唯一标识符
  2. 作者信息:谁创建的提交
  3. 提交时间:何时创建的提交
  4. 提交信息:描述这次更改的内容
  5. 父提交:指向之前的提交(形成链)
  6. 文件树:指向当前文件状态的快照

提交链结构

提交链就像一条时间线:
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)
}

第三步:实现commit命令

更新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)
}

第四步:实现log命令

更新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:创建第一个提交

# 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

练习2:创建多个提交

# 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

🤔 思考题

  1. 为什么提交ID是唯一的?

    • 提示:想想提交ID是如何生成的?
  2. 提交链的作用是什么?

    • 提示:为什么每个提交都要指向父提交?
  3. 如何撤销一个提交?

    • 提示:想想时间机器可以回到过去吗?

🔍 深入理解

Git对象类型

Git有四种基本对象类型:

Git对象类型:
├── 📝 Blob (文件内容)
├── 🌳 Tree (目录结构)
├── 📋 Commit (提交信息)
└── 🏷️ Tag (标签)

提交信息最佳实践

提交信息格式:
<类型>(<范围>): <简短描述>

<详细描述>

<页脚>

示例:
feat(auth): 添加用户登录功能

- 实现用户名密码登录
- 添加JWT token验证
- 集成Redis缓存

Closes #123

提交历史可视化

提交图示例:
* a1b2c3d4 最新提交
* e5f6g7h8 父提交
* i9j0k1l2 祖父提交
* m3n4o5p6 初始提交

🎯 本章小结

恭喜!你已经成功实现了提交系统。让我们回顾一下学到的内容:

✅ 完成的功能

  • ✅ 提交对象创建和管理
  • ✅ 文件树对象系统
  • ✅ commit命令实现
  • ✅ log命令实现
  • ✅ 提交历史遍历
  • ✅ HEAD引用管理

🧠 掌握的概念

  • 提交对象的结构
  • 文件树的作用
  • 提交链的形成
  • 提交信息的规范

🚀 下一步

在下一章中,我们将学习分支系统,包括:

  • 分支创建和管理
  • 分支切换
  • 分支合并
  • 冲突解决

💡 小贴士:每次提交都应该有明确的目的和清晰的描述。好的提交信息就像给未来的自己写的一封信,让代码历史变得有意义!

准备好继续下一章的冒险了吗?让我们一起去探索分支系统的奥秘! 🚀