Skip to content

Commit 0ee2edf

Browse files
fjamespriceclaude
andcommitted
Use commit author date for heatmap instead of push date
When commits are made locally over multiple days and then pushed at once, the contribution heatmap now displays them on their actual author dates rather than the push date. Changes: - Add OriginalUnix field to Action model to store the original content timestamp - Set OriginalUnix to the earliest commit author date when creating push actions - Update heatmap query to use COALESCE(original_unix, created_unix) - Add database migration for the new column Fixes #14051 Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 224b788 commit 0ee2edf

File tree

5 files changed

+73
-21
lines changed

5 files changed

+73
-21
lines changed

models/activities/action.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,10 @@ type Action struct {
145145
Issue *issues_model.Issue `xorm:"-"` // get the issue id from content
146146
IsDeleted bool `xorm:"NOT NULL DEFAULT false"`
147147
RefName string
148-
IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
149-
Content string `xorm:"TEXT"`
150-
CreatedUnix timeutil.TimeStamp `xorm:"created"`
148+
IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
149+
Content string `xorm:"TEXT"`
150+
CreatedUnix timeutil.TimeStamp `xorm:"created"`
151+
OriginalUnix timeutil.TimeStamp `xorm:"INDEX"` // Original timestamp for the content (e.g., commit author date for push actions)
151152
}
152153

153154
func init() {

models/activities/user_heatmap.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi
3838

3939
// Group by 15 minute intervals which will allow the client to accurately shift the timestamp to their timezone.
4040
// The interval is based on the fact that there are timezones such as UTC +5:30 and UTC +12:45.
41-
groupBy := "created_unix / 900 * 900"
41+
// Use original_unix if available (for commit actions, this is the commit author date),
42+
// otherwise fall back to created_unix (the push/action timestamp).
43+
groupBy := "COALESCE(NULLIF(original_unix, 0), created_unix) / 900 * 900"
4244
groupByName := "timestamp" // We need this extra case because mssql doesn't allow grouping by alias
4345
switch {
4446
case setting.Database.Type.IsMySQL():
45-
groupBy = "created_unix DIV 900 * 900"
47+
groupBy = "COALESCE(NULLIF(original_unix, 0), created_unix) DIV 900 * 900"
4648
case setting.Database.Type.IsMSSQL():
4749
groupByName = groupBy
4850
}

models/migrations/migrations.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ func prepareMigrationTasks() []*migration {
400400
newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency),
401401
newMigration(324, "Fix closed milestone completeness for milestones with no issues", v1_26.FixClosedMilestoneCompleteness),
402402
newMigration(325, "Fix missed repo_id when migrate attachments", v1_26.FixMissedRepoIDWhenMigrateAttachments),
403+
newMigration(326, "Add original_unix to action for heatmap commit dates", v1_26.AddOriginalUnixToAction),
403404
}
404405
return preparedMigrations
405406
}

models/migrations/v1_26/v326.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_26
5+
6+
import (
7+
"xorm.io/xorm"
8+
)
9+
10+
// AddOriginalUnixToAction adds original_unix column to action table
11+
// for storing the original timestamp of content (e.g., commit author date).
12+
// This allows the heatmap to display commits on their actual dates
13+
// rather than the push date.
14+
func AddOriginalUnixToAction(x *xorm.Engine) error {
15+
type Action struct {
16+
OriginalUnix int64 `xorm:"INDEX"`
17+
}
18+
19+
return x.Sync(new(Action))
20+
}

services/feed/notifier.go

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"code.gitea.io/gitea/modules/json"
1818
"code.gitea.io/gitea/modules/log"
1919
"code.gitea.io/gitea/modules/repository"
20+
"code.gitea.io/gitea/modules/timeutil"
2021
"code.gitea.io/gitea/modules/util"
2122
notify_service "code.gitea.io/gitea/services/notify"
2223
)
@@ -337,15 +338,29 @@ func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.Use
337338
opType = activities_model.ActionDeleteBranch
338339
}
339340

341+
// Find the earliest commit timestamp to use as the original timestamp for the heatmap.
342+
// This ensures commits show up on their actual author date, not the push date.
343+
var originalUnix timeutil.TimeStamp
344+
if len(commits.Commits) > 0 {
345+
earliest := commits.Commits[0].Timestamp
346+
for _, commit := range commits.Commits[1:] {
347+
if commit.Timestamp.Before(earliest) {
348+
earliest = commit.Timestamp
349+
}
350+
}
351+
originalUnix = timeutil.TimeStamp(earliest.Unix())
352+
}
353+
340354
if err = NotifyWatchers(ctx, &activities_model.Action{
341-
ActUserID: pusher.ID,
342-
ActUser: pusher,
343-
OpType: opType,
344-
Content: string(data),
345-
RepoID: repo.ID,
346-
Repo: repo,
347-
RefName: opts.RefFullName.String(),
348-
IsPrivate: repo.IsPrivate,
355+
ActUserID: pusher.ID,
356+
ActUser: pusher,
357+
OpType: opType,
358+
Content: string(data),
359+
RepoID: repo.ID,
360+
Repo: repo,
361+
RefName: opts.RefFullName.String(),
362+
IsPrivate: repo.IsPrivate,
363+
OriginalUnix: originalUnix,
349364
}); err != nil {
350365
log.Error("NotifyWatchers: %v", err)
351366
}
@@ -402,15 +417,28 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model
402417
return
403418
}
404419

420+
// Find the earliest commit timestamp to use as the original timestamp for the heatmap.
421+
var originalUnix timeutil.TimeStamp
422+
if len(commits.Commits) > 0 {
423+
earliest := commits.Commits[0].Timestamp
424+
for _, commit := range commits.Commits[1:] {
425+
if commit.Timestamp.Before(earliest) {
426+
earliest = commit.Timestamp
427+
}
428+
}
429+
originalUnix = timeutil.TimeStamp(earliest.Unix())
430+
}
431+
405432
if err := NotifyWatchers(ctx, &activities_model.Action{
406-
ActUserID: repo.OwnerID,
407-
ActUser: repo.MustOwner(ctx),
408-
OpType: activities_model.ActionMirrorSyncPush,
409-
RepoID: repo.ID,
410-
Repo: repo,
411-
IsPrivate: repo.IsPrivate,
412-
RefName: opts.RefFullName.String(),
413-
Content: string(data),
433+
ActUserID: repo.OwnerID,
434+
ActUser: repo.MustOwner(ctx),
435+
OpType: activities_model.ActionMirrorSyncPush,
436+
RepoID: repo.ID,
437+
Repo: repo,
438+
IsPrivate: repo.IsPrivate,
439+
RefName: opts.RefFullName.String(),
440+
Content: string(data),
441+
OriginalUnix: originalUnix,
414442
}); err != nil {
415443
log.Error("NotifyWatchers: %v", err)
416444
}

0 commit comments

Comments
 (0)