5 files changed,
71 insertions(+),
38 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-04-03 17:43:54 +0300
Authored at:
2026-04-03 16:45:27 +0300
Change ID:
uoknvknyvtxtumrlmvzrwkorqvkozuum
Parent:
d00a3f1
M
internal/git/config.go
路路路 103 103 return g.setOption("last-sync", lastSync.Format(time.RFC3339)) 104 104 } 105 105 106 +func (g *Repo) LastChecked() (time.Time, error) { 107 + raw, err := g.readOption("last-checked") 108 + if err != nil { 109 + return time.Time{}, err 110 + } 111 + 112 + if raw == "" { 113 + return time.Time{}, fmt.Errorf("last-checked not set") 114 + } 115 + 116 + out, err := time.Parse(time.RFC3339, raw) 117 + if err != nil { 118 + return time.Time{}, fmt.Errorf("failed to parse time: %w", err) 119 + } 120 + 121 + return out, nil 122 +} 123 + 124 +func (g *Repo) SetLastChecked(lastChecked time.Time) error { 125 + return g.setOption("last-checked", lastChecked.Format(time.RFC3339)) 126 +} 127 + 106 128 func (g *Repo) readOption(key string) (string, error) { 107 129 c, err := g.r.Config() 108 130 if err != nil {
M
internal/git/repo.go
路路路 236 236 return err == nil 237 237 } 238 238 239 -func (g *Repo) Fetch(ctx context.Context) error { 239 +func (g *Repo) Fetch(ctx context.Context) (isUpdated bool, err error) { 240 240 return g.fetch(ctx, nil) 241 241 } 242 242 243 -func (g *Repo) FetchFromGithubWithToken(ctx context.Context, token string) error { 243 +func (g *Repo) FetchFromGithubWithToken(ctx context.Context, token string) (isUpdated bool, err error) { 244 244 return g.fetch(ctx, &http.BasicAuth{ 245 245 Username: "x-access-token", // this can be anything but empty 246 246 Password: token, 247 247 }) 248 248 } 249 249 250 -func (g *Repo) fetch(ctx context.Context, auth transport.AuthMethod) error { 250 +func (g *Repo) fetch(ctx context.Context, auth transport.AuthMethod) (bool, error) { 251 251 rmt, err := g.r.Remote(originRemote) 252 252 if err != nil { 253 - return fmt.Errorf("failed to get remote: %w", err) 253 + return false, fmt.Errorf("failed to get remote: %w", err) 254 254 } 255 255 256 - if err = rmt.FetchContext(ctx, &git.FetchOptions{ 256 + err = rmt.FetchContext(ctx, &git.FetchOptions{ 257 257 Auth: auth, 258 258 Tags: git.AllTags, 259 259 Prune: true, 260 260 Force: true, 261 - }); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { 262 - return fmt.Errorf("failed to fetch: %w", err) 261 + }) 262 + 263 + isUpdated := !errors.Is(err, git.NoErrAlreadyUpToDate) 264 + if err != nil && isUpdated { 265 + return false, fmt.Errorf("failed to fetch: %w", err) 263 266 } 264 267 265 - // for some reason fetch doesn't change head for empty repos 266 268 if !g.IsEmpty() { 267 - return nil 269 + return isUpdated, nil 268 270 } 269 271 270 272 refs, err := rmt.List(&git.ListOptions{Auth: auth}) 271 273 if err != nil { 272 - return fmt.Errorf("failed to list references: %w", err) 274 + return false, fmt.Errorf("failed to list references: %w", err) 273 275 } 274 276 275 277 for _, ref := range refs { 路路路 277 279 if err := g.r.Storer.SetReference( 278 280 plumbing.NewSymbolicReference(plumbing.HEAD, ref.Target()), 279 281 ); err != nil { 280 - return fmt.Errorf("failed to set HEAD: %w", err) 282 + return false, fmt.Errorf("failed to set HEAD: %w", err) 281 283 } 282 284 break 283 285 } 284 286 } 285 287 286 - return nil 288 + return isUpdated, nil 287 289 }
M
internal/handlers/repo.go
路路路 47 47 } 48 48 49 49 type RepoIndex struct { 50 - Desc string 51 - SSHUser string 52 - IsEmpty bool 53 - Readme template.HTML 54 - Ref string 55 - Commits []*git.Commit 56 - IsMirror bool 57 - MirrorURL string 58 - MirrorLastSync time.Time 50 + Desc string 51 + SSHUser string 52 + IsEmpty bool 53 + Readme template.HTML 54 + Ref string 55 + Commits []*git.Commit 56 + IsMirror bool 57 + MirrorURL string 58 + MirrorLastSync time.Time 59 + MirrorLastChecked time.Time 59 60 } 60 61 61 62 func (h *handlers) repoIndexHandler(w http.ResponseWriter, r *http.Request) { 路路路 81 82 p.IsMirror = true 82 83 p.MirrorURL, _ = repo.RemoteURL() 83 84 p.MirrorLastSync, _ = repo.LastSync() 85 + p.MirrorLastChecked, _ = repo.LastChecked() 84 86 } 85 87 86 88 if p.IsEmpty { 路路路 189 191 fc, err := repo.FileContent(treePath) 190 192 if err != nil { 191 193 if errors.Is(err, git.ErrFileNotFound) { 192 - h.write404(w, r.URL.Path, err) 194 + h.write404(w, r.URL.Path, err) 193 195 return 194 196 } 195 197 h.write500(w, err)
M
internal/mirror/mirror.go
路路路 121 121 return err 122 122 } 123 123 124 - if err := IsRemoteSupported(remoteURL); err != nil { 124 + if err = IsRemoteSupported(remoteURL); err != nil { 125 125 slog.Error("mirror: remote is not valid", "repo", name, "err", err) 126 126 return err 127 127 } 128 128 129 + var isUpdated bool 129 130 if IsGithubRemote(remoteURL) && w.c.Mirror.GithubToken != "" { 130 - if err := repo.FetchFromGithubWithToken(ctx, w.c.Mirror.GithubToken); err != nil { 131 - slog.Error("mirror: fetch failed (github)", "repo", name, "err", err) 132 - return err 133 - } 131 + isUpdated, err = repo.FetchFromGithubWithToken(ctx, w.c.Mirror.GithubToken) 134 132 } else { 135 - if err := repo.Fetch(ctx); err != nil { 136 - slog.Error("mirror: fetch failed", "repo", name, "err", err) 137 - return err 138 - } 133 + isUpdated, err = repo.Fetch(ctx) 134 + } 135 + if err != nil { 136 + slog.Error("mirror: fetch failed", "repo", name, "err", err) 137 + return err 139 138 } 140 139 141 - if err := repo.SetLastSync(time.Now()); err != nil { 142 - slog.Error("mirror: failed to set last sync time", "repo", name, "err", err) 140 + now := time.Now() 141 + if err := repo.SetLastChecked(now); err != nil { 142 + slog.Error("mirror: failed to set last checked time", "repo", name, "err", err) 143 + } 144 + 145 + if isUpdated { 146 + if err := repo.SetLastSync(now); err != nil { 147 + slog.Error("mirror: failed to set last sync time", "repo", name, "err", err) 148 + } 143 149 } 144 150 145 - slog.Info("mirror: sync completed", "repo", repo.Name()) 151 + slog.Info("mirror: sync completed", "repo", repo.Name(), "updated", isUpdated) 146 152 return nil 147 153 } 148 154
M
web/templates/repo_index.html
路路路 53 53 <br> 54 54 <h2>Mirror status</h2> 55 55 <p> 56 - {{ if .P.MirrorLastSync.IsZero }}Was not yet synced from: 57 - {{ else }}Last updated {{ humanizeRelTime .P.MirrorLastSync }} from: 58 - {{ end }} 59 - <a href="{{.P.MirrorURL}}" target="_blank">{{.P.MirrorURL}}</a> 56 + {{- if .P.MirrorLastChecked.IsZero -}}Not checked yet 57 + {{- else if eq .P.MirrorLastChecked .P.MirrorLastSync -}}Up to date (checked {{ humanizeRelTime .P.MirrorLastChecked }}) 58 + {{- else if .P.MirrorLastSync.IsZero -}}Checked {{ humanizeRelTime .P.MirrorLastChecked }}, not synced yet 59 + {{- else -}}Checked {{ humanizeRelTime .P.MirrorLastChecked }}, last updated {{ humanizeRelTime .P.MirrorLastSync }} 60 + {{- end }} 路 source: <a class="link" href="{{.P.MirrorURL}}" target="_blank">{{.P.MirrorURL}}</a> 60 61 </p> 61 62 {{ end }} 62 63 </div>