all repos

mugit @ dc720d9b4ebce9977602babbfdc86b2ba7ace0fd

馃惍 git server that your cow will love
5 files changed, 71 insertions(+), 38 deletions(-)
mirror: distinguish between sync and check time
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>