功能补齐、查看git提交历史

This commit is contained in:
2026-06-12 09:21:37 +08:00
parent fba3a14367
commit 2a9661a6ef
12 changed files with 329 additions and 9 deletions
+61
View File
@@ -0,0 +1,61 @@
package git
import (
"context"
"strconv"
"strings"
"time"
)
// Commit 表示一条 git log 记录。
type Commit struct {
Hash string
Author string
Email string
Date time.Time
Subject string
}
// Log 返回 dir 的最近 n 条提交历史。n <= 0 时默认 50 条。
func (r *DefaultRunner) Log(ctx context.Context, dir string, n int) ([]Commit, error) {
ctx, cancel := withCtx(ctx, r.cfg.OpsTimeout)
defer cancel()
if n <= 0 {
n = 50
}
out, _, err := r.exec(ctx, dir, nil, "log", "--format=%H%x00%an%x00%ae%x00%aI%x00%s%x00", "-n", strconv.Itoa(n))
if err != nil {
return nil, err
}
return parseLog(out), nil
}
func parseLog(buf []byte) []Commit {
if len(buf) == 0 {
return nil
}
parts := strings.Split(string(buf), "\x00")
var commits []Commit
// parts 以空字符串结尾(因为 format 最后有 %x00),所以每 5 个字段为一组。
for i := 0; i+4 < len(parts); i += 5 {
hash := parts[i]
if hash == "" {
continue
}
author := parts[i+1]
email := parts[i+2]
dateStr := parts[i+3]
subject := parts[i+4]
date, _ := time.Parse(time.RFC3339, dateStr)
commits = append(commits, Commit{
Hash: hash,
Author: author,
Email: email,
Date: date,
Subject: subject,
})
}
return commits
}
+1
View File
@@ -33,6 +33,7 @@ type Runner interface {
WorktreeList(ctx context.Context, dir string) ([]Worktree, error)
ListRemoteBranches(ctx context.Context, dir string, cred *Credential) ([]string, error)
Checkout(ctx context.Context, dir, branch string) error
Log(ctx context.Context, dir string, n int) ([]Commit, error)
}
// FileStatus 表示 `git status --porcelain=v1` 的一行。
+8
View File
@@ -105,6 +105,14 @@ func (f *fakeGitOpsSvc) PushOnWorktree(_ context.Context, _ workspace.Caller, wt
f.lastTarget = "worktree:" + wtID.String()
return nil
}
func (f *fakeGitOpsSvc) LogOnMain(_ context.Context, _ workspace.Caller, wsID uuid.UUID, _ int) ([]git.Commit, error) {
f.lastTarget = "main:" + wsID.String()
return nil, nil
}
func (f *fakeGitOpsSvc) LogOnWorktree(_ context.Context, _ workspace.Caller, wtID uuid.UUID, _ int) ([]git.Commit, error) {
f.lastTarget = "worktree:" + wtID.String()
return nil, nil
}
// wsTestDeps 在 testDeps 基础上挂一个已存在的 workspace + worktree 服务。
func wsTestDeps() (ServerDeps, *fakeWorkspaceSvc, *fakeWorktreeSvc, *fakeGitOpsSvc) {
+8 -6
View File
@@ -77,12 +77,12 @@ type Workspace struct {
// Worktree 表示一个支线 worktree(不含 main_path)。
type Worktree struct {
ID uuid.UUID
WorkspaceID uuid.UUID
Branch string
Path string
Status WorktreeStatus
ActiveHolder string // "user:<uid>" / "session:<sid>" / 空
ID uuid.UUID
WorkspaceID uuid.UUID
Branch string
Path string
Status WorktreeStatus
ActiveHolder string // "user:<uid>" / "session:<sid>" / 空
AcquiredAt *time.Time
LastUsedAt time.Time
PruneWarningAt *time.Time
@@ -167,10 +167,12 @@ type GitOpsService interface {
StatusOnMain(ctx context.Context, c Caller, wsID uuid.UUID) ([]git.FileStatus, error)
CommitOnMain(ctx context.Context, c Caller, wsID uuid.UUID, in CommitInput) (string, error)
PushOnMain(ctx context.Context, c Caller, wsID uuid.UUID) error
LogOnMain(ctx context.Context, c Caller, wsID uuid.UUID, n int) ([]git.Commit, error)
StatusOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID) ([]git.FileStatus, error)
CommitOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID, in CommitInput) (string, error)
PushOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID) error
LogOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID, n int) ([]git.Commit, error)
}
// ===== Input DTO =====
+12
View File
@@ -119,3 +119,15 @@ type commitResp struct {
type branchListResp struct {
Branches []string `json:"branches"`
}
type commitLogResp struct {
Hash string `json:"hash"`
Author string `json:"author"`
Email string `json:"email"`
Date time.Time `json:"date"`
Subject string `json:"subject"`
}
func toCommitLogResp(c git.Commit) commitLogResp {
return commitLogResp{Hash: c.Hash, Author: c.Author, Email: c.Email, Date: c.Date, Subject: c.Subject}
}
+24
View File
@@ -79,6 +79,18 @@ func (s *gitOpsService) PushOnMain(ctx context.Context, c Caller, wsID uuid.UUID
return nil
}
func (s *gitOpsService) LogOnMain(ctx context.Context, c Caller, wsID uuid.UUID, n int) ([]git.Commit, error) {
ws, err := s.guardWorkspaceRead(ctx, c, wsID)
if err != nil {
return nil, err
}
commits, err := s.gitr.Log(ctx, ws.MainPath, n)
if err != nil {
return nil, errs.Wrap(err, errs.CodeGitCmdFailed, "git log")
}
return commits, nil
}
// ===== Worktree =====
func (s *gitOpsService) StatusOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID) ([]git.FileStatus, error) {
@@ -133,6 +145,18 @@ func (s *gitOpsService) PushOnWorktree(ctx context.Context, c Caller, wtID uuid.
return nil
}
func (s *gitOpsService) LogOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID, n int) ([]git.Commit, error) {
wt, _, err := s.guardWorktreeRead(ctx, c, wtID)
if err != nil {
return nil, err
}
commits, err := s.gitr.Log(ctx, wt.Path, n)
if err != nil {
return nil, errs.Wrap(err, errs.CodeGitCmdFailed, "git log")
}
return commits, nil
}
// ===== guards =====
func (s *gitOpsService) guardWorkspaceRead(ctx context.Context, c Caller, wsID uuid.UUID) (*Workspace, error) {
+59
View File
@@ -10,6 +10,7 @@ import (
"encoding/json"
"errors"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
@@ -59,6 +60,7 @@ func (h *Handler) Mount(r chi.Router) {
r.Get("/main/status", h.statusOnMain)
r.Post("/main/commit", h.commitOnMain)
r.Post("/main/push", h.pushOnMain)
r.Get("/main/log", h.logOnMain)
r.Get("/worktrees", h.listWorktrees)
r.Post("/worktrees", h.createWorktree)
@@ -71,6 +73,7 @@ func (h *Handler) Mount(r chi.Router) {
r.Get("/status", h.statusOnWorktree)
r.Post("/commit", h.commitOnWorktree)
r.Post("/push", h.pushOnWorktree)
r.Get("/log", h.logOnWorktree)
})
}
@@ -536,3 +539,59 @@ func (h *Handler) pushGeneric(w http.ResponseWriter, r *http.Request, onMain boo
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) logOnMain(w http.ResponseWriter, r *http.Request) {
h.logGeneric(w, r, true)
}
func (h *Handler) logOnWorktree(w http.ResponseWriter, r *http.Request) {
h.logGeneric(w, r, false)
}
func (h *Handler) logGeneric(w http.ResponseWriter, r *http.Request, onMain bool) {
c, err := h.caller(r)
if err != nil {
writeErr(w, r, err)
return
}
param := "wsID"
if !onMain {
param = "wtID"
}
id, err := parseUUID(chi.URLParam(r, param))
if err != nil {
writeErr(w, r, err)
return
}
n := parseLogN(r)
var commits []git.Commit
if onMain {
commits, err = h.gop.LogOnMain(r.Context(), c, id, n)
} else {
commits, err = h.gop.LogOnWorktree(r.Context(), c, id, n)
}
if err != nil {
writeErr(w, r, err)
return
}
resp := make([]commitLogResp, 0, len(commits))
for _, cc := range commits {
resp = append(resp, toCommitLogResp(cc))
}
writeJSON(w, http.StatusOK, resp)
}
func parseLogN(r *http.Request) int {
nStr := r.URL.Query().Get("n")
if nStr == "" {
return 50
}
n, err := strconv.Atoi(nStr)
if err != nil || n <= 0 {
return 50
}
if n > 200 {
return 200
}
return n
}
+5 -2
View File
@@ -226,11 +226,14 @@ func (f *fakeGit) WorktreeRemove(_ context.Context, _, _ string) error
func (f *fakeGit) WorktreeList(_ context.Context, _ string) ([]git.Worktree, error) {
return nil, nil
}
func (f *fakeGit) FetchRemoteBranch(_ context.Context, _, _ string, _ *git.Credential) error { return nil }
func (f *fakeGit) Checkout(_ context.Context, _, _ string) error { return nil }
func (f *fakeGit) FetchRemoteBranch(_ context.Context, _, _ string, _ *git.Credential) error {
return nil
}
func (f *fakeGit) Checkout(_ context.Context, _, _ string) error { return nil }
func (f *fakeGit) ListRemoteBranches(_ context.Context, _ string, _ *git.Credential) ([]string, error) {
return nil, nil
}
func (f *fakeGit) Log(_ context.Context, _ string, _ int) ([]git.Commit, error) { return nil, nil }
// trackGit 包装 git.Runner,记录 FetchRemoteBranch / Checkout 调用以供断言。
// 嵌入 Runner 接口本身可避免为每个方法重复转发。