-
-
Notifications
You must be signed in to change notification settings - Fork 6k
Group commit list page by date #34098
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b29d401
98157ad
073c593
0581b27
b9dfd3e
b1efeae
d63aca0
69e0505
22f9a51
fca0992
1228d48
948ff41
c81ab46
5d0f6a0
dd62a17
90dcac2
fe9be75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,9 @@ import ( | |
"html/template" | ||
"net/http" | ||
"path" | ||
"sort" | ||
"strings" | ||
"time" | ||
|
||
asymkey_model "code.gitea.io/gitea/models/asymkey" | ||
"code.gitea.io/gitea/models/db" | ||
|
@@ -27,6 +29,7 @@ import ( | |
"code.gitea.io/gitea/modules/markup" | ||
"code.gitea.io/gitea/modules/setting" | ||
"code.gitea.io/gitea/modules/templates" | ||
"code.gitea.io/gitea/modules/timeutil" | ||
"code.gitea.io/gitea/modules/util" | ||
asymkey_service "code.gitea.io/gitea/services/asymkey" | ||
"code.gitea.io/gitea/services/context" | ||
|
@@ -82,11 +85,12 @@ func Commits(ctx *context.Context) { | |
ctx.ServerError("CommitsByRange", err) | ||
return | ||
} | ||
ctx.Data["Commits"], err = processGitCommits(ctx, commits) | ||
processedCommits, err := processGitCommits(ctx, commits) | ||
if err != nil { | ||
ctx.ServerError("processGitCommits", err) | ||
return | ||
} | ||
ctx.Data["GroupCommits"] = GroupCommitsByDate(processedCommits) | ||
commitIDs := make([]string, 0, len(commits)) | ||
for _, c := range commits { | ||
commitIDs = append(commitIDs, c.ID.String()) | ||
|
@@ -198,11 +202,12 @@ func SearchCommits(ctx *context.Context) { | |
return | ||
} | ||
ctx.Data["CommitCount"] = len(commits) | ||
ctx.Data["Commits"], err = processGitCommits(ctx, commits) | ||
processedCommits, err := processGitCommits(ctx, commits) | ||
if err != nil { | ||
ctx.ServerError("processGitCommits", err) | ||
return | ||
} | ||
ctx.Data["GroupCommits"] = GroupCommitsByDate(processedCommits) | ||
|
||
ctx.Data["Keyword"] = query | ||
if all { | ||
|
@@ -244,11 +249,12 @@ func FileHistory(ctx *context.Context) { | |
ctx.ServerError("CommitsByFileAndRange", err) | ||
return | ||
} | ||
ctx.Data["Commits"], err = processGitCommits(ctx, commits) | ||
processedCommits, err := processGitCommits(ctx, commits) | ||
if err != nil { | ||
ctx.ServerError("processGitCommits", err) | ||
return | ||
} | ||
ctx.Data["GroupCommits"] = GroupCommitsByDate(processedCommits) | ||
|
||
ctx.Data["Username"] = ctx.Repo.Owner.Name | ||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name | ||
|
@@ -458,3 +464,57 @@ func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) ([]*git_m | |
} | ||
return commits, nil | ||
} | ||
|
||
// GroupedCommits defines the structure for grouped commits. | ||
type GroupedCommits struct { | ||
Date timeutil.TimeStamp | ||
Commits []*git_model.SignCommitWithStatuses | ||
} | ||
|
||
// GroupCommitsByDate groups the commits by date (in days) using UTC timezone. | ||
func GroupCommitsByDate(commits []*git_model.SignCommitWithStatuses) []GroupedCommits { | ||
// Use Unix timestamp of date as key (truncated to day) | ||
grouped := make(map[int64][]*git_model.SignCommitWithStatuses) | ||
|
||
for _, commit := range commits { | ||
var sigTime time.Time | ||
if commit.Committer != nil { | ||
sigTime = commit.Committer.When | ||
} else if commit.Author != nil { | ||
sigTime = commit.Author.When | ||
} | ||
|
||
// Convert time to UTC timezone first | ||
sigTimeUTC := sigTime.UTC() | ||
|
||
// Truncate time to date part (remove hours, minutes, seconds) | ||
year, month, day := sigTimeUTC.Date() | ||
dateOnly := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) | ||
dateUnix := dateOnly.Unix() | ||
|
||
grouped[dateUnix] = append(grouped[dateUnix], commit) | ||
} | ||
|
||
// Create result slice with pre-allocated capacity | ||
result := make([]GroupedCommits, 0, len(grouped)) | ||
|
||
// Collect all dates and sort them | ||
dates := make([]int64, 0, len(grouped)) | ||
for dateUnix := range grouped { | ||
dates = append(dates, dateUnix) | ||
} | ||
// Sort dates in descending order (most recent first) | ||
sort.Slice(dates, func(i, j int) bool { | ||
return dates[i] > dates[j] | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another edge case: git commit's date could be manually set. So you could see a commit list like this:
And the graph is commit1->commit2->commit3->commit4. What's the expected output for such commit list? It needs some tests to clarify the behavior. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The to be discussed here is which time field to use. Currently, the list uses the committer's time both before and after grouping. If adjustment is needed, we can make modifications later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't mean "which field". I mean "how to group them", assume these dates are all from the "committer's time"
What's the expected output? Or to be more precise
What's the expected output? |
||
|
||
// Build result in sorted order | ||
for _, dateUnix := range dates { | ||
result = append(result, GroupedCommits{ | ||
Date: timeutil.TimeStamp(dateUnix), | ||
Commits: grouped[dateUnix], | ||
}) | ||
} | ||
|
||
return result | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
// Copyright 2017 The Gitea Authors. All rights reserved. | ||
// Copyright 2014 The Gogs Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package repo | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"code.gitea.io/gitea/models/asymkey" | ||
git_model "code.gitea.io/gitea/models/git" | ||
"code.gitea.io/gitea/models/user" | ||
"code.gitea.io/gitea/modules/git" | ||
"code.gitea.io/gitea/modules/timeutil" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestGroupCommitsByDate(t *testing.T) { | ||
// Create test data | ||
// These two commits represent the same moment but in different timezones | ||
// commit1: 2025年04月10日T08:00:00+08:00 | ||
// commit2: 2025年04月09日T23:00:00-01:00 | ||
// Their UTC time is both 2025年04月10日T00:00:00Z | ||
|
||
// Create the first commit (Asia timezone +8) | ||
asiaTimezone := time.FixedZone("Asia/Shanghai", 8*60*60) | ||
commit1Time := time.Date(2025, 4, 10, 8, 0, 0, 0, asiaTimezone) | ||
commit1 := &git_model.SignCommitWithStatuses{ | ||
SignCommit: &asymkey.SignCommit{ | ||
UserCommit: &user.UserCommit{ | ||
Commit: &git.Commit{ | ||
Committer: &git.Signature{ | ||
When: commit1Time, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
// Create the second commit (Western timezone -1) | ||
westTimezone := time.FixedZone("West", -1*60*60) | ||
commit2Time := time.Date(2025, 4, 9, 23, 0, 0, 0, westTimezone) | ||
commit2 := &git_model.SignCommitWithStatuses{ | ||
SignCommit: &asymkey.SignCommit{ | ||
UserCommit: &user.UserCommit{ | ||
Commit: &git.Commit{ | ||
Committer: &git.Signature{ | ||
When: commit2Time, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
// Verify that the two timestamps actually represent the same moment | ||
assert.Equal(t, commit1Time.Unix(), commit2Time.Unix(), "The two commits should have the same Unix timestamp") | ||
|
||
// Test the modified grouping behavior | ||
commits := []*git_model.SignCommitWithStatuses{commit1, commit2} | ||
grouped := GroupCommitsByDate(commits) | ||
|
||
// Output the grouping results for observation | ||
t.Logf("Number of grouped results: %d", len(grouped)) | ||
for i, group := range grouped { | ||
t.Logf("Group %d: Date %s, Number of commits %d", i, time.Unix(int64(group.Date), 0).Format("2006年01月02日"), len(group.Commits)) | ||
for j, c := range group.Commits { | ||
t.Logf(" Commit %d: Time %s", j, c.SignCommit.UserCommit.Commit.Committer.When.Format(time.RFC3339)) | ||
} | ||
} | ||
|
||
// After modification, these two commits should be grouped together as they are on the same day in UTC timezone | ||
assert.Len(t, grouped, 1, "After modification, the two commits should be grouped together") | ||
|
||
// Verify the group date (should be 2025年04月10日, the date in UTC timezone) | ||
utcDate := time.Date(2025, 4, 10, 0, 0, 0, 0, time.UTC) | ||
assert.Equal(t, timeutil.TimeStamp(utcDate.Unix()), grouped[0].Date) | ||
assert.Len(t, grouped[0].Commits, 2) | ||
|
||
// Verify that both commits are in this group | ||
commitMap := make(map[*git_model.SignCommitWithStatuses]bool) | ||
for _, c := range grouped[0].Commits { | ||
commitMap[c] = true | ||
} | ||
assert.True(t, commitMap[commit1], "The first commit should be in the group") | ||
assert.True(t, commitMap[commit2], "The second commit should be in the group") | ||
|
||
// Add a commit with a different date for testing | ||
nextDayTimezone := time.FixedZone("NextDay", 0) | ||
commit3Time := time.Date(2025, 4, 11, 0, 0, 0, 0, nextDayTimezone) | ||
commit3 := &git_model.SignCommitWithStatuses{ | ||
SignCommit: &asymkey.SignCommit{ | ||
UserCommit: &user.UserCommit{ | ||
Commit: &git.Commit{ | ||
Committer: &git.Signature{ | ||
When: commit3Time, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
// Test with commits from different dates | ||
commits = append(commits, commit3) | ||
grouped = GroupCommitsByDate(commits) | ||
|
||
// Now there should be two groups | ||
assert.Len(t, grouped, 2, "There should be two different date groups") | ||
|
||
// Verify date sorting (descending, most recent date first) | ||
assert.True(t, time.Unix(int64(grouped[0].Date), 0).After(time.Unix(int64(grouped[1].Date), 0)), | ||
"Dates should be sorted in descending order") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
{{range $index, $groupCommit := .GroupCommits}} | ||
<div class="ui timeline commits-list-group-by-date" data-index="{{$index}}"> | ||
|
||
<div class="timeline-badge-wrapper"> | ||
<div class="timeline-badge"> | ||
{{svg "octicon-git-commit"}} | ||
</div> | ||
</div> | ||
|
||
<div class="timeline-body"> | ||
<h3 class="flex-text-block tw-py-2 timeline-heading"> | ||
Commits on {{DateUtils.AbsoluteShort $groupCommit.Date}} | ||
</h3> | ||
<div class="tw-flex tw-mt-2 timeline-list-container"> | ||
<ul class="commits-list"> | ||
{{range $groupCommit.Commits}} | ||
{{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}} | ||
<li class="commits-list-item"> | ||
<div class="tw-pt-4 tw-pl-4 title"> | ||
<h4> | ||
<span class="message-wrapper"> | ||
{{if $.PageIsWiki}} | ||
<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | ctx.RenderUtils.RenderEmoji}}</span> | ||
{{else}} | ||
{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}} | ||
<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.Repository.ComposeCommentMetas ctx)}}</span> | ||
{{end}} | ||
</span> | ||
{{if IsMultilineCommitMessage .Message}} | ||
<button class="ui button ellipsis-button" aria-expanded="false" data-global-click="onRepoEllipsisButtonClick">...</button> | ||
{{end}} | ||
{{if IsMultilineCommitMessage .Message}} | ||
<pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .Message ($.Repository.ComposeCommentMetas ctx)}}</pre> | ||
{{end}} | ||
{{if $.CommitsTagsMap}} | ||
{{range (index $.CommitsTagsMap .ID.String)}} | ||
{{- template "repo/tag/name" dict "RepoLink" $.Repository.Link "TagName" .TagName "IsRelease" (not .IsTag) -}} | ||
{{end}} | ||
{{end}} | ||
</h4> | ||
</div> | ||
<div class="tw-flex tw-items-center tw-gap-1 tw-pb-4 tw-pl-4 description"> | ||
<div class="author"> | ||
{{$userName := .Author.Name}} | ||
{{if .User}} | ||
{{if and .User.FullName DefaultShowFullName}} | ||
{{$userName = .User.FullName}} | ||
{{end}} | ||
{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}<a class="muted author-wrapper" href="{{.User.HomeLink}}">{{$userName}}</a> | ||
{{else}} | ||
{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-2"}} | ||
<span class="author-wrapper">{{$userName}}</span> | ||
{{end}} | ||
</div> | ||
<span> | ||
{{if .Committer}} | ||
committed | ||
{{else}} | ||
authored | ||
{{end}} | ||
</span> | ||
{{if .Committer}} | ||
{{DateUtils.TimeSince .Committer.When}} | ||
{{else}} | ||
{{DateUtils.TimeSince .Author.When}} | ||
{{end}} | ||
{{if .Statuses}} | ||
<span>·</span> | ||
{{end}} | ||
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} | ||
</div> | ||
<div class="tw-flex tw-flex-wrap tw-items-center tw-gap-2 tw-pr-4 metadata"> | ||
{{$commitBaseLink := ""}} | ||
{{if $.PageIsWiki}} | ||
{{$commitBaseLink = printf "%s/wiki/commit" $commitRepoLink}} | ||
{{else if $.PageIsPullCommits}} | ||
{{$commitBaseLink = printf "%s/pulls/%d/commits" $commitRepoLink $.Issue.Index}} | ||
{{else if $.Reponame}} | ||
{{$commitBaseLink = printf "%s/commit" $commitRepoLink}} | ||
{{end}} | ||
<div class="commit-sign-badge"> | ||
{{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} | ||
</div> | ||
<div class="tw-flex tw-flex-wrap tw-items-center tw-gap-2"> | ||
<div> | ||
<button class="btn interact-bg tw-p-2 copy-commit-id" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button> | ||
</div> | ||
{{/* at the moment, wiki doesn't support these "view" links like "view at history point" */}} | ||
{{if not $.PageIsWiki}} | ||
{{/* view single file diff */}} | ||
{{if $.FileTreePath}} | ||
<a class="btn interact-bg tw-p-2 view-single-diff" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_file_diff"}}" | ||
href="{{$commitRepoLink}}/commit/{{.ID.String}}?files={{$.FileTreePath}}" | ||
>{{svg "octicon-file-diff"}}</a> | ||
{{end}} | ||
|
||
{{/* view at history point */}} | ||
{{$viewCommitLink := printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}} | ||
{{if $.FileTreePath}}{{$viewCommitLink = printf "%s/%s" $viewCommitLink (PathEscapeSegments $.FileTreePath)}}{{end}} | ||
<a class="btn interact-bg tw-p-2 view-commit-path" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}" href="{{$viewCommitLink}}">{{svg "octicon-file-code"}}</a> | ||
{{end}} | ||
</div> | ||
</div> | ||
</li> | ||
{{end}} | ||
</ul> | ||
</div> | ||
</div> | ||
|
||
</div> | ||
{{end}} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,8 +29,8 @@ | |
</div> | ||
{{end}} | ||
|
||
{{if and .Commits (gt .CommitCount 0)}} | ||
{{template "repo/commits_list" .}} | ||
kerwin612 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still the question, it seems that "wiki" is still using the |
||
{{if and .GroupCommits (gt .CommitCount 0)}} | ||
{{template "repo/commits_list_group_by_date" .}} | ||
{{end}} | ||
|
||
{{template "base/paginate" .}} |