loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'feat: Add summary card for repos and releases' (#6269) from JakobDev/forgejo:repocard into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6269
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Reviewed-by: Otto <otto@codeberg.org>

Otto b01f3b9b 6cea7d7f

+735 -282
-14
models/issues/issue.go
··· 416 416 return fmt.Sprintf("%s/summary-card", issue.HTMLURL()) 417 417 } 418 418 419 - func (issue *Issue) SummaryCardSize() (int, int) { 420 - return 1200, 600 421 - } 422 - 423 - func (issue *Issue) SummaryCardWidth() int { 424 - width, _ := issue.SummaryCardSize() 425 - return width 426 - } 427 - 428 - func (issue *Issue) SummaryCardHeight() int { 429 - _, height := issue.SummaryCardSize() 430 - return height 431 - } 432 - 433 419 // Link returns the issue's relative URL. 434 420 func (issue *Issue) Link() string { 435 421 var path string
+49 -6
models/repo/release.go
··· 97 97 98 98 // LoadAttributes load repo and publisher attributes for a release 99 99 func (r *Release) LoadAttributes(ctx context.Context) error { 100 - var err error 101 - if r.Repo == nil { 102 - r.Repo, err = GetRepositoryByID(ctx, r.RepoID) 103 - if err != nil { 104 - return err 105 - } 100 + err := r.LoadRepo(ctx) 101 + if err != nil { 102 + return err 106 103 } 104 + 107 105 if r.Publisher == nil { 108 106 r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID) 109 107 if err != nil { ··· 123 121 return GetReleaseAttachments(ctx, r) 124 122 } 125 123 124 + // LoadRepo load repo attribute for release 125 + func (r *Release) LoadRepo(ctx context.Context) error { 126 + if r.Repo != nil { 127 + return nil 128 + } 129 + 130 + var err error 131 + r.Repo, err = GetRepositoryByID(ctx, r.RepoID) 132 + 133 + return err 134 + } 135 + 126 136 // LoadArchiveDownloadCount loads the download count for the source archives 127 137 func (r *Release) LoadArchiveDownloadCount(ctx context.Context) error { 128 138 var err error ··· 130 140 return err 131 141 } 132 142 143 + // GetTotalDownloadCount returns the summary of all dowload count of files attached to the release 144 + func (r *Release) GetTotalDownloadCount(ctx context.Context) (int64, error) { 145 + var archiveCount int64 146 + if !r.HideArchiveLinks { 147 + _, err := db.GetEngine(ctx).SQL("SELECT SUM(count) FROM repo_archive_download_count WHERE release_id = ?", r.ID).Get(&archiveCount) 148 + if err != nil { 149 + return 0, err 150 + } 151 + } 152 + 153 + var attachmentCount int64 154 + _, err := db.GetEngine(ctx).SQL("SELECT SUM(download_count) FROM attachment WHERE release_id = ?", r.ID).Get(&attachmentCount) 155 + if err != nil { 156 + return 0, err 157 + } 158 + 159 + return archiveCount + attachmentCount, nil 160 + } 161 + 133 162 // APIURL the api url for a release. release must have attributes loaded 134 163 func (r *Release) APIURL() string { 135 164 return r.Repo.APIURL() + "/releases/" + strconv.FormatInt(r.ID, 10) ··· 158 187 // Link the relative url for a release on the web UI. release must have attributes loaded 159 188 func (r *Release) Link() string { 160 189 return r.Repo.Link() + "/releases/tag/" + util.PathEscapeSegments(r.TagName) 190 + } 191 + 192 + // SummaryCardURL returns the absolute URL to an image providing a summary of the release 193 + func (r *Release) SummaryCardURL() string { 194 + return fmt.Sprintf("%s/releases/summary-card/%s", r.Repo.HTMLURL(), util.PathEscapeSegments(r.TagName)) 195 + } 196 + 197 + // DisplayName retruns the name of the release 198 + func (r *Release) DisplayName() string { 199 + if r.IsTag && r.Title == "" { 200 + return r.TagName 201 + } 202 + 203 + return r.Title 161 204 } 162 205 163 206 // IsReleaseExist returns true if release with given tag name already exists.
+24
models/repo/release_test.go
··· 9 9 "code.gitea.io/gitea/models/db" 10 10 "code.gitea.io/gitea/models/unittest" 11 11 12 + "github.com/stretchr/testify/assert" 12 13 "github.com/stretchr/testify/require" 13 14 ) 14 15 ··· 25 26 err := InsertReleases(db.DefaultContext, r) 26 27 require.NoError(t, err) 27 28 } 29 + 30 + func TestReleaseLoadRepo(t *testing.T) { 31 + require.NoError(t, unittest.PrepareTestDatabase()) 32 + 33 + release := unittest.AssertExistsAndLoadBean(t, &Release{ID: 1}) 34 + assert.Nil(t, release.Repo) 35 + 36 + require.NoError(t, release.LoadRepo(db.DefaultContext)) 37 + 38 + assert.EqualValues(t, 1, release.Repo.ID) 39 + } 40 + 41 + func TestReleaseDisplayName(t *testing.T) { 42 + release := Release{TagName: "TagName"} 43 + 44 + assert.Empty(t, release.DisplayName()) 45 + 46 + release.IsTag = true 47 + assert.Equal(t, "TagName", release.DisplayName()) 48 + 49 + release.Title = "Title" 50 + assert.Equal(t, "Title", release.DisplayName()) 51 + }
+5
models/repo/repo.go
··· 327 327 return setting.AppURL + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) 328 328 } 329 329 330 + // SummaryCardURL returns the absolute URL to an image providing a summary of the repo 331 + func (repo *Repository) SummaryCardURL() string { 332 + return fmt.Sprintf("%s/-/summary-card", repo.HTMLURL()) 333 + } 334 + 330 335 // CommitLink make link to by commit full ID 331 336 // note: won't check whether it's an right id 332 337 func (repo *Repository) CommitLink(commitID string) (result string) {
+25 -5
modules/card/card.go
··· 5 5 6 6 import ( 7 7 "bytes" 8 + "fmt" 8 9 "image" 9 10 "image/color" 10 11 "io" ··· 35 36 Img *image.RGBA 36 37 Font *truetype.Font 37 38 Margin int 39 + Width int 40 + Height int 38 41 } 39 42 40 43 var fontCache = sync.OnceValues(func() (*truetype.Font, error) { 41 44 return truetype.Parse(goregular.TTF) 42 45 }) 46 + 47 + // DefaultSize returns the default size for a card 48 + func DefaultSize() (int, int) { 49 + return 1200, 600 50 + } 43 51 44 52 // NewCard creates a new card with the given dimensions in pixels 45 53 func NewCard(width, height int) (*Card, error) { ··· 55 63 Img: img, 56 64 Font: font, 57 65 Margin: 0, 66 + Width: width, 67 + Height: height, 58 68 }, nil 59 69 } 60 70 ··· 67 77 mid := (bounds.Dx() * percentage / 100) + bounds.Min.X 68 78 subleft := c.Img.SubImage(image.Rect(bounds.Min.X, bounds.Min.Y, mid, bounds.Max.Y)).(*image.RGBA) 69 79 subright := c.Img.SubImage(image.Rect(mid, bounds.Min.Y, bounds.Max.X, bounds.Max.Y)).(*image.RGBA) 70 - return &Card{Img: subleft, Font: c.Font}, 71 - &Card{Img: subright, Font: c.Font} 80 + return &Card{Img: subleft, Font: c.Font, Width: subleft.Bounds().Dx(), Height: subleft.Bounds().Dy()}, 81 + &Card{Img: subright, Font: c.Font, Width: subright.Bounds().Dx(), Height: subright.Bounds().Dy()} 72 82 } 73 83 mid := (bounds.Dy() * percentage / 100) + bounds.Min.Y 74 84 subtop := c.Img.SubImage(image.Rect(bounds.Min.X, bounds.Min.Y, bounds.Max.X, mid)).(*image.RGBA) 75 85 subbottom := c.Img.SubImage(image.Rect(bounds.Min.X, mid, bounds.Max.X, bounds.Max.Y)).(*image.RGBA) 76 - return &Card{Img: subtop, Font: c.Font}, 77 - &Card{Img: subbottom, Font: c.Font} 86 + return &Card{Img: subtop, Font: c.Font, Width: subtop.Bounds().Dx(), Height: subtop.Bounds().Dy()}, 87 + &Card{Img: subbottom, Font: c.Font, Width: subbottom.Bounds().Dx(), Height: subbottom.Bounds().Dy()} 78 88 } 79 89 80 90 // SetMargin sets the margins for the card ··· 244 254 }, 245 255 } 246 256 257 + // Go expects a absolute URL, so we must change a relative to an absolute one 258 + if !strings.Contains(url, "://") { 259 + url = fmt.Sprintf("%s%s", setting.AppURL, strings.TrimPrefix(url, "/")) 260 + } 261 + 247 262 resp, err := client.Get(url) 248 263 if err != nil { 249 - log.Warn("error when fetching external image from %s: %w", url, err) 264 + log.Warn("error when fetching external image from %s: %v", url, err) 250 265 return nil, false 251 266 } 252 267 defer resp.Body.Close() ··· 321 336 } 322 337 c.DrawImage(image) 323 338 } 339 + 340 + // DrawRect draws a rect with the given color 341 + func (c *Card) DrawRect(startX, startY, endX, endY int, color color.Color) { 342 + draw.Draw(c.Img, image.Rect(startX, startY, endX, endY), &image.Uniform{color}, image.Point{}, draw.Src) 343 + }
+2
options/locale/locale_en-US.ini
··· 1155 1155 blame.ignore_revs = Ignoring revisions in <a href="%s">.git-blame-ignore-revs</a>. Click <a href="%s">here to bypass</a> and see the normal blame view. 1156 1156 blame.ignore_revs.failed = Failed to ignore revisions in <a href="%s">.git-blame-ignore-revs</a>. 1157 1157 author_search_tooltip = Shows a maximum of 30 users 1158 + summary_card_alt = Summary card of repository %s 1158 1159 1159 1160 tree_path_not_found_commit = Path %[1]s doesn't exist in commit %[2]s 1160 1161 tree_path_not_found_branch = Path %[1]s doesn't exist in branch %[2]s ··· 2755 2756 release.asset_external_url = External URL 2756 2757 release.add_external_asset = Add external asset 2757 2758 release.invalid_external_url = Invalid external URL: "%s" 2759 + release.summary_card_alt = Summary card of an release titled "%s" in repository %s 2758 2760 2759 2761 branch.name = Branch name 2760 2762 branch.already_exists = A branch named "%s" already exists.
+526
routers/web/repo/card.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + package repo 5 + 6 + import ( 7 + "bytes" 8 + "encoding/hex" 9 + "fmt" 10 + "image" 11 + "image/color" 12 + "image/png" 13 + "net/http" 14 + "strconv" 15 + "strings" 16 + "time" 17 + 18 + "code.gitea.io/gitea/models/db" 19 + issue_model "code.gitea.io/gitea/models/issues" 20 + repo_model "code.gitea.io/gitea/models/repo" 21 + unit_model "code.gitea.io/gitea/models/unit" 22 + user_model "code.gitea.io/gitea/models/user" 23 + "code.gitea.io/gitea/modules/cache" 24 + "code.gitea.io/gitea/modules/card" 25 + "code.gitea.io/gitea/modules/log" 26 + "code.gitea.io/gitea/modules/setting" 27 + "code.gitea.io/gitea/modules/storage" 28 + "code.gitea.io/gitea/services/context" 29 + ) 30 + 31 + // drawUser draws a user avatar in a summary card 32 + func drawUser(ctx *context.Context, card *card.Card, user *user_model.User) error { 33 + if user.UseCustomAvatar { 34 + posterAvatarPath := user.CustomAvatarRelativePath() 35 + if posterAvatarPath != "" { 36 + userAvatarFile, err := storage.Avatars.Open(user.CustomAvatarRelativePath()) 37 + if err != nil { 38 + return err 39 + } 40 + userAvatarImage, _, err := image.Decode(userAvatarFile) 41 + if err != nil { 42 + return err 43 + } 44 + card.DrawImage(userAvatarImage) 45 + } 46 + } else { 47 + posterAvatarLink := user.AvatarLinkWithSize(ctx, 256) 48 + card.DrawExternalImage(posterAvatarLink) 49 + } 50 + return nil 51 + } 52 + 53 + // drawRepoIcon draws the repo icon in a summary card 54 + func drawRepoIcon(ctx *context.Context, card *card.Card, repo *repo_model.Repository) error { 55 + repoAvatarPath := repo.CustomAvatarRelativePath() 56 + 57 + if repoAvatarPath != "" { 58 + repoAvatarFile, err := storage.RepoAvatars.Open(repoAvatarPath) 59 + if err != nil { 60 + return err 61 + } 62 + repoAvatarImage, _, err := image.Decode(repoAvatarFile) 63 + if err != nil { 64 + return err 65 + } 66 + card.DrawImage(repoAvatarImage) 67 + return nil 68 + } 69 + 70 + // If the repo didn't have an avatar, fallback to the repo owner's avatar for the right-hand-side icon 71 + err := repo.LoadOwner(ctx) 72 + if err != nil { 73 + return err 74 + } 75 + if repo.Owner != nil { 76 + err = drawUser(ctx, card, repo.Owner) 77 + if err != nil { 78 + return err 79 + } 80 + } 81 + 82 + return nil 83 + } 84 + 85 + // hexToColor converts a hex color to a go color 86 + func hexToColor(colorStr string) (*color.RGBA, error) { 87 + colorStr = strings.TrimLeft(colorStr, "#") 88 + 89 + b, err := hex.DecodeString(colorStr) 90 + if err != nil { 91 + return nil, err 92 + } 93 + 94 + if len(b) < 3 { 95 + return nil, fmt.Errorf("expected at least 3 bytes from DecodeString, got %d", len(b)) 96 + } 97 + 98 + color := color.RGBA{b[0], b[1], b[2], 255} 99 + 100 + return &color, nil 101 + } 102 + 103 + func drawLanguagesCard(ctx *context.Context, card *card.Card) error { 104 + languageList, err := repo_model.GetTopLanguageStats(ctx, ctx.Repo.Repository, 5) 105 + if err != nil { 106 + return err 107 + } 108 + if len(languageList) == 0 { 109 + card.DrawRect(0, 0, card.Width, card.Height, color.White) 110 + return nil 111 + } 112 + 113 + currentX := 0 114 + var langColor *color.RGBA 115 + 116 + for _, lang := range languageList { 117 + langColor, err = hexToColor(lang.Color) 118 + if err != nil { 119 + return err 120 + } 121 + 122 + langWidth := float32(card.Width) * (lang.Percentage / 100) 123 + card.DrawRect(currentX, 0, currentX+int(langWidth), card.Width, langColor) 124 + currentX += int(langWidth) 125 + } 126 + 127 + if currentX < card.Width { 128 + card.DrawRect(currentX, 0, card.Width, card.Height, langColor) 129 + } 130 + 131 + return nil 132 + } 133 + 134 + func drawRepoSummaryCard(ctx *context.Context, repo *repo_model.Repository) (*card.Card, error) { 135 + width, height := card.DefaultSize() 136 + mainCard, err := card.NewCard(width, height) 137 + if err != nil { 138 + return nil, err 139 + } 140 + 141 + contentCard, languageBarCard := mainCard.Split(false, 90) 142 + 143 + contentCard.SetMargin(60) 144 + topSection, bottomSection := contentCard.Split(false, 75) 145 + issueSummary, issueIcon := topSection.Split(true, 80) 146 + repoInfo, issueDescription := issueSummary.Split(false, 30) 147 + 148 + repoInfo.SetMargin(10) 149 + _, err = repoInfo.DrawText(repo.FullName(), color.Black, 56, card.Top, card.Left) 150 + if err != nil { 151 + return nil, err 152 + } 153 + 154 + issueDescription.SetMargin(10) 155 + _, err = issueDescription.DrawText(repo.Description, color.Gray{128}, 36, card.Top, card.Left) 156 + if err != nil { 157 + return nil, err 158 + } 159 + 160 + issueIcon.SetMargin(10) 161 + err = drawRepoIcon(ctx, issueIcon, repo) 162 + if err != nil { 163 + return nil, err 164 + } 165 + 166 + topCountCard, bottomCountCard := bottomSection.Split(false, 50) 167 + 168 + releaseCount, err := db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{ 169 + // only show draft releases for users who can write, read-only users shouldn't see draft releases. 170 + IncludeDrafts: ctx.Repo.CanWrite(unit_model.TypeReleases), 171 + RepoID: ctx.Repo.Repository.ID, 172 + }) 173 + if err != nil { 174 + return nil, err 175 + } 176 + 177 + starsText := ctx.Locale.TrN( 178 + repo.NumStars, 179 + "explore.stars_one", 180 + "explore.stars_few", 181 + repo.NumStars, 182 + ) 183 + forksText := ctx.Locale.TrN( 184 + repo.NumForks, 185 + "explore.forks_one", 186 + "explore.forks_few", 187 + repo.NumForks, 188 + ) 189 + releasesText := ctx.Locale.TrN( 190 + releaseCount, 191 + "repo.activity.title.releases_1", 192 + "repo.activity.title.releases_n", 193 + releaseCount, 194 + ) 195 + 196 + topCountText := fmt.Sprintf("%s • %s • %s", starsText, forksText, releasesText) 197 + 198 + topCountCard.SetMargin(10) 199 + _, err = topCountCard.DrawText(topCountText, color.Gray{128}, 36, card.Top, card.Left) 200 + if err != nil { 201 + return nil, err 202 + } 203 + 204 + issuesText := ctx.Locale.TrN( 205 + repo.NumOpenIssues, 206 + "repo.activity.title.issues_1", 207 + "repo.activity.title.issues_n", 208 + repo.NumOpenIssues, 209 + ) 210 + pullRequestsText := ctx.Locale.TrN( 211 + repo.NumOpenPulls, 212 + "repo.activity.title.prs_1", 213 + "repo.activity.title.prs_n", 214 + repo.NumOpenPulls, 215 + ) 216 + 217 + bottomCountText := fmt.Sprintf("%s • %s", issuesText, pullRequestsText) 218 + 219 + bottomCountCard.SetMargin(10) 220 + _, err = bottomCountCard.DrawText(bottomCountText, color.Gray{128}, 36, card.Top, card.Left) 221 + if err != nil { 222 + return nil, err 223 + } 224 + 225 + err = drawLanguagesCard(ctx, languageBarCard) 226 + if err != nil { 227 + return nil, err 228 + } 229 + 230 + return mainCard, nil 231 + } 232 + 233 + func drawIssueSummaryCard(ctx *context.Context, issue *issue_model.Issue) (*card.Card, error) { 234 + width, height := card.DefaultSize() 235 + mainCard, err := card.NewCard(width, height) 236 + if err != nil { 237 + return nil, err 238 + } 239 + 240 + mainCard.SetMargin(60) 241 + topSection, bottomSection := mainCard.Split(false, 75) 242 + issueSummary, issueIcon := topSection.Split(true, 80) 243 + repoInfo, issueDescription := issueSummary.Split(false, 15) 244 + 245 + repoInfo.SetMargin(10) 246 + _, err = repoInfo.DrawText(fmt.Sprintf("%s - #%d", issue.Repo.FullName(), issue.Index), color.Gray{128}, 36, card.Top, card.Left) 247 + if err != nil { 248 + return nil, err 249 + } 250 + 251 + issueDescription.SetMargin(10) 252 + _, err = issueDescription.DrawText(issue.Title, color.Black, 56, card.Top, card.Left) 253 + if err != nil { 254 + return nil, err 255 + } 256 + 257 + issueIcon.SetMargin(10) 258 + err = drawRepoIcon(ctx, issueIcon, issue.Repo) 259 + if err != nil { 260 + return nil, err 261 + } 262 + 263 + issueStats, issueAttribution := bottomSection.Split(false, 50) 264 + 265 + var state string 266 + if issue.IsPull && issue.PullRequest.HasMerged { 267 + if issue.PullRequest.Status == 3 { 268 + state = ctx.Locale.TrString("repo.pulls.manually_merged") 269 + } else { 270 + state = ctx.Locale.TrString("repo.pulls.merged") 271 + } 272 + } else if issue.IsClosed { 273 + state = ctx.Locale.TrString("repo.issues.closed_title") 274 + } else if issue.IsPull { 275 + if issue.PullRequest.IsWorkInProgress(ctx) { 276 + state = ctx.Locale.TrString("repo.issues.draft_title") 277 + } else { 278 + state = ctx.Locale.TrString("repo.issues.open_title") 279 + } 280 + } else { 281 + state = ctx.Locale.TrString("repo.issues.open_title") 282 + } 283 + state = strings.ToLower(state) 284 + 285 + issueStats.SetMargin(10) 286 + if issue.IsPull { 287 + reviews := map[int64]bool{} 288 + for _, comment := range issue.Comments { 289 + if comment.Review != nil { 290 + reviews[comment.Review.ID] = true 291 + } 292 + } 293 + _, err = issueStats.DrawText( 294 + fmt.Sprintf("%s, %s, %s", 295 + ctx.Locale.TrN( 296 + issue.NumComments, 297 + "repo.issues.num_comments_1", 298 + "repo.issues.num_comments", 299 + issue.NumComments, 300 + ), 301 + ctx.Locale.TrN( 302 + len(reviews), 303 + "repo.issues.num_reviews_one", 304 + "repo.issues.num_reviews_few", 305 + len(reviews), 306 + ), 307 + state, 308 + ), 309 + color.Gray{128}, 36, card.Top, card.Left) 310 + } else { 311 + _, err = issueStats.DrawText( 312 + fmt.Sprintf("%s, %s", 313 + ctx.Locale.TrN( 314 + issue.NumComments, 315 + "repo.issues.num_comments_1", 316 + "repo.issues.num_comments", 317 + issue.NumComments, 318 + ), 319 + state, 320 + ), 321 + color.Gray{128}, 36, card.Top, card.Left) 322 + } 323 + if err != nil { 324 + return nil, err 325 + } 326 + 327 + issueAttributionIcon, issueAttributionText := issueAttribution.Split(true, 8) 328 + issueAttributionText.SetMargin(5) 329 + _, err = issueAttributionText.DrawText( 330 + fmt.Sprintf( 331 + "%s - %s", 332 + issue.Poster.Name, 333 + issue.Created.AsTime().Format(time.DateOnly), 334 + ), 335 + color.Gray{128}, 36, card.Middle, card.Left) 336 + if err != nil { 337 + return nil, err 338 + } 339 + err = drawUser(ctx, issueAttributionIcon, issue.Poster) 340 + if err != nil { 341 + return nil, err 342 + } 343 + 344 + return mainCard, nil 345 + } 346 + 347 + func drawReleaseSummaryCard(ctx *context.Context, release *repo_model.Release) (*card.Card, error) { 348 + width, height := card.DefaultSize() 349 + mainCard, err := card.NewCard(width, height) 350 + if err != nil { 351 + return nil, err 352 + } 353 + 354 + mainCard.SetMargin(60) 355 + topSection, bottomSection := mainCard.Split(false, 75) 356 + releaseSummary, repoIcon := topSection.Split(true, 80) 357 + repoInfo, releaseDescription := releaseSummary.Split(false, 15) 358 + 359 + repoInfo.SetMargin(10) 360 + _, err = repoInfo.DrawText(release.Repo.FullName(), color.Gray{128}, 36, card.Top, card.Left) 361 + if err != nil { 362 + return nil, err 363 + } 364 + 365 + releaseDescription.SetMargin(10) 366 + _, err = releaseDescription.DrawText(release.DisplayName(), color.Black, 56, card.Top, card.Left) 367 + if err != nil { 368 + return nil, err 369 + } 370 + 371 + repoIcon.SetMargin(10) 372 + err = drawRepoIcon(ctx, repoIcon, release.Repo) 373 + if err != nil { 374 + return nil, err 375 + } 376 + 377 + downloadCountCard, releaseDateCard := bottomSection.Split(true, 75) 378 + 379 + downloadCount, err := release.GetTotalDownloadCount(ctx) 380 + if err != nil { 381 + return nil, err 382 + } 383 + 384 + downloadCountText := ctx.Locale.TrN( 385 + strconv.FormatInt(downloadCount, 10), 386 + "repo.release.download_count_one", 387 + "repo.release.download_count_few", 388 + strconv.FormatInt(downloadCount, 10), 389 + ) 390 + 391 + _, err = downloadCountCard.DrawText(string(downloadCountText), color.Gray{128}, 36, card.Bottom, card.Left) 392 + if err != nil { 393 + return nil, err 394 + } 395 + 396 + _, err = releaseDateCard.DrawText(release.CreatedUnix.AsTime().Format(time.DateOnly), color.Gray{128}, 36, card.Bottom, card.Left) 397 + if err != nil { 398 + return nil, err 399 + } 400 + 401 + return mainCard, nil 402 + } 403 + 404 + // checkCardCache checks if a card in cache and serves it 405 + func checkCardCache(ctx *context.Context, cacheKey string) bool { 406 + cache := cache.GetCache() 407 + pngData, ok := cache.Get(cacheKey).([]byte) 408 + if ok && pngData != nil && len(pngData) > 0 { 409 + ctx.Resp.Header().Set("Content-Type", "image/png") 410 + ctx.Resp.WriteHeader(http.StatusOK) 411 + _, err := ctx.Resp.Write(pngData) 412 + if err != nil { 413 + ctx.ServerError("GetSummaryCard", err) 414 + } 415 + return true 416 + } 417 + 418 + return false 419 + } 420 + 421 + // serveCard server a Card to the user adds it to the cache 422 + func serveCard(ctx *context.Context, card *card.Card, cacheKey string) { 423 + cache := cache.GetCache() 424 + 425 + // Encode image, store in cache 426 + var imageBuffer bytes.Buffer 427 + err := png.Encode(&imageBuffer, card.Img) 428 + if err != nil { 429 + ctx.ServerError("GetSummaryCard", err) 430 + return 431 + } 432 + imageBytes := imageBuffer.Bytes() 433 + err = cache.Put(cacheKey, imageBytes, setting.CacheService.TTLSeconds()) 434 + if err != nil { 435 + // don't abort serving the image if we just had a cache storage failure 436 + log.Warn("failed to cache issue summary card: %v", err) 437 + } 438 + 439 + // Finish the uncached image response 440 + ctx.Resp.Header().Set("Content-Type", "image/png") 441 + ctx.Resp.WriteHeader(http.StatusOK) 442 + _, err = ctx.Resp.Write(imageBytes) 443 + if err != nil { 444 + ctx.ServerError("GetSummaryCard", err) 445 + return 446 + } 447 + } 448 + 449 + func DrawRepoSummaryCard(ctx *context.Context) { 450 + cacheKey := fmt.Sprintf("summary_card:repo:%s:%d", ctx.Locale.Language(), ctx.Repo.Repository.ID) 451 + 452 + if checkCardCache(ctx, cacheKey) { 453 + return 454 + } 455 + 456 + card, err := drawRepoSummaryCard(ctx, ctx.Repo.Repository) 457 + if err != nil { 458 + ctx.ServerError("drawRepoSummaryCar", err) 459 + return 460 + } 461 + 462 + serveCard(ctx, card, cacheKey) 463 + } 464 + 465 + func DrawIssueSummaryCard(ctx *context.Context) { 466 + issue, err := issue_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 467 + if err != nil { 468 + if issue_model.IsErrIssueNotExist(err) { 469 + ctx.Error(http.StatusNotFound) 470 + } else { 471 + ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) 472 + } 473 + return 474 + } 475 + 476 + if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { 477 + ctx.Error(http.StatusNotFound) 478 + return 479 + } 480 + 481 + cacheKey := fmt.Sprintf("summary_card:issue:%s:%d", ctx.Locale.Language(), issue.ID) 482 + 483 + if checkCardCache(ctx, cacheKey) { 484 + return 485 + } 486 + 487 + card, err := drawIssueSummaryCard(ctx, issue) 488 + if err != nil { 489 + ctx.ServerError("drawIssueSummaryCar", err) 490 + return 491 + } 492 + 493 + serveCard(ctx, card, cacheKey) 494 + } 495 + 496 + func DrawReleaseSummaryCard(ctx *context.Context) { 497 + release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Params("*")) 498 + if err != nil { 499 + if repo_model.IsErrReleaseNotExist(err) { 500 + ctx.NotFound("", nil) 501 + } else { 502 + ctx.ServerError("GetReleaseForRepoByID", err) 503 + } 504 + return 505 + } 506 + 507 + err = release.LoadRepo(ctx) 508 + if err != nil { 509 + ctx.ServerError("LoadRepo", err) 510 + return 511 + } 512 + 513 + cacheKey := fmt.Sprintf("summary_card:release:%s:%d", ctx.Locale.Language(), release.ID) 514 + 515 + if checkCardCache(ctx, cacheKey) { 516 + return 517 + } 518 + 519 + card, err := drawReleaseSummaryCard(ctx, release) 520 + if err != nil { 521 + ctx.ServerError("drawRepoSummaryCar", err) 522 + return 523 + } 524 + 525 + serveCard(ctx, card, cacheKey) 526 + }
+2 -222
routers/web/repo/issue.go
··· 10 10 "errors" 11 11 "fmt" 12 12 "html/template" 13 - "image" 14 - "image/color" 15 - "image/png" 16 13 "math/big" 17 14 "net/http" 18 15 "net/url" ··· 34 31 "code.gitea.io/gitea/models/unit" 35 32 user_model "code.gitea.io/gitea/models/user" 36 33 "code.gitea.io/gitea/modules/base" 37 - "code.gitea.io/gitea/modules/cache" 38 - "code.gitea.io/gitea/modules/card" 39 34 "code.gitea.io/gitea/modules/container" 40 35 "code.gitea.io/gitea/modules/emoji" 41 36 "code.gitea.io/gitea/modules/git" ··· 47 42 "code.gitea.io/gitea/modules/optional" 48 43 repo_module "code.gitea.io/gitea/modules/repository" 49 44 "code.gitea.io/gitea/modules/setting" 50 - "code.gitea.io/gitea/modules/storage" 51 45 api "code.gitea.io/gitea/modules/structs" 52 46 "code.gitea.io/gitea/modules/templates" 53 47 "code.gitea.io/gitea/modules/templates/vars" ··· 2076 2070 ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName() 2077 2071 ctx.Data["NewPinAllowed"] = pinAllowed 2078 2072 ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0 2073 + ctx.Data["OpenGraphImageURL"] = issue.SummaryCardURL() 2074 + ctx.Data["OpenGraphImageAltText"] = ctx.Tr("repo.issues.summary_card_alt", issue.Title, issue.Repo.FullName()) 2079 2075 2080 2076 prepareHiddenCommentType(ctx) 2081 2077 if ctx.Written() { ··· 2231 2227 } 2232 2228 2233 2229 ctx.JSON(http.StatusOK, convert.ToIssue(ctx, ctx.Doer, issue)) 2234 - } 2235 - 2236 - // GetSummaryCard get an issue of a repository 2237 - func GetSummaryCard(ctx *context.Context) { 2238 - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 2239 - if err != nil { 2240 - if issues_model.IsErrIssueNotExist(err) { 2241 - ctx.Error(http.StatusNotFound) 2242 - } else { 2243 - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) 2244 - } 2245 - return 2246 - } 2247 - 2248 - if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { 2249 - ctx.Error(http.StatusNotFound) 2250 - return 2251 - } 2252 - 2253 - cache := cache.GetCache() 2254 - cacheKey := fmt.Sprintf("summary_card:issue:%s:%d", ctx.Locale.Language(), issue.ID) 2255 - pngData, ok := cache.Get(cacheKey).([]byte) 2256 - if ok && pngData != nil && len(pngData) > 0 { 2257 - ctx.Resp.Header().Set("Content-Type", "image/png") 2258 - ctx.Resp.WriteHeader(http.StatusOK) 2259 - _, err = ctx.Resp.Write(pngData) 2260 - if err != nil { 2261 - ctx.ServerError("GetSummaryCard", err) 2262 - } 2263 - return 2264 - } 2265 - 2266 - card, err := drawSummaryCard(ctx, issue) 2267 - if err != nil { 2268 - ctx.ServerError("GetSummaryCard", err) 2269 - return 2270 - } 2271 - 2272 - // Encode image, store in cache 2273 - var imageBuffer bytes.Buffer 2274 - err = png.Encode(&imageBuffer, card.Img) 2275 - if err != nil { 2276 - ctx.ServerError("GetSummaryCard", err) 2277 - return 2278 - } 2279 - imageBytes := imageBuffer.Bytes() 2280 - err = cache.Put(cacheKey, imageBytes, setting.CacheService.TTLSeconds()) 2281 - if err != nil { 2282 - // don't abort serving the image if we just had a cache storage failure 2283 - log.Warn("failed to cache issue summary card: %v", err) 2284 - } 2285 - 2286 - // Finish the uncached image response 2287 - ctx.Resp.Header().Set("Content-Type", "image/png") 2288 - ctx.Resp.WriteHeader(http.StatusOK) 2289 - _, err = ctx.Resp.Write(imageBytes) 2290 - if err != nil { 2291 - ctx.ServerError("GetSummaryCard", err) 2292 - return 2293 - } 2294 - } 2295 - 2296 - func drawSummaryCard(ctx *context.Context, issue *issues_model.Issue) (*card.Card, error) { 2297 - width, height := issue.SummaryCardSize() 2298 - mainCard, err := card.NewCard(width, height) 2299 - if err != nil { 2300 - return nil, err 2301 - } 2302 - 2303 - mainCard.SetMargin(60) 2304 - topSection, bottomSection := mainCard.Split(false, 75) 2305 - issueSummary, issueIcon := topSection.Split(true, 80) 2306 - repoInfo, issueDescription := issueSummary.Split(false, 15) 2307 - 2308 - repoInfo.SetMargin(10) 2309 - _, err = repoInfo.DrawText(fmt.Sprintf("%s - #%d", issue.Repo.FullName(), issue.Index), color.Gray{128}, 36, card.Top, card.Left) 2310 - if err != nil { 2311 - return nil, err 2312 - } 2313 - 2314 - issueDescription.SetMargin(10) 2315 - _, err = issueDescription.DrawText(issue.Title, color.Black, 56, card.Top, card.Left) 2316 - if err != nil { 2317 - return nil, err 2318 - } 2319 - 2320 - issueIcon.SetMargin(10) 2321 - 2322 - repoAvatarPath := issue.Repo.CustomAvatarRelativePath() 2323 - if repoAvatarPath != "" { 2324 - repoAvatarFile, err := storage.RepoAvatars.Open(repoAvatarPath) 2325 - if err != nil { 2326 - return nil, err 2327 - } 2328 - repoAvatarImage, _, err := image.Decode(repoAvatarFile) 2329 - if err != nil { 2330 - return nil, err 2331 - } 2332 - issueIcon.DrawImage(repoAvatarImage) 2333 - } else { 2334 - // If the repo didn't have an avatar, fallback to the repo owner's avatar for the right-hand-side icon 2335 - err = issue.Repo.LoadOwner(ctx) 2336 - if err != nil { 2337 - return nil, err 2338 - } 2339 - if issue.Repo.Owner != nil { 2340 - err = drawUser(ctx, issueIcon, issue.Repo.Owner) 2341 - if err != nil { 2342 - return nil, err 2343 - } 2344 - } 2345 - } 2346 - 2347 - issueStats, issueAttribution := bottomSection.Split(false, 50) 2348 - 2349 - var state string 2350 - if issue.IsPull && issue.PullRequest.HasMerged { 2351 - if issue.PullRequest.Status == 3 { 2352 - state = ctx.Locale.TrString("repo.pulls.manually_merged") 2353 - } else { 2354 - state = ctx.Locale.TrString("repo.pulls.merged") 2355 - } 2356 - } else if issue.IsClosed { 2357 - state = ctx.Locale.TrString("repo.issues.closed_title") 2358 - } else if issue.IsPull { 2359 - if issue.PullRequest.IsWorkInProgress(ctx) { 2360 - state = ctx.Locale.TrString("repo.issues.draft_title") 2361 - } else { 2362 - state = ctx.Locale.TrString("repo.issues.open_title") 2363 - } 2364 - } else { 2365 - state = ctx.Locale.TrString("repo.issues.open_title") 2366 - } 2367 - state = strings.ToLower(state) 2368 - 2369 - issueStats.SetMargin(10) 2370 - if issue.IsPull { 2371 - reviews := map[int64]bool{} 2372 - for _, comment := range issue.Comments { 2373 - if comment.Review != nil { 2374 - reviews[comment.Review.ID] = true 2375 - } 2376 - } 2377 - _, err = issueStats.DrawText( 2378 - fmt.Sprintf("%s, %s, %s", 2379 - ctx.Locale.TrN( 2380 - issue.NumComments, 2381 - "repo.issues.num_comments_1", 2382 - "repo.issues.num_comments", 2383 - issue.NumComments, 2384 - ), 2385 - ctx.Locale.TrN( 2386 - len(reviews), 2387 - "repo.issues.num_reviews_one", 2388 - "repo.issues.num_reviews_few", 2389 - len(reviews), 2390 - ), 2391 - state, 2392 - ), 2393 - color.Gray{128}, 36, card.Top, card.Left) 2394 - } else { 2395 - _, err = issueStats.DrawText( 2396 - fmt.Sprintf("%s, %s", 2397 - ctx.Locale.TrN( 2398 - issue.NumComments, 2399 - "repo.issues.num_comments_1", 2400 - "repo.issues.num_comments", 2401 - issue.NumComments, 2402 - ), 2403 - state, 2404 - ), 2405 - color.Gray{128}, 36, card.Top, card.Left) 2406 - } 2407 - if err != nil { 2408 - return nil, err 2409 - } 2410 - 2411 - issueAttributionIcon, issueAttributionText := issueAttribution.Split(true, 8) 2412 - issueAttributionText.SetMargin(5) 2413 - _, err = issueAttributionText.DrawText( 2414 - fmt.Sprintf( 2415 - "%s - %s", 2416 - issue.Poster.Name, 2417 - issue.Created.AsTime().Format("2006-01-02"), 2418 - ), 2419 - color.Gray{128}, 36, card.Middle, card.Left) 2420 - if err != nil { 2421 - return nil, err 2422 - } 2423 - err = drawUser(ctx, issueAttributionIcon, issue.Poster) 2424 - if err != nil { 2425 - return nil, err 2426 - } 2427 - 2428 - return mainCard, nil 2429 - } 2430 - 2431 - func drawUser(ctx *context.Context, card *card.Card, user *user_model.User) error { 2432 - if user.UseCustomAvatar { 2433 - posterAvatarPath := user.CustomAvatarRelativePath() 2434 - if posterAvatarPath != "" { 2435 - userAvatarFile, err := storage.Avatars.Open(user.CustomAvatarRelativePath()) 2436 - if err != nil { 2437 - return err 2438 - } 2439 - userAvatarImage, _, err := image.Decode(userAvatarFile) 2440 - if err != nil { 2441 - return err 2442 - } 2443 - card.DrawImage(userAvatarImage) 2444 - } 2445 - } else { 2446 - posterAvatarLink := user.AvatarLinkWithSize(ctx, 256) 2447 - card.DrawExternalImage(posterAvatarLink) 2448 - } 2449 - return nil 2450 2230 } 2451 2231 2452 2232 // UpdateIssueTitle change issue's title
+8 -5
routers/web/repo/release.go
··· 365 365 addVerifyTagToContext(ctx) 366 366 367 367 ctx.Data["PageIsSingleTag"] = release.IsTag 368 - if release.IsTag { 369 - ctx.Data["Title"] = release.TagName 370 - } else { 371 - ctx.Data["Title"] = release.Title 372 - } 368 + ctx.Data["Title"] = release.DisplayName() 373 369 374 370 err = release.LoadArchiveDownloadCount(ctx) 375 371 if err != nil { ··· 378 374 } 379 375 380 376 ctx.Data["Releases"] = releases 377 + 378 + ctx.Data["OpenGraphTitle"] = fmt.Sprintf("%s - %s", release.DisplayName(), release.Repo.FullName()) 379 + ctx.Data["OpenGraphDescription"] = base.EllipsisString(release.Note, 300) 380 + ctx.Data["OpenGraphURL"] = release.HTMLURL() 381 + ctx.Data["OpenGraphImageURL"] = release.SummaryCardURL() 382 + ctx.Data["OpenGraphImageAltText"] = ctx.Tr("repo.release.summary_card_alt", release.DisplayName(), release.Repo.FullName()) 383 + 381 384 ctx.HTML(http.StatusOK, tplReleasesList) 382 385 } 383 386
+3 -1
routers/web/web.go
··· 1146 1146 m.Group("/{type:issues|pulls}", func() { 1147 1147 m.Group("/{index}", func() { 1148 1148 m.Get("/info", repo.GetIssueInfo) 1149 - m.Get("/summary-card", repo.GetSummaryCard) 1149 + m.Get("/summary-card", repo.DrawIssueSummaryCard) 1150 1150 }) 1151 1151 }) 1152 + m.Get("/-/summary-card", repo.DrawRepoSummaryCard) 1152 1153 }, ignSignIn, context.RepoAssignment, context.UnitTypes()) // for "/{username}/{reponame}" which doesn't require authentication 1153 1154 1154 1155 // Grouping for those endpoints that do require authentication ··· 1298 1299 m.Get("/latest", repo.LatestRelease) 1299 1300 m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS) 1300 1301 m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom) 1302 + m.Get("/summary-card/*", repo.DrawReleaseSummaryCard) 1301 1303 }, ctxDataSet("EnableFeed", setting.Other.EnableFeed), 1302 1304 repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, true)) 1303 1305 m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment)
+7
services/context/repo.go
··· 25 25 unit_model "code.gitea.io/gitea/models/unit" 26 26 user_model "code.gitea.io/gitea/models/user" 27 27 "code.gitea.io/gitea/modules/cache" 28 + "code.gitea.io/gitea/modules/card" 28 29 "code.gitea.io/gitea/modules/git" 29 30 "code.gitea.io/gitea/modules/gitrepo" 30 31 code_indexer "code.gitea.io/gitea/modules/indexer/code" ··· 631 632 ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, repo.ID) 632 633 ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID) 633 634 } 635 + 636 + cardWidth, cardHeight := card.DefaultSize() 637 + ctx.Data["OpenGraphImageURL"] = repo.SummaryCardURL() 638 + ctx.Data["OpenGraphImageWidth"] = cardWidth 639 + ctx.Data["OpenGraphImageHeight"] = cardHeight 640 + ctx.Data["OpenGraphImageAltText"] = ctx.Tr("repo.summary_card_alt", repo.FullName()) 634 641 635 642 if repo.IsFork { 636 643 RetrieveBaseRepo(ctx, repo)
+29 -8
templates/base/head_opengraph.tmpl
··· 1 1 {{- /* og:description - a one to two sentence description of your object, maybe it only needs at most 300 bytes */ -}} 2 + {{if .OpenGraphTitle}} 3 + <meta property="og:title" content="{{.OpenGraphTitle}}"> 4 + {{end}} 5 + {{if .OpenGraphDescription}} 6 + <meta property="og:description" content="{{.OpenGraphDescription}}"> 7 + {{end}} 8 + {{if .OpenGraphURL}} 9 + <meta property="og:url" content="{{.OpenGraphURL}}"> 10 + {{end}} 11 + {{if .OpenGraphImageURL}} 12 + <meta property="og:image" content="{{.OpenGraphImageURL}}"> 13 + {{if .OpenGraphImageWidth}} 14 + <meta property="og:image:width" content="{{.OpenGraphImageWidth}}"> 15 + {{end}} 16 + {{if .OpenGraphImageHeight}} 17 + <meta property="og:image:height" content="{{.OpenGraphImageHeight}}"> 18 + {{end}} 19 + {{if .OpenGraphImageAltText}} 20 + <meta property="og:image:alt" content="{{.OpenGraphImageAltText}}"> 21 + {{end}} 22 + {{end}} 2 23 {{if .PageIsUserProfile}} 3 24 <meta property="og:title" content="{{.ContextUser.DisplayName}}"> 4 25 <meta property="og:type" content="profile"> ··· 14 35 {{if .Issue.Content}} 15 36 <meta property="og:description" content="{{StringUtils.EllipsisString .Issue.Content 300}}"> 16 37 {{end}} 17 - <meta property="og:image" content="{{.Issue.SummaryCardURL}}"> 18 - <meta property="og:image:width" content="{{.Issue.SummaryCardWidth}}"> 19 - <meta property="og:image:height" content="{{.Issue.SummaryCardHeight}}"> 20 - <meta property="og:image:alt" content="{{ctx.Locale.Tr "repo.issues.summary_card_alt" .Issue.Title .Repository.FullName}}"> 21 38 {{else if or .PageIsDiff .IsViewFile}} 22 39 <meta property="og:title" content="{{.Title}}"> 23 40 <meta property="og:url" content="{{AppUrl}}{{.Link}}"> ··· 35 52 <meta property="og:description" content="{{StringUtils.EllipsisString .Repository.Description 300}}"> 36 53 {{end}} 37 54 {{else}} 38 - <meta property="og:title" content="{{.Repository.Name}}"> 39 - <meta property="og:url" content="{{.Repository.HTMLURL}}"> 40 - {{if .Repository.Description}} 55 + {{if not .OpenGraphTitle}} 56 + <meta property="og:title" content="{{.Repository.Name}}"> 57 + {{end}} 58 + {{if not .OpenGraphURL}} 59 + <meta property="og:url" content="{{.Repository.HTMLURL}}"> 60 + {{end}} 61 + {{if and (.Repository.Description) (not .OpenGraphDescription)}} 41 62 <meta property="og:description" content="{{StringUtils.EllipsisString .Repository.Description 300}}"> 42 63 {{end}} 43 64 {{end}} 44 65 <meta property="og:type" content="object"> 45 - {{if not .Issue}} 66 + {{if and (not .Issue) (not .OpenGraphImageURL)}} 46 67 {{if (.Repository.AvatarLink ctx)}} 47 68 <meta property="og:image" content="{{.Repository.AvatarLink ctx}}"> 48 69 {{else}}
+55 -21
tests/integration/opengraph_test.go
··· 94 94 name: "file in repo", 95 95 url: "/user27/repo49/src/branch/master/test/test.txt", 96 96 expected: map[string]string{ 97 - "og:title": "repo49/test/test.txt at master", 98 - "og:url": setting.AppURL + "/user27/repo49/src/branch/master/test/test.txt", 99 - "og:type": "object", 100 - "og:image": setting.AppURL + "assets/img/avatar_default.png", 101 - "og:site_name": siteName, 97 + "og:title": "repo49/test/test.txt at master", 98 + "og:url": setting.AppURL + "/user27/repo49/src/branch/master/test/test.txt", 99 + "og:type": "object", 100 + "og:image": setting.AppURL + "user27/repo49/-/summary-card", 101 + "og:image:alt": "Summary card of repository user27/repo49", 102 + "og:image:width": "1200", 103 + "og:image:height": "600", 104 + "og:site_name": siteName, 102 105 }, 103 106 }, 104 107 { 105 108 name: "wiki page for repo without description", 106 109 url: "/user2/repo1/wiki/Page-With-Spaced-Name", 107 110 expected: map[string]string{ 108 - "og:title": "Page With Spaced Name", 109 - "og:url": setting.AppURL + "/user2/repo1/wiki/Page-With-Spaced-Name", 110 - "og:type": "object", 111 - "og:image": setting.AppURL + "avatars/ab53a2911ddf9b4817ac01ddcd3d975f", 112 - "og:site_name": siteName, 111 + "og:title": "Page With Spaced Name", 112 + "og:url": setting.AppURL + "/user2/repo1/wiki/Page-With-Spaced-Name", 113 + "og:type": "object", 114 + "og:image": setting.AppURL + "user2/repo1/-/summary-card", 115 + "og:image:alt": "Summary card of repository user2/repo1", 116 + "og:image:width": "1200", 117 + "og:image:height": "600", 118 + "og:site_name": siteName, 113 119 }, 114 120 }, 115 121 { 116 122 name: "index page for repo without description", 117 123 url: "/user2/repo1", 118 124 expected: map[string]string{ 119 - "og:title": "repo1", 120 - "og:url": setting.AppURL + "user2/repo1", 121 - "og:type": "object", 122 - "og:image": setting.AppURL + "avatars/ab53a2911ddf9b4817ac01ddcd3d975f", 123 - "og:site_name": siteName, 125 + "og:title": "repo1", 126 + "og:url": setting.AppURL + "user2/repo1", 127 + "og:type": "object", 128 + "og:image": setting.AppURL + "user2/repo1/-/summary-card", 129 + "og:image:alt": "Summary card of repository user2/repo1", 130 + "og:image:width": "1200", 131 + "og:image:height": "600", 132 + "og:site_name": siteName, 124 133 }, 125 134 }, 126 135 { 127 136 name: "index page for repo with description", 128 137 url: "/user27/repo49", 129 138 expected: map[string]string{ 130 - "og:title": "repo49", 131 - "og:url": setting.AppURL + "user27/repo49", 132 - "og:description": "A wonderful repository with more than just a README.md", 133 - "og:type": "object", 134 - "og:image": setting.AppURL + "assets/img/avatar_default.png", 135 - "og:site_name": siteName, 139 + "og:title": "repo49", 140 + "og:url": setting.AppURL + "user27/repo49", 141 + "og:description": "A wonderful repository with more than just a README.md", 142 + "og:type": "object", 143 + "og:image": setting.AppURL + "user27/repo49/-/summary-card", 144 + "og:image:alt": "Summary card of repository user27/repo49", 145 + "og:image:width": "1200", 146 + "og:image:height": "600", 147 + "og:site_name": siteName, 148 + }, 149 + }, 150 + { 151 + name: "release", 152 + url: "/user2/repo1/releases/tag/v1.1", 153 + expected: map[string]string{ 154 + "og:title": "testing-release - user2/repo1", 155 + "og:url": setting.AppURL + "user2/repo1/releases/tag/v1.1", 156 + "og:type": "object", 157 + "og:image": setting.AppURL + "user2/repo1/releases/summary-card/v1.1", 158 + "og:image:alt": "Summary card of an release titled \"testing-release\" in repository user2/repo1", 159 + "og:image:width": "1200", 160 + "og:image:height": "600", 161 + "og:site_name": siteName, 136 162 }, 137 163 }, 138 164 } ··· 167 193 url string 168 194 }{ 169 195 { 196 + name: "repo", 197 + url: "/user2/repo1/-/summary-card", 198 + }, 199 + { 170 200 name: "issue", 171 201 url: "/user2/repo1/issues/1/summary-card", 172 202 }, 173 203 { 174 204 name: "pull request", 175 205 url: "/user2/repo1/pulls/2/summary-card", 206 + }, 207 + { 208 + name: "release", 209 + url: "/user2/repo1/releases/summary-card/v1.1", 176 210 }, 177 211 } 178 212