diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index ae5baa9c47..4a298a3b21 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -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" @@ -28,6 +30,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" @@ -83,11 +86,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()) @@ -199,11 +203,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 { @@ -245,11 +250,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 @@ -463,3 +469,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] + }) + + // Build result in sorted order + for _, dateUnix := range dates { + result = append(result, GroupedCommits{ + Date: timeutil.TimeStamp(dateUnix), + Commits: grouped[dateUnix], + }) + } + + return result +} diff --git a/routers/web/repo/commit_test.go b/routers/web/repo/commit_test.go new file mode 100644 index 0000000000..355edddb7b --- /dev/null +++ b/routers/web/repo/commit_test.go @@ -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-10T08:00:00+08:00 + // commit2: 2025-04-09T23:00:00-01:00 + // Their UTC time is both 2025-04-10T00: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") +} diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 8b99dd95da..3c595aed13 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -665,7 +665,7 @@ func PrepareCompareDiff( ctx.ServerError("processGitCommits", err) return false } - ctx.Data["Commits"] = commits + ctx.Data["GroupCommits"] = GroupCommitsByDate(commits) ctx.Data["CommitCount"] = len(commits) title := ci.HeadBranch diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 43ddc265cf..f9db2fd1b7 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -627,7 +627,7 @@ func ViewPullCommits(ctx *context.Context) { ctx.ServerError("processGitCommits", err) return } - ctx.Data["Commits"] = commits + ctx.Data["GroupCommits"] = GroupCommitsByDate(commits) ctx.Data["CommitCount"] = len(commits) ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) diff --git a/templates/repo/commits_list_group_by_date.tmpl b/templates/repo/commits_list_group_by_date.tmpl new file mode 100644 index 0000000000..492a36ea67 --- /dev/null +++ b/templates/repo/commits_list_group_by_date.tmpl @@ -0,0 +1,111 @@ +{{range $index, $groupCommit := .GroupCommits}} +
+ +
+
+ {{svg "octicon-git-commit"}} +
+
+ +
+

+ Commits on {{DateUtils.AbsoluteShort $groupCommit.Date}} +

+
+
    + {{range $groupCommit.Commits}} + {{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}} +
  • +
    +

    + + {{if $.PageIsWiki}} + {{.Summary | ctx.RenderUtils.RenderEmoji}} + {{else}} + {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}} + {{ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.Repository.ComposeCommentMetas ctx)}} + {{end}} + + {{if IsMultilineCommitMessage .Message}} + + {{end}} + {{if IsMultilineCommitMessage .Message}} +
    {{ctx.RenderUtils.RenderCommitBody .Message ($.Repository.ComposeCommentMetas ctx)}}
    + {{end}} + {{if $.CommitsTagsMap}} + {{range (index $.CommitsTagsMap .ID.String)}} + {{- template "repo/tag/name" dict "RepoLink" $.Repository.Link "TagName" .TagName "IsRelease" (not .IsTag) -}} + {{end}} + {{end}} +

    +
    +
    +
    + {{$userName := .Author.Name}} + {{if .User}} + {{if and .User.FullName DefaultShowFullName}} + {{$userName = .User.FullName}} + {{end}} + {{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}{{$userName}} + {{else}} + {{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-2"}} + {{$userName}} + {{end}} +
    + + {{if .Committer}} + committed + {{else}} + authored + {{end}} + + {{if .Committer}} + {{DateUtils.TimeSince .Committer.When}} + {{else}} + {{DateUtils.TimeSince .Author.When}} + {{end}} + {{if .Statuses}} + ยท + {{end}} + {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} +
    + +
  • + {{end}} +
+
+
+ +
+{{end}} diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index a0c5eacdd4..0da89e82dd 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -29,8 +29,8 @@ {{end}} -{{if and .Commits (gt .CommitCount 0)}} - {{template "repo/commits_list" .}} +{{if and .GroupCommits (gt .CommitCount 0)}} + {{template "repo/commits_list_group_by_date" .}} {{end}} {{template "base/paginate" .}} diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go index ed60bdb58a..14ad267612 100644 --- a/tests/integration/git_general_test.go +++ b/tests/integration/git_general_test.go @@ -707,7 +707,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href") + commitURL, exists := doc.doc.Find(".timeline.commits-list-group-by-date .commit-sign-badge a").Last().Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go index 4d43847f1b..f84ef5b4d9 100644 --- a/tests/integration/pull_status_test.go +++ b/tests/integration/pull_status_test.go @@ -49,7 +49,7 @@ func TestPullCreate_CommitStatus(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href") + commitURL, exists := doc.doc.Find(".timeline.commits-list-group-by-date .commit-sign-badge a").Last().Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -87,12 +87,12 @@ func TestPullCreate_CommitStatus(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) doc = NewHTMLParser(t, resp.Body) - commitURL, exists = doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href") + commitURL, exists = doc.doc.Find(".timeline.commits-list-group-by-date .commit-sign-badge a").Last().Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) assert.Equal(t, commitID, path.Base(commitURL)) - cls, ok := doc.doc.Find("#commits-table tbody tr td.message .commit-status").Last().Attr("class") + cls, ok := doc.doc.Find(".timeline.commits-list-group-by-date .description .commit-status").Last().Attr("class") assert.True(t, ok) assert.Contains(t, cls, statesIcons[status]) } diff --git a/tests/integration/repo_commits_search_test.go b/tests/integration/repo_commits_search_test.go index 9b05e36399..0a1a26fea5 100644 --- a/tests/integration/repo_commits_search_test.go +++ b/tests/integration/repo_commits_search_test.go @@ -22,7 +22,7 @@ func testRepoCommitsSearch(t *testing.T, query, commit string) { resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) - sel := doc.doc.Find("#commits-table tbody tr td.sha a") + sel := doc.doc.Find(".timeline.commits-list-group-by-date .commit-sign-badge a") assert.Equal(t, commit, strings.TrimSpace(sel.Text())) } diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index dee0aa6176..898b8a6e53 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -33,7 +33,7 @@ func TestRepoCommits(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) - commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") + commitURL, exists := doc.doc.Find(".timeline.commits-list-group-by-date .commit-sign-badge .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) } @@ -50,7 +50,7 @@ func Test_ReposGitCommitListNotMaster(t *testing.T) { doc := NewHTMLParser(t, resp.Body) commits := []string{} - doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) { + doc.doc.Find(".timeline.commits-list-group-by-date .commit-sign-badge .commit-id-short").Each(func(i int, s *goquery.Selection) { commitURL, exists := s.Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -63,7 +63,7 @@ func Test_ReposGitCommitListNotMaster(t *testing.T) { assert.Equal(t, "5099b81332712fe655e34e8dd63574f503f61811", commits[2]) userNames := []string{} - doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) { + doc.doc.Find(".timeline.commits-list-group-by-date .description .author-wrapper").Each(func(i int, s *goquery.Selection) { userPath, exists := s.Attr("href") assert.True(t, exists) assert.NotEmpty(t, userPath) @@ -87,7 +87,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") + commitURL, exists := doc.doc.Find(".timeline.commits-list-group-by-date .commit-sign-badge .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -105,7 +105,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { doc = NewHTMLParser(t, resp.Body) // Check if commit status is displayed in message column (.tippy-target to ignore the tippy trigger) - sel := doc.doc.Find("#commits-table .message .tippy-target .commit-status") + sel := doc.doc.Find(".timeline.commits-list-group-by-date .description .tippy-target .commit-status") assert.Equal(t, 1, sel.Length()) for _, class := range classes { assert.True(t, sel.HasClass(class)) @@ -181,7 +181,7 @@ func TestRepoCommitsStatusParallel(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") + commitURL, exists := doc.doc.Find(".timeline.commits-list-group-by-date .commit-sign-badge .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -216,7 +216,7 @@ func TestRepoCommitsStatusMultiple(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") + commitURL, exists := doc.doc.Find(".timeline.commits-list-group-by-date .commit-sign-badge .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -241,6 +241,6 @@ func TestRepoCommitsStatusMultiple(t *testing.T) { doc = NewHTMLParser(t, resp.Body) // Check that the data-global-init="initCommitStatuses" (for trigger) and commit-status (svg) are present - sel := doc.doc.Find(`#commits-table .message [data-global-init="initCommitStatuses"] .commit-status`) + sel := doc.doc.Find(`.timeline.commits-list-group-by-date .description [data-global-init="initCommitStatuses"] .commit-status`) assert.Equal(t, 1, sel.Length()) } diff --git a/tests/integration/view_test.go b/tests/integration/view_test.go index 9ed3e30857..8891ca432c 100644 --- a/tests/integration/view_test.go +++ b/tests/integration/view_test.go @@ -48,9 +48,9 @@ func TestCommitListActions(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - AssertHTMLElement(t, htmlDoc, `.commit-list .copy-commit-id`, true) - AssertHTMLElement(t, htmlDoc, `.commit-list .view-single-diff`, false) - AssertHTMLElement(t, htmlDoc, `.commit-list .view-commit-path`, true) + AssertHTMLElement(t, htmlDoc, `.timeline.commits-list-group-by-date .copy-commit-id`, true) + AssertHTMLElement(t, htmlDoc, `.timeline.commits-list-group-by-date .view-single-diff`, false) + AssertHTMLElement(t, htmlDoc, `.timeline.commits-list-group-by-date .view-commit-path`, true) }) t.Run("RepoFileHistory", func(t *testing.T) { @@ -60,8 +60,8 @@ func TestCommitListActions(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - AssertHTMLElement(t, htmlDoc, `.commit-list .copy-commit-id`, true) - AssertHTMLElement(t, htmlDoc, `.commit-list .view-single-diff`, true) - AssertHTMLElement(t, htmlDoc, `.commit-list .view-commit-path`, true) + AssertHTMLElement(t, htmlDoc, `.timeline.commits-list-group-by-date .copy-commit-id`, true) + AssertHTMLElement(t, htmlDoc, `.timeline.commits-list-group-by-date .view-single-diff`, true) + AssertHTMLElement(t, htmlDoc, `.timeline.commits-list-group-by-date .view-commit-path`, true) }) } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 306db34300..6bdbdda0a2 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2281,3 +2281,110 @@ tbody.commit-list { .branch-selector-dropdown .scrolling.menu .loading-indicator { height: 4em; } + +.commits-list-group-by-date.timeline { + display: flex; + margin-left: 16px; + position: relative; + padding-bottom: 6px; +} + +.commits-list-group-by-date.timeline[data-index="0"] { + margin-top: 6px; +} + +.commits-list-group-by-date.timeline::before { + background-color: var(--color-timeline); + bottom: 0; + content: ""; + display: block; + left: 0; + position: absolute; + top: 0; + width: 2px; +} + +.commits-list-group-by-date.timeline .timeline-badge-wrapper { + position: relative; + z-index: 1; +} + +.commits-list-group-by-date.timeline .timeline-badge { + border-style: solid; + border-radius: var(--border-radius-full); + color: var(--color-text); + background-color: var(--color-box-body); + float: left; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: -16px; + width: 34px; + height: 34px; + overflow: hidden; +} + +.commits-list-group-by-date.timeline .timeline-heading { + font-size: 15px; + font-weight: var(--font-weight-normal); + color: var(--color-text-light-2); + margin: 0; +} + +.commits-list-group-by-date.timeline .timeline-body { + max-width: 100%; + flex: auto; + padding-left: 5px; +} + +.commits-list-group-by-date.timeline .timeline-list-container { + border: 1px solid var(--color-secondary); + background: var(--color-box-body); + border-radius: var(--border-radius); +} + +.commits-list-group-by-date.timeline .commits-list { + list-style: none; + margin: 0; + padding: 0; + flex: auto; +} + +.commits-list-group-by-date.timeline .commits-list-item { + list-style: none; + display: grid; + gap: .5rem; + min-height: 2rem; + position: relative; + --core-grid-template-columns: minmax(30%, 1fr); + --last-grid-template-column: minmax(0, max-content); + grid-template-columns: var(--core-grid-template-columns) var(--last-grid-template-column); + grid-template-areas: "primary metadata" "main-content metadata"; + grid-template-rows: repeat(2, auto); +} + +.commits-list-group-by-date.timeline .commits-list-item:not(:last-child) { + border-bottom: 1px solid var(--color-secondary); +} + +.commits-list-group-by-date.timeline .commits-list-item:hover { + background-color: var(--color-hover) +} + +.commits-list-group-by-date.timeline .commits-list-item .title { + grid-area: primary; +} + +.commits-list-group-by-date.timeline .commits-list-item .description { + grid-area: main-content; +} + +.commits-list-group-by-date.timeline .commits-list-item .metadata { + grid-area: metadata; +} + +.commits-list-group-by-date.timeline .author img { + width: 16px; + height: 16px; +}