From 4415797c896aed012b41ba7443670029d378f0ee Mon Sep 17 00:00:00 2001 From: pomidorry Date: Wed, 6 May 2026 16:45:51 +0300 Subject: [PATCH] add last pull mirror sync success timestamp --- models/migrations/migrations.go | 1 + models/migrations/v1_27/v332.go | 27 ++++++++++++++ models/repo/repo.go | 1 + models/repo/update.go | 9 +++++ modules/structs/repo.go | 4 ++- services/convert/repository.go | 3 +- services/convert/repository_test.go | 47 +++++++++++++++++++++++++ services/mirror/mirror_pull.go | 4 +++ templates/swagger/v1_json.tmpl | 5 +++ templates/swagger/v1_openapi3_json.tmpl | 5 +++ 10 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 models/migrations/v1_27/v332.go create mode 100644 services/convert/repository_test.go diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index c3a8f08b5d..c3f68d2449 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_pull_sync_success_unix to repository", v1_27.AddLastPullSyncSuccessUnixToRepository), } return preparedMigrations } diff --git a/models/migrations/v1_27/v332.go b/models/migrations/v1_27/v332.go new file mode 100644 index 0000000000..4fba94665a --- /dev/null +++ b/models/migrations/v1_27/v332.go @@ -0,0 +1,27 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +type repositoryWithLastPullSyncSuccessUnix struct { + LastPullSyncSuccessUnix int64 `xorm:"INDEX"` +} + +func (repositoryWithLastPullSyncSuccessUnix) TableName() string { + return "repository" +} + +func AddLastPullSyncSuccessUnixToRepository(x *xorm.Engine) error { + if err := x.Sync(new(repositoryWithLastPullSyncSuccessUnix)); err != nil { + return err + } + + _, err := x.Exec("UPDATE repository SET last_pull_sync_success_unix = ?", int64(timeutil.TimeStampNow())) + return err +} diff --git a/models/repo/repo.go b/models/repo/repo.go index 7814bb4876..ed8758dd57 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -219,6 +219,7 @@ type Repository struct { CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"` + LastPullSyncSuccessUnix timeutil.TimeStamp `xorm:"INDEX"` } func init() { diff --git a/models/repo/update.go b/models/repo/update.go index bf560cf695..d8ac0222d2 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -9,6 +9,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) @@ -33,6 +34,14 @@ func UpdateRepositoryUpdatedTime(ctx context.Context, repoID int64, updateTime t return err } +// UpdateRepositoryLastPullSyncSuccess updates a repository's last successful pull mirror sync time. +func UpdateRepositoryLastPullSyncSuccess(ctx context.Context, repoID int64, syncTime timeutil.TimeStamp) error { + _, err := db.GetEngine(ctx).ID(repoID).Cols("last_pull_sync_success_unix").NoAutoTime().Update(&Repository{ + LastPullSyncSuccessUnix: syncTime, + }) + return err +} + // UpdateRepositoryColsWithAutoTime updates repository's columns and the timestamp fields automatically func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, colName string, moreColNames ...string) error { _, err := db.GetEngine(ctx).ID(repo.ID).Cols(append([]string{colName}, moreColNames...)...).Update(repo) diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 0c3a0ab44e..8d16c80020 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -121,6 +121,9 @@ type Repository struct { DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"` AvatarURL string `json:"avatar_url"` Internal bool `json:"internal"` + // swagger:strfmt date-time + LastPullSyncSuccess time.Time `json:"last_pull_sync_success"` + Licenses []string `json:"licenses"` MirrorInterval string `json:"mirror_interval"` // ObjectFormatName of the underlying git repository ObjectFormatName ObjectFormatName `json:"object_format_name"` @@ -128,7 +131,6 @@ type Repository struct { MirrorUpdated time.Time `json:"mirror_updated"` 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..509681e1bb 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -247,12 +247,13 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR DefaultTargetBranch: defaultTargetBranch, AvatarURL: repo.AvatarLink(ctx), Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate, + LastPullSyncSuccess: repo.LastPullSyncSuccessUnix.AsTime(), + Licenses: util.SliceNilAsEmpty(repoLicenses.StringList()), MirrorInterval: mirrorInterval, MirrorUpdated: mirrorUpdated, RepoTransfer: transfer, Topics: util.SliceNilAsEmpty(repo.Topics), ObjectFormatName: api.ObjectFormatName(repo.ObjectFormatName), - Licenses: util.SliceNilAsEmpty(repoLicenses.StringList()), } } diff --git a/services/convert/repository_test.go b/services/convert/repository_test.go new file mode 100644 index 0000000000..18bf3863c7 --- /dev/null +++ b/services/convert/repository_test.go @@ -0,0 +1,47 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "testing" + "time" + + "code.gitea.io/gitea/models/db" + access_model "code.gitea.io/gitea/models/perm/access" + perm_model "code.gitea.io/gitea/models/perm" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestToRepoIncludesLastPullSyncSuccess(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + ctx := t.Context() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + repo.IsMirror = true + require.NoError(t, repo_model.DeleteMirrorByRepoID(ctx, repo.ID)) + repo.LastPullSyncSuccessUnix = timeutil.TimeStamp(1714000000) + require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_mirror", "last_pull_sync_success_unix")) + + mirror := &repo_model.Mirror{ + RepoID: repo.ID, + Interval: time.Hour, + } + require.NoError(t, db.Insert(ctx, mirror)) + + mirror.UpdatedUnix = timeutil.TimeStamp(1715000000) + mirror.NextUpdateUnix = timeutil.TimeStamp(1716000000) + require.NoError(t, repo_model.UpdateMirror(ctx, mirror)) + + apiRepo := ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeRead}) + require.NotNil(t, apiRepo) + + assert.Equal(t, mirror.UpdatedUnix.AsTime(), apiRepo.MirrorUpdated) + assert.Equal(t, repo.LastPullSyncSuccessUnix.AsTime(), apiRepo.LastPullSyncSuccess) + assert.NotEqual(t, apiRepo.MirrorUpdated, apiRepo.LastPullSyncSuccess) +} diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 9ce35f9eab..05958e215d 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -419,6 +419,10 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { log.Error("SyncMirrors [repo: %-v]: unable to add repo to license updater queue: %v", m.Repo, err) return false } + if err = repo_model.UpdateRepositoryLastPullSyncSuccess(ctx, m.Repo.ID, m.UpdatedUnix); err != nil { + log.Error("SyncMirrors [repo: %-v]: failed to update repository last_pull_sync_success_unix: %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..03fc6d7a46 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -29020,6 +29020,11 @@ "type": "string", "x-go-name": "LanguagesURL" }, + "last_pull_sync_success": { + "type": "string", + "format": "date-time", + "x-go-name": "LastPullSyncSuccess" + }, "licenses": { "type": "array", "items": { diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index 33adff75e0..f43b01e1da 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -9273,6 +9273,11 @@ "type": "string", "x-go-name": "LanguagesURL" }, + "last_pull_sync_success": { + "format": "date-time", + "type": "string", + "x-go-name": "LastPullSyncSuccess" + }, "licenses": { "items": { "type": "string"