feat: add git pull to update cached repositories from remote
Deploy PR-Helper / deploy (push) Successful in 34s

- Replace dead FetchRemote with PullRepo (fetch + merge) in services/git.go
- Add POST /api/repos/:id/pull endpoint with DB size/last_used update
- Add '更新' button next to each cached repo in the index page
This commit is contained in:
2026-06-21 14:31:33 +08:00
parent 25291a81a3
commit 20be99b287
4 changed files with 85 additions and 11 deletions
+35
View File
@@ -140,6 +140,41 @@ func (h *ReposHandler) CleanupRepos(c *gin.Context) {
c.JSON(http.StatusOK, result)
}
// PullRepo handles POST /api/repos/:id/pull — fetches and merges latest changes.
func (h *ReposHandler) PullRepo(c *gin.Context) {
user := GetCurrentUser(c)
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthenticated"})
return
}
id := c.Param("id")
var localPath string
err := h.db.QueryRow(`SELECT local_path FROM repositories WHERE id = ? AND user_id = ?`, id, user.ID).Scan(&localPath)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "repository not found"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
result, err := services.PullRepo(localPath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.db.Exec(`UPDATE repositories SET size_bytes = ?, last_used = NOW() WHERE id = ?`, result.SizeBytes, id)
c.JSON(http.StatusOK, gin.H{
"ok": true,
"repo_id": id,
"size_bytes": result.SizeBytes,
})
}
// CloneRepo handles POST /api/repos with SSE progress events.
func (h *ReposHandler) CloneRepo(c *gin.Context) {
user := GetCurrentUser(c)
+1
View File
@@ -114,6 +114,7 @@ func main() {
r.POST("/api/repos", authMw, repos.CloneRepo)
r.DELETE("/api/repos/:id", authMw, repos.DeleteRepo)
r.POST("/api/repos/:id/cleanup", authMw, repos.CleanupRepos)
r.POST("/api/repos/:id/pull", authMw, repos.PullRepo)
r.GET("/api/repos/:id/graph", authMw, repos.GetGraph)
r.GET("/api/repos/:id/refs", authMw, repos.GetRefs)
r.GET("/api/repos/:id/commits", authMw, repos.GetCommits)
+39 -7
View File
@@ -441,21 +441,53 @@ func GetBranchCommits(repoPath, branchName string, maxCommits int) ([]CommitInfo
return GetCommitLog(repo, branchName, maxCommits)
}
// FetchRemote fetches latest changes for a repo.
func FetchRemote(repoPath string) error {
// PullRepo fetches and merges latest changes from origin into the current branch.
func PullRepo(repoPath string) (*CloneResult, error) {
repo, err := OpenRepo(repoPath)
if err != nil {
return err
return nil, err
}
remote, err := repo.Remote("origin")
w, err := repo.Worktree()
if err != nil {
return err
return nil, fmt.Errorf("worktree: %w", err)
}
return remote.Fetch(&git.FetchOptions{
Force: true,
err = w.Pull(&git.PullOptions{})
if err != nil && err != git.NoErrAlreadyUpToDate {
return nil, fmt.Errorf("git pull: %w", err)
}
result := &CloneResult{RepoPath: repoPath}
iter, err := repo.CommitObjects()
if err == nil {
_ = iter.ForEach(func(c *object.Commit) error {
result.CommitNum++
return nil
})
}
branches, err := repo.Branches()
if err == nil {
_ = branches.ForEach(func(ref *plumbing.Reference) error {
result.Branches = append(result.Branches, ref.Name().Short())
return nil
})
}
tags, err := repo.Tags()
if err == nil {
_ = tags.ForEach(func(ref *plumbing.Reference) error {
result.Tags = append(result.Tags, ref.Name().Short())
return nil
})
}
result.SizeBytes = dirSize(repoPath)
return result, nil
}
// dirSize returns the total size of files in a directory.
func dirSize(path string) int64 {
var size int64
+6
View File
@@ -52,10 +52,16 @@
<a href="/repo/{{.ID}}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">{{.URL}}</a>
<p class="text-xs text-gray-400 mt-1">最后使用: {{.LastUsed}}</p>
</div>
<div class="flex gap-2">
<button hx-post="/api/repos/{{.ID}}/pull" hx-swap="none"
hx-on::before-request="this.disabled=true;this.textContent='更新中...'"
hx-on::after-request="window.location.reload()"
class="text-xs text-indigo-500 hover:text-indigo-700">更新</button>
<button hx-delete="/api/repos/{{.ID}}" hx-confirm="确定删除此仓库缓存?"
hx-swap="none" hx-on::after-request="window.location.reload()"
class="text-xs text-red-500 hover:text-red-700">删除</button>
</div>
</div>
{{end}}
</div>
{{else}}