git-sync-web 自动提交 2026-09-09 18:02:17
This commit is contained in:
@@ -131,9 +131,114 @@ func NewEngine(base string) (*Engine, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
_ = e.ensureSummariesDir()
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (e *Engine) summariesDir() string {
|
||||
return filepath.Join(e.baseDir, "summaries")
|
||||
}
|
||||
|
||||
// ensureSummariesDir 创建本地变更摘要目录(gitignored,不同步到远程)。
|
||||
func (e *Engine) ensureSummariesDir() error {
|
||||
dir := e.summariesDir()
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
gi := filepath.Join(dir, ".gitignore")
|
||||
if _, err := os.Stat(gi); err != nil {
|
||||
_ = os.WriteFile(gi, []byte("*\n"), 0o644)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) summaryPath(name string) string {
|
||||
return filepath.Join(e.summariesDir(), name+".md")
|
||||
}
|
||||
|
||||
func (e *Engine) pendingPath(name string) string {
|
||||
return filepath.Join(e.summariesDir(), name+".pending")
|
||||
}
|
||||
|
||||
func (e *Engine) GetPendingSummary(name string) string {
|
||||
b, err := os.ReadFile(e.pendingPath(name))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
|
||||
func (e *Engine) SavePendingSummary(name, text string) error {
|
||||
if err := e.ensureSummariesDir(); err != nil {
|
||||
return err
|
||||
}
|
||||
t := strings.TrimSpace(text)
|
||||
if t == "" {
|
||||
return os.Remove(e.pendingPath(name))
|
||||
}
|
||||
return os.WriteFile(e.pendingPath(name), []byte(t+"\n"), 0o644)
|
||||
}
|
||||
|
||||
// autoSummary 在没有任何人工摘要时,依据本次改动自动生成一句话摘要(仅本地记录用)。
|
||||
func (e *Engine) autoSummary(repoPath string) string {
|
||||
out, err := gitRun(repoPath, 15*time.Second, "status", "--porcelain")
|
||||
if err != nil {
|
||||
return "自动提交"
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(out), "\n")
|
||||
var files []string
|
||||
for _, ln := range lines {
|
||||
ln = strings.TrimSpace(ln)
|
||||
if ln == "" {
|
||||
continue
|
||||
}
|
||||
p := strings.TrimSpace(ln[3:])
|
||||
if i := strings.Index(p, "->"); i >= 0 {
|
||||
p = strings.TrimSpace(p[i+2:])
|
||||
}
|
||||
p = strings.Trim(p, `"`)
|
||||
if p != "" {
|
||||
files = append(files, p)
|
||||
}
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return "自动提交(无改动文件)"
|
||||
}
|
||||
const max = 6
|
||||
shown := files
|
||||
if len(shown) > max {
|
||||
shown = files[:max]
|
||||
}
|
||||
more := ""
|
||||
if len(files) > max {
|
||||
more = fmt.Sprintf(" 等%d个", len(files))
|
||||
}
|
||||
return fmt.Sprintf("改动 %d 个文件:%s%s", len(files), strings.Join(shown, ", "), more)
|
||||
}
|
||||
|
||||
// RecordCommit 把一次提交(commit id + 摘要)追加写入本地按项目分开的摘要文件(不上传)。
|
||||
func (e *Engine) RecordCommit(name, sha, summary string) {
|
||||
if err := e.ensureSummariesDir(); err != nil {
|
||||
return
|
||||
}
|
||||
ts := time.Now().Format("2006-01-02 15:04:05")
|
||||
entry := fmt.Sprintf("## %s · %s\n%s\n\n", strings.TrimSpace(sha), ts, strings.TrimSpace(summary))
|
||||
f, err := os.OpenFile(e.summaryPath(name), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, _ = f.WriteString(entry)
|
||||
}
|
||||
|
||||
func (e *Engine) GetSummaryLog(name string) string {
|
||||
b, err := os.ReadFile(e.summaryPath(name))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (e *Engine) Bus() *EventBus { return e.bus }
|
||||
|
||||
func (e *Engine) configNames() []string {
|
||||
@@ -619,10 +724,22 @@ func (e *Engine) syncOnce(s *repoState, trigger string) {
|
||||
return
|
||||
}
|
||||
cmsg := "git-sync-web 自动提交 " + time.Now().Format("2006-01-02 15:04:05")
|
||||
pend := e.GetPendingSummary(s.cfg.Name)
|
||||
if pend != "" {
|
||||
cmsg = pend
|
||||
}
|
||||
if _, cerr := gitRun(cfg.RepoPath, 120*time.Second, "commit", "-m", cmsg); cerr != nil {
|
||||
e.setFail(s, "自动提交失败: "+cerr.Error())
|
||||
return
|
||||
}
|
||||
if sha, serr := gitRun(cfg.RepoPath, 10*time.Second, "rev-parse", "--short", "HEAD"); serr == nil {
|
||||
sum := cmsg
|
||||
if pend == "" {
|
||||
sum = e.autoSummary(cfg.RepoPath)
|
||||
}
|
||||
e.RecordCommit(s.cfg.Name, sha, sum)
|
||||
}
|
||||
_ = os.Remove(e.pendingPath(s.cfg.Name))
|
||||
e.logf(s, "info", "自动提交完成,将随本轮同步推送")
|
||||
dirty = 0
|
||||
} else {
|
||||
@@ -986,6 +1103,13 @@ func (e *Engine) ConflictResolve(name string) error {
|
||||
}
|
||||
|
||||
func (e *Engine) Create(c *RepoConfig) error {
|
||||
branch := c.Branch
|
||||
if branch == "" {
|
||||
branch = "main"
|
||||
}
|
||||
if err := ensureRepo(c.RepoPath, branch); err != nil {
|
||||
return fmt.Errorf("初始化仓库失败: %w", err)
|
||||
}
|
||||
if err := validateRepoConfig(e.cfgDir, c, true); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1006,6 +1130,13 @@ func (e *Engine) Update(c *RepoConfig) error {
|
||||
if s == nil {
|
||||
return fmt.Errorf("配置不存在: %s", c.Name)
|
||||
}
|
||||
branch := c.Branch
|
||||
if branch == "" {
|
||||
branch = "main"
|
||||
}
|
||||
if err := ensureRepo(c.RepoPath, branch); err != nil {
|
||||
return fmt.Errorf("初始化仓库失败: %w", err)
|
||||
}
|
||||
if err := validateRepoConfig(e.cfgDir, c, false); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1250,10 +1381,11 @@ func (e *Engine) FileTree(name string) []FileNode {
|
||||
}
|
||||
|
||||
type FileContent struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Binary bool `json:"binary"`
|
||||
Size int `json:"size"`
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Binary bool `json:"binary"`
|
||||
Size int `json:"size"`
|
||||
Changed []int `json:"changed,omitempty"`
|
||||
}
|
||||
|
||||
func (e *Engine) ReadFileContent(name, rel string) (*FileContent, error) {
|
||||
@@ -1282,7 +1414,106 @@ func (e *Engine) ReadFileContent(name, rel string) (*FileContent, error) {
|
||||
if bytes.Contains(data, []byte{0}) {
|
||||
return &FileContent{Path: rel, Binary: true, Size: int(info.Size())}, nil
|
||||
}
|
||||
return &FileContent{Path: rel, Content: string(data), Size: int(info.Size())}, nil
|
||||
content := string(data)
|
||||
changed, _ := fileChangeLines(s.cfg.RepoPath, rel, content)
|
||||
return &FileContent{Path: rel, Content: content, Size: int(info.Size()), Changed: changed}, nil
|
||||
}
|
||||
|
||||
func isUntracked(repoPath, rel string) bool {
|
||||
out, err := gitRun(repoPath, 10*time.Second, "status", "--porcelain", "--", rel)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "??") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fileChangeLines(repoPath, rel, content string) ([]int, error) {
|
||||
if _, err := gitRun(repoPath, 10*time.Second, "rev-parse", "--verify", "HEAD"); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if isUntracked(repoPath, rel) {
|
||||
return allLineNumbers(content), nil
|
||||
}
|
||||
out, err := gitRun(repoPath, 30*time.Second, "diff", "--no-color", "HEAD", "--", rel)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return parseDiffAddedLines(out), nil
|
||||
}
|
||||
|
||||
func allLineNumbers(content string) []int {
|
||||
n := strings.Count(content, "\n")
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
n++
|
||||
lines := make([]int, 0, n)
|
||||
for i := 1; i <= n; i++ {
|
||||
lines = append(lines, i)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func parseDiffAddedLines(diff string) []int {
|
||||
set := map[int]struct{}{}
|
||||
newLine := 0
|
||||
for _, raw := range strings.Split(diff, "\n") {
|
||||
line := raw
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
if i := strings.Index(line, " +"); i >= 0 {
|
||||
rest := line[i+2:]
|
||||
if c, ok := parseHunkNewStart(rest); ok {
|
||||
newLine = c
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if newLine == 0 {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(line, "+"):
|
||||
if strings.HasPrefix(line, "+++") {
|
||||
continue
|
||||
}
|
||||
set[newLine] = struct{}{}
|
||||
newLine++
|
||||
case strings.HasPrefix(line, " "):
|
||||
newLine++
|
||||
case strings.HasPrefix(line, "-"):
|
||||
default:
|
||||
}
|
||||
}
|
||||
out := make([]int, 0, len(set))
|
||||
for k := range set {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Ints(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func parseHunkNewStart(rest string) (int, bool) {
|
||||
end := strings.IndexAny(rest, " @")
|
||||
if end >= 0 {
|
||||
rest = rest[:end]
|
||||
}
|
||||
if i := strings.Index(rest, ","); i >= 0 {
|
||||
rest = rest[:i]
|
||||
}
|
||||
n := strings.TrimSpace(rest)
|
||||
if n == "" {
|
||||
return 0, false
|
||||
}
|
||||
v, err := strconv.Atoi(n)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
|
||||
func (e *Engine) WriteFileContent(name, rel, content string) error {
|
||||
@@ -1323,6 +1554,9 @@ func (e *Engine) CommitFile(name, rel, message string) error {
|
||||
if _, err := gitRun(s.cfg.RepoPath, 60*time.Second, "commit", "-m", message, "--", rel); err != nil {
|
||||
return fmt.Errorf("git commit 失败: %w", err)
|
||||
}
|
||||
if sha, serr := gitRun(s.cfg.RepoPath, 10*time.Second, "rev-parse", "--short", "HEAD"); serr == nil {
|
||||
e.RecordCommit(name, sha, message)
|
||||
}
|
||||
e.logf(s, "info", "网页端提交 %s 成功", rel)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user