mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-14 02:11:37 +02:00
feat(api): add last_sync to repository API (#37566)
This PR adds a new repository API field, `mirror_last_sync_at`, to expose the timestamp of the last successful pull mirror sync. Unlike `mirror_updated`, this field does not affect mirror scheduling and is updated only after a successful pull sync. Failed sync attempts leave the value unchanged. What changed - added `mirror_last_sync_at` to the repository API response - updated pull mirror sync flow to persist the timestamp only on successful sync - kept `mirror_updated` behavior unchanged for queue/scheduling purposes `mirror_updated` is currently tied to mirror queue behavior, so it cannot safely represent the last successful sync time. The new field makes that state explicit for API consumers without changing scheduling semantics. --------- Signed-off-by: pomidorry <106489913+Pomidorry@users.noreply.github.com> Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
parent
c78c84c3ca
commit
67f86bc3fe
@ -409,6 +409,7 @@ func prepareMigrationTasks() []*migration {
|
|||||||
// Gitea 1.26.0 ends at migration ID number 330 (database version 331)
|
// 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(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel),
|
||||||
|
newMigration(332, "Add last_sync_unix to mirror", v1_27.AddLastSyncUnixToMirror),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
21
models/migrations/v1_27/v332.go
Normal file
21
models/migrations/v1_27/v332.go
Normal file
@ -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:"INDEX"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mirrorWithLastSyncUnix) TableName() string {
|
||||||
|
return "mirror"
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddLastSyncUnixToMirror(x *xorm.Engine) error {
|
||||||
|
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||||
|
IgnoreDropIndices: true,
|
||||||
|
}, new(mirrorWithLastSyncUnix))
|
||||||
|
return err
|
||||||
|
}
|
||||||
@ -27,6 +27,7 @@ type Mirror struct {
|
|||||||
|
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
NextUpdateUnix timeutil.TimeStamp `xorm:"INDEX"`
|
NextUpdateUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
|
LastSyncUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
|
|
||||||
LFS bool `xorm:"lfs_enabled NOT NULL DEFAULT false"`
|
LFS bool `xorm:"lfs_enabled NOT NULL DEFAULT false"`
|
||||||
LFSEndpoint string `xorm:"lfs_endpoint TEXT"`
|
LFSEndpoint string `xorm:"lfs_endpoint TEXT"`
|
||||||
|
|||||||
@ -125,10 +125,12 @@ type Repository struct {
|
|||||||
// ObjectFormatName of the underlying git repository
|
// ObjectFormatName of the underlying git repository
|
||||||
ObjectFormatName ObjectFormatName `json:"object_format_name"`
|
ObjectFormatName ObjectFormatName `json:"object_format_name"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
MirrorUpdated time.Time `json:"mirror_updated"`
|
MirrorUpdated time.Time `json:"mirror_updated"`
|
||||||
RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"`
|
// swagger:strfmt date-time
|
||||||
Topics []string `json:"topics"`
|
MirrorLastSyncAt time.Time `json:"mirror_last_sync_at"`
|
||||||
Licenses []string `json:"licenses"`
|
RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"`
|
||||||
|
Topics []string `json:"topics"`
|
||||||
|
Licenses []string `json:"licenses"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRepoOption options when creating repository
|
// CreateRepoOption options when creating repository
|
||||||
|
|||||||
@ -152,11 +152,13 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||||||
|
|
||||||
mirrorInterval := ""
|
mirrorInterval := ""
|
||||||
var mirrorUpdated time.Time
|
var mirrorUpdated time.Time
|
||||||
|
var lastSync time.Time
|
||||||
if repo.IsMirror {
|
if repo.IsMirror {
|
||||||
pullMirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
|
pullMirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
mirrorInterval = pullMirror.Interval.String()
|
mirrorInterval = pullMirror.Interval.String()
|
||||||
mirrorUpdated = pullMirror.UpdatedUnix.AsTime()
|
mirrorUpdated = pullMirror.UpdatedUnix.AsTime()
|
||||||
|
lastSync = pullMirror.LastSyncUnix.AsTime()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,6 +249,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||||||
DefaultTargetBranch: defaultTargetBranch,
|
DefaultTargetBranch: defaultTargetBranch,
|
||||||
AvatarURL: repo.AvatarLink(ctx),
|
AvatarURL: repo.AvatarLink(ctx),
|
||||||
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
|
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
|
||||||
|
MirrorLastSyncAt: lastSync,
|
||||||
MirrorInterval: mirrorInterval,
|
MirrorInterval: mirrorInterval,
|
||||||
MirrorUpdated: mirrorUpdated,
|
MirrorUpdated: mirrorUpdated,
|
||||||
RepoTransfer: transfer,
|
RepoTransfer: transfer,
|
||||||
|
|||||||
@ -305,6 +305,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
|
|||||||
|
|
||||||
log.Trace("SyncMirrors [repo: %-v]: Scheduling next update", m.Repo)
|
log.Trace("SyncMirrors [repo: %-v]: Scheduling next update", m.Repo)
|
||||||
m.ScheduleNextUpdate()
|
m.ScheduleNextUpdate()
|
||||||
|
m.LastSyncUnix = m.UpdatedUnix
|
||||||
if err = repo_model.UpdateMirror(ctx, m); err != nil {
|
if err = repo_model.UpdateMirror(ctx, m); err != nil {
|
||||||
log.Error("SyncMirrors [repo: %-v]: failed to UpdateMirror with next update date: %v", m.Repo, err)
|
log.Error("SyncMirrors [repo: %-v]: failed to UpdateMirror with next update date: %v", m.Repo, err)
|
||||||
return false
|
return false
|
||||||
|
|||||||
5
templates/swagger/v1_json.tmpl
generated
5
templates/swagger/v1_json.tmpl
generated
@ -29039,6 +29039,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "MirrorInterval"
|
"x-go-name": "MirrorInterval"
|
||||||
},
|
},
|
||||||
|
"mirror_last_sync_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"x-go-name": "MirrorLastSyncAt"
|
||||||
|
},
|
||||||
"mirror_updated": {
|
"mirror_updated": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
|
|||||||
@ -9292,6 +9292,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "MirrorInterval"
|
"x-go-name": "MirrorInterval"
|
||||||
},
|
},
|
||||||
|
"mirror_last_sync_at": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "MirrorLastSyncAt"
|
||||||
|
},
|
||||||
"mirror_updated": {
|
"mirror_updated": {
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@ -93,6 +93,9 @@ func TestMirrorPull(t *testing.T) {
|
|||||||
ok := mirror_service.SyncPullMirror(ctx, mirrorRepo.ID)
|
ok := mirror_service.SyncPullMirror(ctx, mirrorRepo.ID)
|
||||||
assert.True(t, ok)
|
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
|
// actually there is a tag in the source repo, so after "sync", that tag will also come into the mirror
|
||||||
initCount++
|
initCount++
|
||||||
|
|
||||||
@ -110,4 +113,14 @@ func TestMirrorPull(t *testing.T) {
|
|||||||
count, err = db.Count[repo_model.Release](t.Context(), findOptions)
|
count, err = db.Count[repo_model.Release](t.Context(), findOptions)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, initCount, count)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user