diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index c3a8f08b5d..c1d247bc41 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -409,6 +409,7 @@ func prepareMigrationTasks() []*migration { // Gitea 1.26.0 ends at migration ID number 330 (database version 331) newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel), + newMigration(332, "Add last_sync_unix to mirror", v1_27.AddLastSyncUnixToMirror), } return preparedMigrations } diff --git a/models/migrations/v1_27/v332.go b/models/migrations/v1_27/v332.go new file mode 100644 index 0000000000..8cf087d56e --- /dev/null +++ b/models/migrations/v1_27/v332.go @@ -0,0 +1,21 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import "xorm.io/xorm" + +type mirrorWithLastSyncUnix struct { + LastSyncUnix int64 `xorm:"last_sync_unix INDEX"` +} + +func (mirrorWithLastSyncUnix) TableName() string { + return "mirror" +} + +func AddLastSyncUnixToMirror(x *xorm.Engine) error { + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(mirrorWithLastSyncUnix)) + return err +} diff --git a/models/repo/mirror.go b/models/repo/mirror.go index be7b785612..1069943d40 100644 --- a/models/repo/mirror.go +++ b/models/repo/mirror.go @@ -27,6 +27,7 @@ type Mirror struct { UpdatedUnix timeutil.TimeStamp `xorm:"INDEX"` NextUpdateUnix timeutil.TimeStamp `xorm:"INDEX"` + LastSyncUnix timeutil.TimeStamp `xorm:"INDEX"` LFS bool `xorm:"lfs_enabled NOT NULL DEFAULT false"` LFSEndpoint string `xorm:"lfs_endpoint TEXT"` @@ -98,6 +99,13 @@ func TouchMirror(ctx context.Context, m *Mirror) error { return err } +// UpdateMirrorLastSyncTime updates the mirror's last successful sync time. +func UpdateMirrorLastSyncTime(ctx context.Context, m *Mirror, syncTime timeutil.TimeStamp) error { + m.LastSyncUnix = syncTime + _, err := db.GetEngine(ctx).ID(m.ID).Cols("last_sync_unix").NoAutoTime().Update(m) + return err +} + // DeleteMirrorByRepoID deletes a mirror by repoID func DeleteMirrorByRepoID(ctx context.Context, repoID int64) error { _, err := db.GetEngine(ctx).Delete(&Mirror{RepoID: repoID}) diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 0c3a0ab44e..5626f7e14b 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -125,10 +125,12 @@ type Repository struct { // ObjectFormatName of the underlying git repository ObjectFormatName ObjectFormatName `json:"object_format_name"` // swagger:strfmt date-time - MirrorUpdated time.Time `json:"mirror_updated"` - RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"` - Topics []string `json:"topics"` - Licenses []string `json:"licenses"` + MirrorUpdated time.Time `json:"mirror_updated"` + // swagger:strfmt date-time + MirrorLastSyncAt time.Time `json:"mirror_last_sync_at"` + RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"` + Topics []string `json:"topics"` + Licenses []string `json:"licenses"` } // CreateRepoOption options when creating repository diff --git a/services/convert/repository.go b/services/convert/repository.go index 503f6bb2a3..ec010ee375 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -152,11 +152,13 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR mirrorInterval := "" var mirrorUpdated time.Time + var lastSync time.Time if repo.IsMirror { pullMirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID) if err == nil { mirrorInterval = pullMirror.Interval.String() mirrorUpdated = pullMirror.UpdatedUnix.AsTime() + lastSync = pullMirror.LastSyncUnix.AsTime() } } @@ -247,6 +249,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR DefaultTargetBranch: defaultTargetBranch, AvatarURL: repo.AvatarLink(ctx), Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate, + MirrorLastSyncAt: lastSync, MirrorInterval: mirrorInterval, MirrorUpdated: mirrorUpdated, RepoTransfer: transfer, diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 9ce35f9eab..ccdee55d0e 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -412,12 +412,15 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { } } - // Update License + if err = repo_model.UpdateMirrorLastSyncTime(ctx, m, m.UpdatedUnix); err != nil { + log.Error("SyncMirrors [repo: %-v]: failed to update mirror last_sync_unix: %v", m.Repo, err) + } + + // Update license metadata after a successful mirror sync. if err = repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{ RepoID: m.Repo.ID, }); err != nil { log.Error("SyncMirrors [repo: %-v]: unable to add repo to license updater queue: %v", m.Repo, err) - return false } log.Trace("SyncMirrors [repo: %-v]: Successfully updated", m.Repo) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 26d45940f2..8b21b53dc6 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -29039,6 +29039,11 @@ "type": "string", "x-go-name": "MirrorInterval" }, + "mirror_last_sync_at": { + "type": "string", + "format": "date-time", + "x-go-name": "MirrorLastSyncAt" + }, "mirror_updated": { "type": "string", "format": "date-time", diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index 33adff75e0..ec04a0a1ee 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -9292,6 +9292,11 @@ "type": "string", "x-go-name": "MirrorInterval" }, + "mirror_last_sync_at": { + "format": "date-time", + "type": "string", + "x-go-name": "MirrorLastSyncAt" + }, "mirror_updated": { "format": "date-time", "type": "string", diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go index 7902dc10cb..0e81aa3d5e 100644 --- a/tests/integration/mirror_pull_test.go +++ b/tests/integration/mirror_pull_test.go @@ -93,6 +93,9 @@ func TestMirrorPull(t *testing.T) { ok := mirror_service.SyncPullMirror(ctx, mirrorRepo.ID) assert.True(t, ok) + mirror := unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: mirrorRepo.ID}) + assert.Equal(t, mirror.UpdatedUnix, mirror.LastSyncUnix) + // actually there is a tag in the source repo, so after "sync", that tag will also come into the mirror initCount++ @@ -110,4 +113,14 @@ func TestMirrorPull(t *testing.T) { count, err = db.Count[repo_model.Release](t.Context(), findOptions) assert.NoError(t, err) assert.Equal(t, initCount, count) + + mirror = unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: mirrorRepo.ID}) + lastMirrorSync := mirror.LastSyncUnix + assert.NoError(t, mirror_service.UpdateAddress(ctx, mirror, repoPath+"-missing")) + + ok = mirror_service.SyncPullMirror(ctx, mirrorRepo.ID) + assert.False(t, ok) + + mirror = unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: mirrorRepo.ID}) + assert.Equal(t, lastMirrorSync, mirror.LastSyncUnix) }