Template
1
0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo synced 2024-11-28 12:46:09 +01:00

Compare commits

...

20 commits

Author SHA1 Message Date
Giteabot fd1edb9d9d
Fix DeleteCollaboration transaction behaviour (#28886) (#28889)
Backport #28886 by @KN4CK3R

The method can't be called with an outer transaction because if the user
is not a collaborator the outer transaction will be rolled back even if
the inner transaction uses the no-error path.

`has == 0` leads to `return nil` which cancels the transaction. A
standalone call of this method does nothing but if used with an outer
transaction, that will be canceled.

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
2024-01-22 09:10:45 +01:00
Giteabot 633996db8e
Fix schedule not trigger bug because matching full ref name with short ref name (#28874) (#28888)
Backport #28874 by @lunny

Fix #28533

Caused by #28691

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-01-22 13:33:14 +08:00
FuXiaoHei 7f0ce2dfc7
Fix uploaded artifacts should be overwritten (#28726) backport v1.21 (#28832)
Backport https://github.com/go-gitea/gitea/pull/28726 by @fuxiaohei

Fix Uploaded artifacts should be overwritten
https://github.com/go-gitea/gitea/issues/28549

When upload different content to uploaded artifact, it checks that
content size is not match in db record with previous artifact size, then
the new artifact is refused.

Now if it finds uploading content size is not matching db record when
receiving chunks, it updates db records to follow the latest size value.
2024-01-22 09:49:21 +08:00
Giteabot b7c944b9e4
Prevent anonymous container access if RequireSignInView is enabled (#28877) (#28882)
Backport #28877 by @KN4CK3R

Fixes #28875

If `RequireSignInView` is enabled, the ghost user has no access rights.

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
2024-01-22 01:44:38 +08:00
Giteabot cf9a416d62
Fix migrate storage bug (#28830) (#28867)
Backport #28830 by @lunny

Fix #28728

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-01-21 16:32:06 +00:00
Giteabot 8c7bda8755
Fix incorrect PostgreSQL connection string for Unix sockets (#28865) (#28870)
Backport #28865 by @sdvcrx

Fix #28864

Co-authored-by: sdvcrx <memory.silentvoyage@gmail.com>
2024-01-21 16:06:34 +00:00
Giteabot b7e32b2382
Avoid duplicate JS error messages on UI (#28873) (#28881)
Backport #28873 by wxiaoguang

Gitea treat JS errors seriously, so sometimes the JS errors caused by
3rdparty code (eg: browser extensions) would also be reported on Gitea
UI: TypeError: WeakMap key undefined (caused by extension DarkReader's
bug) #28861

To avoid fill the user's screen with a lot of error messages, this PR
merges the same error messages into one.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-01-21 15:40:26 +00:00
Giteabot e3dfb512d6
Warn that DISABLE_QUERY_AUTH_TOKEN is false only if it's explicitly defined (#28783) (#28868)
Backport #28783 by @yardenshoham

So we don't warn on default behavior

- Fixes https://github.com/go-gitea/gitea/issues/28758
- Follows https://github.com/go-gitea/gitea/pull/28390

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Yarden Shoham <git@yardenshoham.com>
2024-01-21 15:21:22 +00:00
Giteabot 0d50f27469
Fix archive creating LFS hooks and breaking pull requests (#28848) (#28851)
Backport #28848 by @brechtvl

When LFS hooks are present in gitea-repositories, operations like git
push for creating a pull request fail. These repositories are not meant
to include LFS files or git push them, that is handled separately. And
so they should not have LFS hooks.

Installing git-lfs on some systems (like Debian Linux) will
automatically set up /etc/gitconfig to create LFS hooks in repositories.
For most git commands in Gitea this is not a problem, either because
they run on a temporary clone or the git command does not create LFS
hooks.

But one case where this happens is git archive for creating repository
archives. To fix that, add a GIT_CONFIG_NOSYSTEM=1 to disable using the
system configuration for that command.

According to a comment, GIT_CONFIG_NOSYSTEM is not used for all git
commands because the system configuration can be intentionally set up
for Gitea to use.

Resolves #19810, #21148

Co-authored-by: Brecht Van Lommel <brecht@blender.org>
2024-01-21 14:43:31 +00:00
Mihir Joshi 62f2d717b7
Fix reverting a merge commit failing (#28794) (#28825)
Backport https://github.com/go-gitea/gitea/pull/28794

Fixes #22236

---
Error occurring currently while trying to revert commit using read-tree
-m approach:
> 2022/12/26 16:04:43 ...rvices/pull/patch.go:240:AttemptThreeWayMerge()
[E] [63a9c61a] Unable to run read-tree -m! Error: exit status 128 -
fatal: this operation must be run in a work tree
> 	 - fatal: this operation must be run in a work tree

We need to clone a non-bare repository for `git read-tree -m` to work.


bb371aee6e
adds support to create a non-bare cloned temporary upload repository.

After cloning a non-bare temporary upload repository, we [set default
index](https://github.com/go-gitea/gitea/blob/main/services/repository/files/cherry_pick.go#L37)
(`git read-tree HEAD`).
This operation ends up resetting the git index file (see investigation
details below), due to which, we need to call `git update-index
--refresh` afterward.

Here's the diff of the index file before and after we execute
SetDefaultIndex: https://www.diffchecker.com/hyOP3eJy/

Notice the **ctime**, **mtime** are set to 0 after SetDefaultIndex.

You can reproduce the same behavior using these steps:
```bash
$ git clone https://try.gitea.io/me-heer/test.git -s -b main
$ cd test
$ git read-tree HEAD
$ git read-tree -m 1f085d7ed8 1f085d7ed8 9933caed00
error: Entry '1' not uptodate. Cannot merge.
```

After which, we can fix like this:
```bash
$ git update-index --refresh
$ git read-tree -m 1f085d7ed8 1f085d7ed8 9933caed00
```
2024-01-21 14:18:37 +00:00
Giteabot 89960c3dfb
tests: missing refs/ in bare repositories (#28844) (#28852)
Backport #28844 by @AdamMajer

Git 2.43.0 will not detect a git repository as valid without refs/
subdirectory present. `git gc` cleans this up and puts it in
packed-refs. We must keep refs/ non-empty.

Co-authored-by: Adam Majer <amajer@suse.de>
2024-01-19 15:39:21 +08:00
Giteabot 8b5c9186a5
Remove duplicated checkinit on git module (#28824) (#28831)
Backport #28824 by @lunny

`checkInit` has been invoked in `InitSimple`. So it's unnecessary to
invoke it twice in `InitFull`.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-01-17 10:16:33 +00:00
Yarden Shoham 8b4abb1719
Bump @github/relative-time-element to 4.3.1 (#28819) (#28826)
Backport #28819

- Fixes https://github.com/go-gitea/gitea/issues/28747

# Before

![image](https://github.com/go-gitea/gitea/assets/20454870/65d8dc84-680f-4c16-9aa1-b5986102e4e7)

# After

![image](https://github.com/go-gitea/gitea/assets/20454870/7cb288e7-ebde-4e94-a10a-cac28d6bdcfd)

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
2024-01-17 17:52:06 +08:00
Lunny Xiao f4923854f6
Add changelog for 1.21.4 (#28812) 2024-01-17 10:46:50 +08:00
Giteabot d590607106
Render code block in activity tab (#28816) (#28818)
Backport #28816 by @JakobDev

This is a little bugfix. Inline code is usually rendered in issue
titles, but it is missing in the activity tab.

Before:
![Screenshot 2024-01-16 at 14-20-51
Test](https://github.com/go-gitea/gitea/assets/15185051/383370f3-0fb2-49de-81cc-014e5cf86727)
After:

![grafik](https://github.com/go-gitea/gitea/assets/15185051/83eaf973-ce9a-44ce-beea-2db49fc8bd73)

Co-authored-by: JakobDev <jakobdev@gmx.de>
2024-01-17 01:35:26 +01:00
Giteabot 4746291b08
Use refname:strip-2 instead of refname:short when syncing tags (#28797) (#28811)
Backport #28797 by @lunny

Fix #28694 

Generally, `refname:short` should be equal to `refname:lstrip=2` except
`core.warnAmbiguousRefs is used to select the strict abbreviation mode.`

ref:
https://git-scm.com/docs/git-for-each-ref#Documentation/git-for-each-ref.txt-refname

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-01-16 07:33:33 +00:00
KN4CK3R 022552d5b6
Rework markup link rendering (#26745) (#28803)
Backport #26745
Fixes #26548

This PR refactors the rendering of markup links. The old code uses
`strings.Replace` to change some urls while the new code uses more
context to decide which link should be generated.

The added tests should ensure the same output for the old and new
behaviour (besides the bug).

We may need to refactor the rendering a bit more to make it clear how
the different helper methods render the input string. There are lots of
options (resolve links / images / mentions / git hashes / emojis / ...)
but you don't really know what helper uses which options. For example,
we currently support images in the user description which should not be
allowed I think:

<details>
  <summary>Profile</summary>

https://try.gitea.io/KN4CK3R


![grafik](https://github.com/go-gitea/gitea/assets/1666336/109ae422-496d-4200-b52e-b3a528f553e5)

</details>
2024-01-16 02:13:29 +00:00
Giteabot 376fa0d8c4
Forbid removing the last admin user (#28337) (#28793)
Backport #28337 by @yp05327

Co-authored-by: yp05327 <576951401@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-01-16 01:51:46 +00:00
Giteabot be541d9877
Fix links in issue card (#28806) (#28807)
Backport #28806 by @denyskon

Fixes_
https://github.com/go-gitea/gitea/issues/23318#issuecomment-1611086747

Co-authored-by: Denys Konovalov <kontakt@denyskon.de>
2024-01-16 09:30:07 +08:00
KN4CK3R ae99233db0
Fix GetCommitStatuses (#28787) (#28804)
Backport #28787

Replaces #28802
2024-01-15 19:30:12 +01:00
97 changed files with 1482 additions and 503 deletions

View file

@ -4,6 +4,60 @@ This changelog goes through all the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com). been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.21.4](https://github.com/go-gitea/gitea/releases/tag/1.21.4) - 2024-01-16
* SECURITY
* Update github.com/cloudflare/circl (#28789) (#28790)
* Require token for GET subscription endpoint (#28765) (#28768)
* BUGFIXES
* Use refname:strip-2 instead of refname:short when syncing tags (#28797) (#28811)
* Fix links in issue card (#28806) (#28807)
* Fix nil pointer panic when exec some gitea cli command (#28791) (#28795)
* Require token for GET subscription endpoint (#28765) (#28778)
* Fix button size in "attached header right" (#28770) (#28774)
* Fix `convert.ToTeams` on empty input (#28426) (#28767)
* Hide code related setting options in repository when code unit is disabled (#28631) (#28749)
* Fix incorrect URL for "Reference in New Issue" (#28716) (#28723)
* Fix panic when parsing empty pgsql host (#28708) (#28709)
* Upgrade xorm to new version which supported update join for all supported databases (#28590) (#28668)
* Fix alpine package files are not rebuilt (#28638) (#28665)
* Avoid cycle-redirecting user/login page (#28636) (#28658)
* Fix empty ref for cron workflow runs (#28640) (#28647)
* Remove unnecessary syncbranchToDB with tests (#28624) (#28629)
* Use known issue IID to generate new PR index number when migrating from GitLab (#28616) (#28618)
* Fix flex container width (#28603) (#28605)
* Fix the scroll behavior for emoji/mention list (#28597) (#28601)
* Fix wrong due date rendering in issue list page (#28588) (#28591)
* Fix `status_check_contexts` matching bug (#28582) (#28589)
* Fix 500 error of searching commits (#28576) (#28579)
* Use information from previous blame parts (#28572) (#28577)
* Update mermaid for 1.21 (#28571)
* Fix 405 method not allowed CORS / OIDC (#28583) (#28586) (#28587) (#28611)
* Fix `GetCommitStatuses` (#28787) (#28804)
* Forbid removing the last admin user (#28337) (#28793)
* Fix schedule tasks bugs (#28691) (#28780)
* Fix issue dependencies (#27736) (#28776)
* Fix system webhooks API bug (#28531) (#28666)
* Fix when private user following user, private user will not be counted in his own view (#28037) (#28792)
* Render code block in activity tab (#28816) (#28818)
* ENHANCEMENTS
* Rework markup link rendering (#26745) (#28803)
* Modernize merge button (#28140) (#28786)
* Speed up loading the dashboard on mysql/mariadb (#28546) (#28784)
* Assign pull request to project during creation (#28227) (#28775)
* Show description as tooltip instead of title for labels (#28754) (#28766)
* Make template `DateTime` show proper tooltip (#28677) (#28683)
* Switch destination directory for apt signing keys (#28639) (#28642)
* Include heap pprof in diagnosis report to help debugging memory leaks (#28596) (#28599)
* DOCS
* Suggest to use Type=simple for systemd service (#28717) (#28722)
* Extend description for ARTIFACT_RETENTION_DAYS (#28626) (#28630)
* MISC
* Add -F to commit search to treat keywords as strings (#28744) (#28748)
* Add download attribute to release attachments (#28739) (#28740)
* Concatenate error in `checkIfPRContentChanged` (#28731) (#28737)
* Improve 1.21 document for Database Preparation (#28643) (#28644)
## [1.21.3](https://github.com/go-gitea/gitea/releases/tag/1.21.3) - 2023-12-21 ## [1.21.3](https://github.com/go-gitea/gitea/releases/tag/1.21.3) - 2023-12-21
* SECURITY * SECURITY

View file

@ -110,6 +110,9 @@ func migrateLFS(ctx context.Context, dstStorage storage.ObjectStorage) error {
func migrateAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error { func migrateAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error {
return db.Iterate(ctx, nil, func(ctx context.Context, user *user_model.User) error { return db.Iterate(ctx, nil, func(ctx context.Context, user *user_model.User) error {
if user.CustomAvatarRelativePath() == "" {
return nil
}
_, err := storage.Copy(dstStorage, user.CustomAvatarRelativePath(), storage.Avatars, user.CustomAvatarRelativePath()) _, err := storage.Copy(dstStorage, user.CustomAvatarRelativePath(), storage.Avatars, user.CustomAvatarRelativePath())
return err return err
}) })
@ -117,6 +120,9 @@ func migrateAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error
func migrateRepoAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error { func migrateRepoAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error {
return db.Iterate(ctx, nil, func(ctx context.Context, repo *repo_model.Repository) error { return db.Iterate(ctx, nil, func(ctx context.Context, repo *repo_model.Repository) error {
if repo.CustomAvatarRelativePath() == "" {
return nil
}
_, err := storage.Copy(dstStorage, repo.CustomAvatarRelativePath(), storage.RepoAvatars, repo.CustomAvatarRelativePath()) _, err := storage.Copy(dstStorage, repo.CustomAvatarRelativePath(), storage.RepoAvatars, repo.CustomAvatarRelativePath())
return err return err
}) })

View file

@ -57,6 +57,21 @@ func (err ErrUserOwnPackages) Error() string {
return fmt.Sprintf("user still has ownership of packages [uid: %d]", err.UID) return fmt.Sprintf("user still has ownership of packages [uid: %d]", err.UID)
} }
// ErrDeleteLastAdminUser represents a "DeleteLastAdminUser" kind of error.
type ErrDeleteLastAdminUser struct {
UID int64
}
// IsErrDeleteLastAdminUser checks if an error is a ErrDeleteLastAdminUser.
func IsErrDeleteLastAdminUser(err error) bool {
_, ok := err.(ErrDeleteLastAdminUser)
return ok
}
func (err ErrDeleteLastAdminUser) Error() string {
return fmt.Sprintf("can not delete the last admin user [uid: %d]", err.UID)
}
// ErrNoPendingRepoTransfer is an error type for repositories without a pending // ErrNoPendingRepoTransfer is an error type for repositories without a pending
// transfer request // transfer request
type ErrNoPendingRepoTransfer struct { type ErrNoPendingRepoTransfer struct {

View file

@ -649,3 +649,10 @@
repo_id: 49 repo_id: 49
type: 2 type: 2
created_unix: 946684810 created_unix: 946684810
-
id: 98
repo_id: 59
type: 1
config: "{}"
created_unix: 946684810

View file

@ -1693,3 +1693,16 @@
size: 0 size: 0
is_fsck_enabled: true is_fsck_enabled: true
close_issues_via_commit_in_any_branch: false close_issues_via_commit_in_any_branch: false
-
id: 59
owner_id: 2
owner_name: user2
lower_name: test_commit_revert
name: test_commit_revert
default_branch: main
is_empty: false
is_archived: false
is_private: true
status: 0
num_issues: 0

View file

@ -66,7 +66,7 @@
num_followers: 2 num_followers: 2
num_following: 1 num_following: 1
num_stars: 2 num_stars: 2
num_repos: 14 num_repos: 15
num_teams: 0 num_teams: 0
num_members: 0 num_members: 0
visibility: 0 visibility: 0

View file

@ -25,7 +25,6 @@ import (
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"xorm.io/builder" "xorm.io/builder"
"xorm.io/xorm"
) )
// CommitStatus holds a single Status of a single Commit // CommitStatus holds a single Status of a single Commit
@ -220,60 +219,58 @@ func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus {
// CommitStatusOptions holds the options for query commit statuses // CommitStatusOptions holds the options for query commit statuses
type CommitStatusOptions struct { type CommitStatusOptions struct {
db.ListOptions db.ListOptions
RepoID int64
SHA string
State string State string
SortType string SortType string
} }
// GetCommitStatuses returns all statuses for a given commit. func (opts *CommitStatusOptions) ToConds() builder.Cond {
func GetCommitStatuses(ctx context.Context, repo *repo_model.Repository, sha string, opts *CommitStatusOptions) ([]*CommitStatus, int64, error) { var cond builder.Cond = builder.Eq{
if opts.Page <= 0 { "repo_id": opts.RepoID,
opts.Page = 1 "sha": opts.SHA,
}
if opts.PageSize <= 0 {
opts.Page = setting.ItemsPerPage
} }
countSession := listCommitStatusesStatement(ctx, repo, sha, opts)
countSession = db.SetSessionPagination(countSession, opts)
maxResults, err := countSession.Count(new(CommitStatus))
if err != nil {
log.Error("Count PRs: %v", err)
return nil, maxResults, err
}
statuses := make([]*CommitStatus, 0, opts.PageSize)
findSession := listCommitStatusesStatement(ctx, repo, sha, opts)
findSession = db.SetSessionPagination(findSession, opts)
sortCommitStatusesSession(findSession, opts.SortType)
return statuses, maxResults, findSession.Find(&statuses)
}
func listCommitStatusesStatement(ctx context.Context, repo *repo_model.Repository, sha string, opts *CommitStatusOptions) *xorm.Session {
sess := db.GetEngine(ctx).Where("repo_id = ?", repo.ID).And("sha = ?", sha)
switch opts.State { switch opts.State {
case "pending", "success", "error", "failure", "warning": case "pending", "success", "error", "failure", "warning":
sess.And("state = ?", opts.State) cond = cond.And(builder.Eq{
"state": opts.State,
})
} }
return sess
return cond
} }
func sortCommitStatusesSession(sess *xorm.Session, sortType string) { func (opts *CommitStatusOptions) ToOrders() string {
switch sortType { switch opts.SortType {
case "oldest": case "oldest":
sess.Asc("created_unix") return "created_unix ASC"
case "recentupdate": case "recentupdate":
sess.Desc("updated_unix") return "updated_unix DESC"
case "leastupdate": case "leastupdate":
sess.Asc("updated_unix") return "updated_unix ASC"
case "leastindex": case "leastindex":
sess.Desc("index") return "`index` DESC"
case "highestindex": case "highestindex":
sess.Asc("index") return "`index` ASC"
default: default:
sess.Desc("created_unix") return "created_unix DESC"
} }
} }
// GetCommitStatuses returns all statuses for a given commit.
func GetCommitStatuses(ctx context.Context, opts *CommitStatusOptions) ([]*CommitStatus, int64, error) {
sess := db.GetEngine(ctx).
Where(opts.ToConds()).
OrderBy(opts.ToOrders())
db.SetSessionPagination(sess, opts)
statuses := make([]*CommitStatus, 0, opts.PageSize)
count, err := sess.FindAndCount(&statuses)
return statuses, count, err
}
// CommitStatusIndex represents a table for commit status index // CommitStatusIndex represents a table for commit status index
type CommitStatusIndex struct { type CommitStatusIndex struct {
ID int64 ID int64

View file

@ -22,7 +22,11 @@ func TestGetCommitStatuses(t *testing.T) {
sha1 := "1234123412341234123412341234123412341234" sha1 := "1234123412341234123412341234123412341234"
statuses, maxResults, err := git_model.GetCommitStatuses(db.DefaultContext, repo1, sha1, &git_model.CommitStatusOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 50}}) statuses, maxResults, err := git_model.GetCommitStatuses(db.DefaultContext, &git_model.CommitStatusOptions{
ListOptions: db.ListOptions{Page: 1, PageSize: 50},
RepoID: repo1.ID,
SHA: sha1,
})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, int(maxResults), 5) assert.Equal(t, int(maxResults), 5)
assert.Len(t, statuses, 5) assert.Len(t, statuses, 5)
@ -46,4 +50,13 @@ func TestGetCommitStatuses(t *testing.T) {
assert.Equal(t, "deploy/awesomeness", statuses[4].Context) assert.Equal(t, "deploy/awesomeness", statuses[4].Context)
assert.Equal(t, structs.CommitStatusError, statuses[4].State) assert.Equal(t, structs.CommitStatusError, statuses[4].State)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(db.DefaultContext)) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(db.DefaultContext))
statuses, maxResults, err = git_model.GetCommitStatuses(db.DefaultContext, &git_model.CommitStatusOptions{
ListOptions: db.ListOptions{Page: 2, PageSize: 50},
RepoID: repo1.ID,
SHA: sha1,
})
assert.NoError(t, err)
assert.Equal(t, int(maxResults), 5)
assert.Empty(t, statuses)
} }

View file

@ -109,9 +109,11 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
var err error var err error
if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: issue.Repo.Link(), Links: markup.Links{
Metas: issue.Repo.ComposeMetas(), Base: issue.Repo.Link(),
},
Metas: issue.Repo.ComposeMetas(),
}, comment.Content); err != nil { }, comment.Content); err != nil {
return nil, err return nil, err
} }

View file

@ -578,8 +578,7 @@ func (repo *Repository) CanEnableEditor() bool {
// DescriptionHTML does special handles to description and return HTML string. // DescriptionHTML does special handles to description and return HTML string.
func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{ desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: repo.HTMLURL(),
// Don't use Metas to speedup requests // Don't use Metas to speedup requests
}, repo.Description) }, repo.Description)
if err != nil { if err != nil {

View file

@ -705,9 +705,18 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve
return committer.Commit() return committer.Commit()
} }
// IsLastAdminUser check whether user is the last admin
func IsLastAdminUser(ctx context.Context, user *User) bool {
if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: util.OptionalBoolTrue}) <= 1 {
return true
}
return false
}
// CountUserFilter represent optional filters for CountUsers // CountUserFilter represent optional filters for CountUsers
type CountUserFilter struct { type CountUserFilter struct {
LastLoginSince *int64 LastLoginSince *int64
IsAdmin util.OptionalBool
} }
// CountUsers returns number of users. // CountUsers returns number of users.
@ -716,13 +725,25 @@ func CountUsers(ctx context.Context, opts *CountUserFilter) int64 {
} }
func countUsers(ctx context.Context, opts *CountUserFilter) int64 { func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
sess := db.GetEngine(ctx).Where(builder.Eq{"type": "0"}) sess := db.GetEngine(ctx)
cond := builder.NewCond()
cond = cond.And(builder.Eq{"type": UserTypeIndividual})
if opts != nil && opts.LastLoginSince != nil { if opts != nil {
sess = sess.Where(builder.Gte{"last_login_unix": *opts.LastLoginSince}) if opts.LastLoginSince != nil {
cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince})
}
if !opts.IsAdmin.IsNone() {
cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()})
}
}
count, err := sess.Where(cond).Count(new(User))
if err != nil {
log.Error("user.countUsers: %v", err)
} }
count, _ := sess.Count(new(User))
return count return count
} }

View file

@ -93,7 +93,7 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any))
} }
func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) { func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
if setting.Service.RequireSignInView && doer == nil { if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) {
return perm.AccessModeNone, nil return perm.AccessModeNone, nil
} }

View file

@ -49,9 +49,9 @@ func TestFormat_Flag(t *testing.T) {
{ {
name: "multiple fields", name: "multiple fields",
givenFormat: foreachref.NewFormat("refname:short", "objecttype", "objectname"), givenFormat: foreachref.NewFormat("refname:lstrip=2", "objecttype", "objectname"),
wantFlag: "refname:short %(refname:short)%00objecttype %(objecttype)%00objectname %(objectname)%00%00", wantFlag: "refname:lstrip=2 %(refname:lstrip=2)%00objecttype %(objecttype)%00objectname %(objectname)%00%00",
}, },
} }

View file

@ -166,10 +166,6 @@ func InitSimple(ctx context.Context) error {
// InitFull initializes git module with version check and change global variables, sync gitconfig. // InitFull initializes git module with version check and change global variables, sync gitconfig.
// It should only be called once at the beginning of the program initialization (TestMain/GlobalInitInstalled) as this code makes unsynchronized changes to variables. // It should only be called once at the beginning of the program initialization (TestMain/GlobalInitInstalled) as this code makes unsynchronized changes to variables.
func InitFull(ctx context.Context) (err error) { func InitFull(ctx context.Context) (err error) {
if err = checkInit(); err != nil {
return err
}
if err = InitSimple(ctx); err != nil { if err = InitSimple(ctx); err != nil {
return err return err
} }

View file

@ -8,6 +8,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
) )
@ -62,11 +63,15 @@ func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, t
cmd.AddOptionFormat("--format=%s", format.String()) cmd.AddOptionFormat("--format=%s", format.String())
cmd.AddDynamicArguments(commitID) cmd.AddDynamicArguments(commitID)
// Avoid LFS hooks getting installed because of /etc/gitconfig, which can break pull requests.
env := append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1")
var stderr strings.Builder var stderr strings.Builder
err := cmd.Run(&RunOpts{ err := cmd.Run(&RunOpts{
Dir: repo.Path, Dir: repo.Path,
Stdout: target, Stdout: target,
Stderr: &stderr, Stderr: &stderr,
Env: env,
}) })
if err != nil { if err != nil {
return ConcatenateError(err, stderr.String()) return ConcatenateError(err, stderr.String())

View file

@ -112,7 +112,9 @@ func (repo *Repository) GetTagWithID(idStr, name string) (*Tag, error) {
// GetTagInfos returns all tag infos of the repository. // GetTagInfos returns all tag infos of the repository.
func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
forEachRefFmt := foreachref.NewFormat("objecttype", "refname:short", "object", "objectname", "creator", "contents", "contents:signature") // Generally, refname:short should be equal to refname:lstrip=2 except core.warnAmbiguousRefs is used to select the strict abbreviation mode.
// https://git-scm.com/docs/git-for-each-ref#Documentation/git-for-each-ref.txt-refname
forEachRefFmt := foreachref.NewFormat("objecttype", "refname:lstrip=2", "object", "objectname", "creator", "contents", "contents:signature")
stdoutReader, stdoutWriter := io.Pipe() stdoutReader, stdoutWriter := io.Pipe()
defer stdoutReader.Close() defer stdoutReader.Close()
@ -162,7 +164,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
func parseTagRef(ref map[string]string) (tag *Tag, err error) { func parseTagRef(ref map[string]string) (tag *Tag, err error) {
tag = &Tag{ tag = &Tag{
Type: ref["objecttype"], Type: ref["objecttype"],
Name: ref["refname:short"], Name: ref["refname:lstrip=2"],
} }
tag.ID, err = NewIDFromString(ref["objectname"]) tag.ID, err = NewIDFromString(ref["objectname"])

View file

@ -209,8 +209,8 @@ func TestRepository_parseTagRef(t *testing.T) {
name: "lightweight tag", name: "lightweight tag",
givenRef: map[string]string{ givenRef: map[string]string{
"objecttype": "commit", "objecttype": "commit",
"refname:short": "v1.9.1", "refname:lstrip=2": "v1.9.1",
// object will be empty for lightweight tags // object will be empty for lightweight tags
"object": "", "object": "",
"objectname": "ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889", "objectname": "ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889",
@ -238,8 +238,8 @@ func TestRepository_parseTagRef(t *testing.T) {
name: "annotated tag", name: "annotated tag",
givenRef: map[string]string{ givenRef: map[string]string{
"objecttype": "tag", "objecttype": "tag",
"refname:short": "v0.0.1", "refname:lstrip=2": "v0.0.1",
// object will refer to commit hash for annotated tag // object will refer to commit hash for annotated tag
"object": "3325fd8a973321fd59455492976c042dde3fd1ca", "object": "3325fd8a973321fd59455492976c042dde3fd1ca",
"objectname": "8c68a1f06fc59c655b7e3905b159d761e91c53c9", "objectname": "8c68a1f06fc59c655b7e3905b159d761e91c53c9",
@ -267,11 +267,11 @@ func TestRepository_parseTagRef(t *testing.T) {
name: "annotated tag with signature", name: "annotated tag with signature",
givenRef: map[string]string{ givenRef: map[string]string{
"objecttype": "tag", "objecttype": "tag",
"refname:short": "v0.0.1", "refname:lstrip=2": "v0.0.1",
"object": "3325fd8a973321fd59455492976c042dde3fd1ca", "object": "3325fd8a973321fd59455492976c042dde3fd1ca",
"objectname": "8c68a1f06fc59c655b7e3905b159d761e91c53c9", "objectname": "8c68a1f06fc59c655b7e3905b159d761e91c53c9",
"creator": "Foo Bar <foo@bar.com> 1565789218 +0300", "creator": "Foo Bar <foo@bar.com> 1565789218 +0300",
"contents": `Add changelog of v1.9.1 (#7859) "contents": `Add changelog of v1.9.1 (#7859)
* add changelog of v1.9.1 * add changelog of v1.9.1

View file

@ -79,9 +79,10 @@ func envMark(envName string) string {
// Render renders the data of the document to HTML via the external tool. // Render renders the data of the document to HTML via the external tool.
func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
var ( var (
urlRawPrefix = strings.Replace(ctx.URLPrefix, "/src/", "/raw/", 1) command = strings.NewReplacer(
command = strings.NewReplacer(envMark("GITEA_PREFIX_SRC"), ctx.URLPrefix, envMark("GITEA_PREFIX_SRC"), ctx.Links.SrcLink(),
envMark("GITEA_PREFIX_RAW"), urlRawPrefix).Replace(p.Command) envMark("GITEA_PREFIX_RAW"), ctx.Links.RawLink(),
).Replace(p.Command)
commands = strings.Fields(command) commands = strings.Fields(command)
args = commands[1:] args = commands[1:]
) )
@ -121,14 +122,14 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
ctx.Ctx = graceful.GetManager().ShutdownContext() ctx.Ctx = graceful.GetManager().ShutdownContext()
} }
processCtx, _, finished := process.GetManager().AddContext(ctx.Ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.URLPrefix)) processCtx, _, finished := process.GetManager().AddContext(ctx.Ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.Links.SrcLink()))
defer finished() defer finished()
cmd := exec.CommandContext(processCtx, commands[0], args...) cmd := exec.CommandContext(processCtx, commands[0], args...)
cmd.Env = append( cmd.Env = append(
os.Environ(), os.Environ(),
"GITEA_PREFIX_SRC="+ctx.URLPrefix, "GITEA_PREFIX_SRC="+ctx.Links.SrcLink(),
"GITEA_PREFIX_RAW="+urlRawPrefix, "GITEA_PREFIX_RAW="+ctx.Links.RawLink(),
) )
if !p.IsInputFile { if !p.IsInputFile {
cmd.Stdin = input cmd.Stdin = input

View file

@ -80,15 +80,10 @@ const keywordClass = "issue-keyword"
// IsLink reports whether link fits valid format. // IsLink reports whether link fits valid format.
func IsLink(link []byte) bool { func IsLink(link []byte) bool {
return isLink(link)
}
// isLink reports whether link fits valid format.
func isLink(link []byte) bool {
return validLinksPattern.Match(link) return validLinksPattern.Match(link)
} }
func isLinkStr(link string) bool { func IsLinkStr(link string) bool {
return validLinksPattern.MatchString(link) return validLinksPattern.MatchString(link)
} }
@ -344,7 +339,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
node = node.FirstChild node = node.FirstChild
} }
visitNode(ctx, procs, procs, node) visitNode(ctx, procs, node)
newNodes := make([]*html.Node, 0, 5) newNodes := make([]*html.Node, 0, 5)
@ -375,7 +370,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
return nil return nil
} }
func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node) { func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
// Add user-content- to IDs and "#" links if they don't already have them // Add user-content- to IDs and "#" links if they don't already have them
for idx, attr := range node.Attr { for idx, attr := range node.Attr {
val := strings.TrimPrefix(attr.Val, "#") val := strings.TrimPrefix(attr.Val, "#")
@ -390,35 +385,29 @@ func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node
} }
if attr.Key == "class" && attr.Val == "emoji" { if attr.Key == "class" && attr.Val == "emoji" {
textProcs = nil procs = nil
} }
} }
// We ignore code and pre. // We ignore code and pre.
switch node.Type { switch node.Type {
case html.TextNode: case html.TextNode:
textNode(ctx, textProcs, node) textNode(ctx, procs, node)
case html.ElementNode: case html.ElementNode:
if node.Data == "img" { if node.Data == "img" {
for i, attr := range node.Attr { for i, attr := range node.Attr {
if attr.Key != "src" { if attr.Key != "src" {
continue continue
} }
if len(attr.Val) > 0 && !isLinkStr(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") { if len(attr.Val) > 0 && !IsLinkStr(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
prefix := ctx.URLPrefix attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
if ctx.IsWiki {
prefix = util.URLJoin(prefix, "wiki", "raw")
}
prefix = strings.Replace(prefix, "/src/", "/media/", 1)
attr.Val = util.URLJoin(prefix, attr.Val)
} }
attr.Val = camoHandleLink(attr.Val) attr.Val = camoHandleLink(attr.Val)
node.Attr[i] = attr node.Attr[i] = attr
} }
} else if node.Data == "a" { } else if node.Data == "a" {
// Restrict text in links to emojis // Restrict text in links to emojis
textProcs = emojiProcessors procs = emojiProcessors
} else if node.Data == "code" || node.Data == "pre" { } else if node.Data == "code" || node.Data == "pre" {
return return
} else if node.Data == "i" { } else if node.Data == "i" {
@ -444,7 +433,7 @@ func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node
} }
} }
for n := node.FirstChild; n != nil; n = n.NextSibling { for n := node.FirstChild; n != nil; n = n.NextSibling {
visitNode(ctx, procs, textProcs, n) visitNode(ctx, procs, n)
} }
} }
// ignore everything else // ignore everything else
@ -641,10 +630,6 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
} }
func shortLinkProcessor(ctx *RenderContext, node *html.Node) { func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
shortLinkProcessorFull(ctx, node, false)
}
func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
next := node.NextSibling next := node.NextSibling
for node != nil && node != next { for node != nil && node != next {
m := shortLinkPattern.FindStringSubmatchIndex(node.Data) m := shortLinkPattern.FindStringSubmatchIndex(node.Data)
@ -665,7 +650,7 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
if equalPos := strings.IndexByte(v, '='); equalPos == -1 { if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
// There is no equal in this argument; this is a mandatory arg // There is no equal in this argument; this is a mandatory arg
if props["name"] == "" { if props["name"] == "" {
if isLinkStr(v) { if IsLinkStr(v) {
// If we clearly see it is a link, we save it so // If we clearly see it is a link, we save it so
// But first we need to ensure, that if both mandatory args provided // But first we need to ensure, that if both mandatory args provided
@ -740,7 +725,7 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
DataAtom: atom.A, DataAtom: atom.A,
} }
childNode.Parent = linkNode childNode.Parent = linkNode
absoluteLink := isLinkStr(link) absoluteLink := IsLinkStr(link)
if !absoluteLink { if !absoluteLink {
if image { if image {
link = strings.ReplaceAll(link, " ", "+") link = strings.ReplaceAll(link, " ", "+")
@ -751,16 +736,9 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
link = url.PathEscape(link) link = url.PathEscape(link)
} }
} }
urlPrefix := ctx.URLPrefix
if image { if image {
if !absoluteLink { if !absoluteLink {
if IsSameDomain(urlPrefix) { link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), link)
urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1)
}
if ctx.IsWiki {
link = util.URLJoin("wiki", "raw", link)
}
link = util.URLJoin(urlPrefix, link)
} }
title := props["title"] title := props["title"]
if title == "" { if title == "" {
@ -789,18 +767,15 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
} else { } else {
if !absoluteLink { if !absoluteLink {
if ctx.IsWiki { if ctx.IsWiki {
link = util.URLJoin("wiki", link) link = util.URLJoin(ctx.Links.WikiLink(), link)
} else {
link = util.URLJoin(ctx.Links.SrcLink(), link)
} }
link = util.URLJoin(urlPrefix, link)
} }
childNode.Type = html.TextNode childNode.Type = html.TextNode
childNode.Data = name childNode.Data = name
} }
if noLink { linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
linkNode = childNode
} else {
linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
}
replaceContent(node, m[0], m[1], linkNode) replaceContent(node, m[0], m[1], linkNode)
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
} }

View file

@ -287,8 +287,8 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) {
} }
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
if ctx.URLPrefix == "" { if ctx.Links.Base == "" {
ctx.URLPrefix = TestAppURL ctx.Links.Base = TestRepoURL
} }
var buf strings.Builder var buf strings.Builder
@ -303,19 +303,23 @@ func TestRender_AutoLink(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
var buffer strings.Builder var buffer strings.Builder
err := PostProcess(&RenderContext{ err := PostProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: TestRepoURL, Links: Links{
Metas: localMetas, Base: TestRepoURL,
},
Metas: localMetas,
}, strings.NewReader(input), &buffer) }, strings.NewReader(input), &buffer)
assert.Equal(t, err, nil) assert.Equal(t, err, nil)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
buffer.Reset() buffer.Reset()
err = PostProcess(&RenderContext{ err = PostProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: TestRepoURL, Links: Links{
Metas: localMetas, Base: TestRepoURL,
IsWiki: true, },
Metas: localMetas,
IsWiki: true,
}, strings.NewReader(input), &buffer) }, strings.NewReader(input), &buffer)
assert.Equal(t, err, nil) assert.Equal(t, err, nil)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
@ -342,9 +346,11 @@ func TestRender_FullIssueURLs(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
var result strings.Builder var result strings.Builder
err := postProcess(&RenderContext{ err := postProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: TestRepoURL, Links: Links{
Metas: localMetas, Base: TestRepoURL,
},
Metas: localMetas,
}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result) }, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result.String()) assert.Equal(t, expected, result.String())

View file

@ -42,8 +42,10 @@ func TestRender_Commits(t *testing.T) {
buffer, err := RenderString(&RenderContext{ buffer, err := RenderString(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: ".md", RelativePath: ".md",
URLPrefix: TestRepoURL, Links: Links{
Metas: localMetas, Base: TestRepoURL,
},
Metas: localMetas,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -93,8 +95,10 @@ func TestRender_CrossReferences(t *testing.T) {
buffer, err := RenderString(&RenderContext{ buffer, err := RenderString(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: "a.md", RelativePath: "a.md",
URLPrefix: setting.AppSubURL, Links: Links{
Metas: localMetas, Base: setting.AppSubURL,
},
Metas: localMetas,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -138,7 +142,9 @@ func TestRender_links(t *testing.T) {
buffer, err := RenderString(&RenderContext{ buffer, err := RenderString(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: "a.md", RelativePath: "a.md",
URLPrefix: TestRepoURL, Links: Links{
Base: TestRepoURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -238,7 +244,9 @@ func TestRender_email(t *testing.T) {
res, err := RenderString(&RenderContext{ res, err := RenderString(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: "a.md", RelativePath: "a.md",
URLPrefix: TestRepoURL, Links: Links{
Base: TestRepoURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
@ -309,7 +317,9 @@ func TestRender_emoji(t *testing.T) {
buffer, err := RenderString(&RenderContext{ buffer, err := RenderString(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: "a.md", RelativePath: "a.md",
URLPrefix: TestRepoURL, Links: Links{
Base: TestRepoURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -371,29 +381,34 @@ func TestRender_ShortLinks(t *testing.T) {
test := func(input, expected, expectedWiki string) { test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(&RenderContext{ buffer, err := markdown.RenderString(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: tree, Links: Links{
Base: TestRepoURL,
BranchPath: "master",
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
buffer, err = markdown.RenderString(&RenderContext{ buffer, err = markdown.RenderString(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: TestRepoURL, Links: Links{
Metas: localMetas, Base: TestRepoURL,
IsWiki: true, },
Metas: localMetas,
IsWiki: true,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
} }
rawtree := util.URLJoin(TestRepoURL, "raw", "master") mediatree := util.URLJoin(TestRepoURL, "media", "master")
url := util.URLJoin(tree, "Link") url := util.URLJoin(tree, "Link")
otherURL := util.URLJoin(tree, "Other-Link") otherURL := util.URLJoin(tree, "Other-Link")
encodedURL := util.URLJoin(tree, "Link%3F") encodedURL := util.URLJoin(tree, "Link%3F")
imgurl := util.URLJoin(rawtree, "Link.jpg") imgurl := util.URLJoin(mediatree, "Link.jpg")
otherImgurl := util.URLJoin(rawtree, "Link+Other.jpg") otherImgurl := util.URLJoin(mediatree, "Link+Other.jpg")
encodedImgurl := util.URLJoin(rawtree, "Link+%23.jpg") encodedImgurl := util.URLJoin(mediatree, "Link+%23.jpg")
notencodedImgurl := util.URLJoin(rawtree, "some", "path", "Link+#.jpg") notencodedImgurl := util.URLJoin(mediatree, "some", "path", "Link+#.jpg")
urlWiki := util.URLJoin(TestRepoURL, "wiki", "Link") urlWiki := util.URLJoin(TestRepoURL, "wiki", "Link")
otherURLWiki := util.URLJoin(TestRepoURL, "wiki", "Other-Link") otherURLWiki := util.URLJoin(TestRepoURL, "wiki", "Other-Link")
encodedURLWiki := util.URLJoin(TestRepoURL, "wiki", "Link%3F") encodedURLWiki := util.URLJoin(TestRepoURL, "wiki", "Link%3F")
@ -475,21 +490,25 @@ func TestRender_ShortLinks(t *testing.T) {
func TestRender_RelativeImages(t *testing.T) { func TestRender_RelativeImages(t *testing.T) {
setting.AppURL = TestAppURL setting.AppURL = TestAppURL
tree := util.URLJoin(TestRepoURL, "src", "master")
test := func(input, expected, expectedWiki string) { test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(&RenderContext{ buffer, err := markdown.RenderString(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: tree, Links: Links{
Metas: localMetas, Base: TestRepoURL,
BranchPath: "master",
},
Metas: localMetas,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
buffer, err = markdown.RenderString(&RenderContext{ buffer, err = markdown.RenderString(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: TestRepoURL, Links: Links{
Metas: localMetas, Base: TestRepoURL,
IsWiki: true, },
Metas: localMetas,
IsWiki: true,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
@ -521,9 +540,11 @@ func Test_ParseClusterFuzz(t *testing.T) {
var res strings.Builder var res strings.Builder
err := PostProcess(&RenderContext{ err := PostProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Links: Links{
Metas: localMetas, Base: "https://example.com",
},
Metas: localMetas,
}, strings.NewReader(data), &res) }, strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotContains(t, res.String(), "<html") assert.NotContains(t, res.String(), "<html")
@ -532,9 +553,11 @@ func Test_ParseClusterFuzz(t *testing.T) {
res.Reset() res.Reset()
err = PostProcess(&RenderContext{ err = PostProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Links: Links{
Metas: localMetas, Base: "https://example.com",
},
Metas: localMetas,
}, strings.NewReader(data), &res) }, strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
@ -543,6 +566,7 @@ func Test_ParseClusterFuzz(t *testing.T) {
func TestPostProcess_RenderDocument(t *testing.T) { func TestPostProcess_RenderDocument(t *testing.T) {
setting.AppURL = TestAppURL setting.AppURL = TestAppURL
setting.StaticURLPrefix = TestAppURL // can't run standalone
localMetas := map[string]string{ localMetas := map[string]string{
"user": "go-gitea", "user": "go-gitea",
@ -553,9 +577,11 @@ func TestPostProcess_RenderDocument(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
var res strings.Builder var res strings.Builder
err := PostProcess(&RenderContext{ err := PostProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Links: Links{
Metas: localMetas, Base: "https://example.com",
},
Metas: localMetas,
}, strings.NewReader(input), &res) }, strings.NewReader(input), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String())) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
@ -589,9 +615,8 @@ func TestIssue16020(t *testing.T) {
var res strings.Builder var res strings.Builder
err := PostProcess(&RenderContext{ err := PostProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Metas: localMetas,
Metas: localMetas,
}, strings.NewReader(data), &res) }, strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, data, res.String()) assert.Equal(t, data, res.String())
@ -606,9 +631,8 @@ func BenchmarkEmojiPostprocess(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
var res strings.Builder var res strings.Builder
err := PostProcess(&RenderContext{ err := PostProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Metas: localMetas,
Metas: localMetas,
}, strings.NewReader(data), &res) }, strings.NewReader(data), &res)
assert.NoError(b, err) assert.NoError(b, err)
} }
@ -617,8 +641,10 @@ func BenchmarkEmojiPostprocess(b *testing.B) {
func TestFuzz(t *testing.T) { func TestFuzz(t *testing.T) {
s := "t/l/issues/8#/../../a" s := "t/l/issues/8#/../../a"
renderContext := RenderContext{ renderContext := RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com/go-gitea/gitea", Links: Links{
Base: "https://example.com/go-gitea/gitea",
},
Metas: map[string]string{ Metas: map[string]string{
"user": "go-gitea", "user": "go-gitea",
"repo": "gitea", "repo": "gitea",
@ -635,9 +661,8 @@ func TestIssue18471(t *testing.T) {
var res strings.Builder var res strings.Builder
err := PostProcess(&RenderContext{ err := PostProcess(&RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: "https://example.com", Metas: localMetas,
Metas: localMetas,
}, strings.NewReader(data), &res) }, strings.NewReader(data), &res)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -87,18 +87,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
// Check if the destination is a real link // Check if the destination is a real link
link := v.Destination link := v.Destination
if len(link) > 0 && !markup.IsLink(link) { if len(link) > 0 && !markup.IsLink(link) {
prefix := pc.Get(urlPrefixKey).(string) v.Destination = []byte(giteautil.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), string(link)))
if pc.Get(isWikiKey).(bool) {
prefix = giteautil.URLJoin(prefix, "wiki", "raw")
}
prefix = strings.Replace(prefix, "/src/", "/media/", 1)
lnk := strings.TrimLeft(string(link), "/")
lnk = giteautil.URLJoin(prefix, lnk)
link = []byte(lnk)
} }
v.Destination = link
parent := n.Parent() parent := n.Parent()
// Create a link around image only if parent is not already a link // Create a link around image only if parent is not already a link
@ -107,7 +97,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
// Create a link wrapper // Create a link wrapper
wrap := ast.NewLink() wrap := ast.NewLink()
wrap.Destination = link wrap.Destination = v.Destination
wrap.Title = v.Title wrap.Title = v.Title
wrap.SetAttributeString("target", []byte("_blank")) wrap.SetAttributeString("target", []byte("_blank"))
@ -143,11 +133,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
link[0] != '#' && !bytes.HasPrefix(link, byteMailto) { link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
// special case: this is not a link, a hash link or a mailto:, so it's a // special case: this is not a link, a hash link or a mailto:, so it's a
// relative URL // relative URL
lnk := string(link)
if pc.Get(isWikiKey).(bool) { var base string
lnk = giteautil.URLJoin("wiki", lnk) if ctx.IsWiki {
base = ctx.Links.WikiLink()
} else {
base = ctx.Links.Base
} }
link = []byte(giteautil.URLJoin(pc.Get(urlPrefixKey).(string), lnk))
link = []byte(giteautil.URLJoin(base, string(link)))
} }
if len(link) > 0 && link[0] == '#' { if len(link) > 0 && link[0] == '#' {
link = []byte("#user-content-" + string(link)[1:]) link = []byte("#user-content-" + string(link)[1:])
@ -188,9 +182,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
applyElementDir(v) applyElementDir(v)
case *ast.Text: case *ast.Text:
if v.SoftLineBreak() && !v.HardLineBreak() { if v.SoftLineBreak() && !v.HardLineBreak() {
renderMetas := pc.Get(renderMetasKey).(map[string]string) if ctx.Metas["mode"] != "document" {
mode := renderMetas["mode"]
if mode != "document" {
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
} else { } else {
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)

View file

@ -34,9 +34,6 @@ var (
) )
var ( var (
urlPrefixKey = parser.NewContextKey()
isWikiKey = parser.NewContextKey()
renderMetasKey = parser.NewContextKey()
renderContextKey = parser.NewContextKey() renderContextKey = parser.NewContextKey()
renderConfigKey = parser.NewContextKey() renderConfigKey = parser.NewContextKey()
) )
@ -66,9 +63,6 @@ func (l *limitWriter) Write(data []byte) (int, error) {
// newParserContext creates a parser.Context with the render context set // newParserContext creates a parser.Context with the render context set
func newParserContext(ctx *markup.RenderContext) parser.Context { func newParserContext(ctx *markup.RenderContext) parser.Context {
pc := parser.NewContext(parser.WithIDs(newPrefixedIDs())) pc := parser.NewContext(parser.WithIDs(newPrefixedIDs()))
pc.Set(urlPrefixKey, ctx.URLPrefix)
pc.Set(isWikiKey, ctx.IsWiki)
pc.Set(renderMetasKey, ctx.Metas)
pc.Set(renderContextKey, ctx) pc.Set(renderContextKey, ctx)
return pc return pc
} }

View file

@ -52,16 +52,20 @@ func TestRender_StandardLinks(t *testing.T) {
test := func(input, expected, expectedWiki string) { test := func(input, expected, expectedWiki string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL, Links: markup.Links{
Base: setting.AppSubURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
buffer, err = RenderString(&markup.RenderContext{ buffer, err = RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL, Links: markup.Links{
IsWiki: true, Base: setting.AppSubURL,
},
IsWiki: true,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
@ -83,8 +87,10 @@ func TestRender_Images(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL, Links: markup.Links{
Base: setting.AppSubURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -107,7 +113,6 @@ func TestRender_Images(t *testing.T) {
"[!["+title+"]("+url+")]("+href+")", "[!["+title+"]("+url+")]("+href+")",
`<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`) `<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`)
url = "/../../.images/src/02/train.jpg"
test( test(
"!["+title+"]("+url+")", "!["+title+"]("+url+")",
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`) `<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`)
@ -286,14 +291,16 @@ func TestTotal_RenderWiki(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
setting.AppSubURL = AppSubURL setting.AppSubURL = AppSubURL
answers := testAnswers(util.URLJoin(AppSubURL, "wiki/"), util.URLJoin(AppSubURL, "wiki", "raw/")) answers := testAnswers(util.URLJoin(AppSubURL, "wiki"), util.URLJoin(AppSubURL, "wiki", "raw"))
for i := 0; i < len(sameCases); i++ { for i := 0; i < len(sameCases); i++ {
line, err := RenderString(&markup.RenderContext{ line, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: AppSubURL, Links: markup.Links{
Metas: localMetas, Base: setting.AppSubURL,
IsWiki: true, },
Metas: localMetas,
IsWiki: true,
}, sameCases[i]) }, sameCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, answers[i], line) assert.Equal(t, answers[i], line)
@ -314,9 +321,11 @@ func TestTotal_RenderWiki(t *testing.T) {
for i := 0; i < len(testCases); i += 2 { for i := 0; i < len(testCases); i += 2 {
line, err := RenderString(&markup.RenderContext{ line, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: AppSubURL, Links: markup.Links{
IsWiki: true, Base: setting.AppSubURL,
},
IsWiki: true,
}, testCases[i]) }, testCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, testCases[i+1], line) assert.Equal(t, testCases[i+1], line)
@ -327,13 +336,16 @@ func TestTotal_RenderString(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
setting.AppSubURL = AppSubURL setting.AppSubURL = AppSubURL
answers := testAnswers(util.URLJoin(AppSubURL, "src", "master/"), util.URLJoin(AppSubURL, "raw", "master/")) answers := testAnswers(util.URLJoin(AppSubURL, "src", "master"), util.URLJoin(AppSubURL, "media", "master"))
for i := 0; i < len(sameCases); i++ { for i := 0; i < len(sameCases); i++ {
line, err := RenderString(&markup.RenderContext{ line, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: util.URLJoin(AppSubURL, "src", "master/"), Links: markup.Links{
Metas: localMetas, Base: AppSubURL,
BranchPath: "master",
},
Metas: localMetas,
}, sameCases[i]) }, sameCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, answers[i], line) assert.Equal(t, answers[i], line)
@ -343,8 +355,10 @@ func TestTotal_RenderString(t *testing.T) {
for i := 0; i < len(testCases); i += 2 { for i := 0; i < len(testCases); i += 2 {
line, err := RenderString(&markup.RenderContext{ line, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: AppSubURL, Links: markup.Links{
Base: AppSubURL,
},
}, testCases[i]) }, testCases[i])
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, testCases[i+1], line) assert.Equal(t, testCases[i+1], line)
@ -556,3 +570,367 @@ foo: bar
assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
} }
} }
func TestRenderLinks(t *testing.T) {
input := ` space @mention-user
/just/a/path.bin
https://example.com/file.bin
[local link](file.bin)
[remote link](https://example.com)
[[local link|file.bin]]
[[remote link|https://example.com]]
![local image](image.jpg)
![remote image](https://example.com/image.jpg)
[[local image|image.jpg]]
[[remote link|https://example.com/image.jpg]]
https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
:+1:
mail@domain.com
@mention-user test
#123
space
`
cases := []struct {
Links markup.Links
IsWiki bool
Expected string
}{
{ // 0
Links: markup.Links{},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/src/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 1
Links: markup.Links{},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/wiki/raw/image.jpg" rel="nofollow"><img src="/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 2
Links: markup.Links{
Base: "https://gitea.io/",
},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="https://gitea.io/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/src/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="https://gitea.io/image.jpg" rel="nofollow"><img src="https://gitea.io/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 3
Links: markup.Links{
Base: "https://gitea.io/",
},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="https://gitea.io/wiki/raw/image.jpg" rel="nofollow"><img src="https://gitea.io/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 4
Links: markup.Links{
Base: "/relative/path",
},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/relative/path/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/src/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/image.jpg" rel="nofollow"><img src="/relative/path/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 5
Links: markup.Links{
Base: "/relative/path",
},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 6
Links: markup.Links{
Base: "/user/repo",
BranchPath: "branch/main",
},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/src/branch/main/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/media/branch/main/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/media/branch/main/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 7
Links: markup.Links{
Base: "/relative/path",
BranchPath: "branch/main",
},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 8
Links: markup.Links{
Base: "/user/repo",
TreePath: "sub/folder",
},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/src/sub/folder/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/image.jpg" rel="nofollow"><img src="/user/repo/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 9
Links: markup.Links{
Base: "/relative/path",
TreePath: "sub/folder",
},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 10
Links: markup.Links{
Base: "/user/repo",
BranchPath: "branch/main",
TreePath: "sub/folder",
},
IsWiki: false,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/src/branch/main/sub/folder/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/media/branch/main/sub/folder/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/media/branch/main/sub/folder/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
{ // 11
Links: markup.Links{
Base: "/relative/path",
BranchPath: "branch/main",
TreePath: "sub/folder",
},
IsWiki: true,
Expected: `<p>space @mention-user<br/>
/just/a/path.bin<br/>
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
<span class="emoji" aria-label="thumbs up">👍</span><br/>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
@mention-user test<br/>
#123<br/>
space</p>
`,
},
}
for i, c := range cases {
result, err := RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input)
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
assert.Equal(t, c.Expected, result, "Unexpected result in testcase %v", i)
}
}

View file

@ -4,7 +4,6 @@
package markup package markup
import ( import (
"bytes"
"fmt" "fmt"
"html" "html"
"io" "io"
@ -101,8 +100,7 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
w := &Writer{ w := &Writer{
HTMLWriter: htmlWriter, HTMLWriter: htmlWriter,
URLPrefix: ctx.URLPrefix, Ctx: ctx,
IsWiki: ctx.IsWiki,
} }
htmlWriter.ExtendingWriter = w htmlWriter.ExtendingWriter = w
@ -132,63 +130,53 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri
// Writer implements org.Writer // Writer implements org.Writer
type Writer struct { type Writer struct {
*org.HTMLWriter *org.HTMLWriter
URLPrefix string Ctx *markup.RenderContext
IsWiki bool
} }
var byteMailto = []byte("mailto:") const mailto = "mailto:"
// WriteRegularLink renders images, links or videos func (r *Writer) resolveLink(l org.RegularLink) string {
func (r *Writer) WriteRegularLink(l org.RegularLink) { link := html.EscapeString(l.URL)
link := []byte(html.EscapeString(l.URL))
if l.Protocol == "file" { if l.Protocol == "file" {
link = link[len("file:"):] link = link[len("file:"):]
} }
if len(link) > 0 && !markup.IsLink(link) && if len(link) > 0 && !markup.IsLinkStr(link) &&
link[0] != '#' && !bytes.HasPrefix(link, byteMailto) { link[0] != '#' && !strings.HasPrefix(link, mailto) {
lnk := string(link) base := r.Ctx.Links.Base
if r.IsWiki { switch l.Kind() {
lnk = util.URLJoin("wiki", lnk) case "image", "video":
base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki)
} }
link = []byte(util.URLJoin(r.URLPrefix, lnk)) link = util.URLJoin(base, link)
} }
return link
}
// WriteRegularLink renders images, links or videos
func (r *Writer) WriteRegularLink(l org.RegularLink) {
link := r.resolveLink(l)
// Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427 // Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427
switch l.Kind() { switch l.Kind() {
case "image": case "image":
if l.Description == nil { if l.Description == nil {
imageSrc := getMediaURL(link) fmt.Fprintf(r, `<img src="%s" alt="%s" />`, link, link)
fmt.Fprintf(r, `<img src="%s" alt="%s" title="%s" />`, imageSrc, link, link)
} else { } else {
description := strings.TrimPrefix(org.String(l.Description...), "file:") imageSrc := r.resolveLink(l.Description[0].(org.RegularLink))
imageSrc := getMediaURL([]byte(description))
fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc) fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc)
} }
case "video": case "video":
if l.Description == nil { if l.Description == nil {
imageSrc := getMediaURL(link) fmt.Fprintf(r, `<video src="%s">%s</video>`, link, link)
fmt.Fprintf(r, `<video src="%s" title="%s">%s</video>`, imageSrc, link, link)
} else { } else {
description := strings.TrimPrefix(org.String(l.Description...), "file:") videoSrc := r.resolveLink(l.Description[0].(org.RegularLink))
videoSrc := getMediaURL([]byte(description)) fmt.Fprintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
fmt.Fprintf(r, `<a href="%s"><video src="%s" title="%s"></video></a>`, link, videoSrc, videoSrc)
} }
default: default:
description := string(link) description := link
if l.Description != nil { if l.Description != nil {
description = r.WriteNodesAsString(l.Description...) description = r.WriteNodesAsString(l.Description...)
} }
fmt.Fprintf(r, `<a href="%s" title="%s">%s</a>`, link, description, description) fmt.Fprintf(r, `<a href="%s" title="%s">%s</a>`, link, description, description)
} }
} }
func getMediaURL(l []byte) string {
srcURL := string(l)
// Check if link is valid
if len(srcURL) > 0 && !markup.IsLink(l) {
srcURL = strings.Replace(srcURL, "/src/", "/media/", 1)
}
return srcURL
}

View file

@ -27,8 +27,10 @@ func TestRender_StandardLinks(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL, Links: markup.Links{
Base: setting.AppSubURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -48,8 +50,10 @@ func TestRender_Media(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL, Links: markup.Links{
Base: setting.AppSubURL,
},
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@ -59,19 +63,19 @@ func TestRender_Media(t *testing.T) {
result := util.URLJoin(AppSubURL, url) result := util.URLJoin(AppSubURL, url)
test("[[file:"+url+"]]", test("[[file:"+url+"]]",
"<p><img src=\""+result+"\" alt=\""+result+"\" title=\""+result+"\" /></p>") "<p><img src=\""+result+"\" alt=\""+result+"\" /></p>")
// With description. // With description.
test("[[https://example.com][https://example.com/example.svg]]", test("[[https://example.com][https://example.com/example.svg]]",
`<p><a href="https://example.com"><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></a></p>`) `<p><a href="https://example.com"><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></a></p>`)
test("[[https://example.com][https://example.com/example.mp4]]", test("[[https://example.com][https://example.com/example.mp4]]",
`<p><a href="https://example.com"><video src="https://example.com/example.mp4" title="https://example.com/example.mp4"></video></a></p>`) `<p><a href="https://example.com"><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></a></p>`)
// Without description. // Without description.
test("[[https://example.com/example.svg]]", test("[[https://example.com/example.svg]]",
`<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" title="https://example.com/example.svg" /></p>`) `<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></p>`)
test("[[https://example.com/example.mp4]]", test("[[https://example.com/example.mp4]]",
`<p><video src="https://example.com/example.mp4" title="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`) `<p><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`)
} }
func TestRender_Source(t *testing.T) { func TestRender_Source(t *testing.T) {
@ -80,8 +84,7 @@ func TestRender_Source(t *testing.T) {
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
URLPrefix: setting.AppSubURL,
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))

View file

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
) )
@ -69,7 +70,7 @@ type RenderContext struct {
RelativePath string // relative path from tree root of the branch RelativePath string // relative path from tree root of the branch
Type string Type string
IsWiki bool IsWiki bool
URLPrefix string Links Links
Metas map[string]string Metas map[string]string
DefaultLink string DefaultLink string
GitRepo *git.Repository GitRepo *git.Repository
@ -80,6 +81,45 @@ type RenderContext struct {
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
} }
type Links struct {
Base string
BranchPath string
TreePath string
}
func (l *Links) HasBranchInfo() bool {
return l.BranchPath != ""
}
func (l *Links) SrcLink() string {
return util.URLJoin(l.Base, "src", l.BranchPath, l.TreePath)
}
func (l *Links) MediaLink() string {
return util.URLJoin(l.Base, "media", l.BranchPath, l.TreePath)
}
func (l *Links) RawLink() string {
return util.URLJoin(l.Base, "raw", l.BranchPath, l.TreePath)
}
func (l *Links) WikiLink() string {
return util.URLJoin(l.Base, "wiki")
}
func (l *Links) WikiRawLink() string {
return util.URLJoin(l.Base, "wiki/raw")
}
func (l *Links) ResolveMediaLink(isWiki bool) string {
if isWiki {
return l.WikiRawLink()
} else if l.HasBranchInfo() {
return l.MediaLink()
}
return l.Base
}
// Cancel runs any cleanup functions that have been registered for this Ctx // Cancel runs any cleanup functions that have been registered for this Ctx
func (ctx *RenderContext) Cancel() { func (ctx *RenderContext) Cancel() {
if ctx == nil { if ctx == nil {

View file

@ -168,8 +168,8 @@ func getPostgreSQLConnectionString(dbHost, dbUser, dbPasswd, dbName, dbsslMode s
RawQuery: dbParam, RawQuery: dbParam,
} }
query := connURL.Query() query := connURL.Query()
if strings.HasPrefix(dbHost, "/") { // looks like a unix socket if strings.HasPrefix(host, "/") { // looks like a unix socket
query.Add("host", dbHost) query.Add("host", host)
connURL.Host = ":" + port connURL.Host = ":" + port
} }
query.Set("sslmode", dbsslMode) query.Set("sslmode", dbsslMode)

View file

@ -77,6 +77,14 @@ func Test_getPostgreSQLConnectionString(t *testing.T) {
SSLMode: "false", SSLMode: "false",
Output: "postgres://testuser:space%20space%20%21%23$%25%5E%5E%25%5E%60%60%60-=%3F=@:5432/gitea?host=%2Ftmp%2Fpg.sock&sslmode=false", Output: "postgres://testuser:space%20space%20%21%23$%25%5E%5E%25%5E%60%60%60-=%3F=@:5432/gitea?host=%2Ftmp%2Fpg.sock&sslmode=false",
}, },
{
Host: "/tmp/pg.sock:6432",
User: "testuser",
Passwd: "pass",
Name: "gitea",
SSLMode: "false",
Output: "postgres://testuser:pass@:6432/gitea?host=%2Ftmp%2Fpg.sock&sslmode=false",
},
{ {
Host: "localhost", Host: "localhost",
User: "pgsqlusername", User: "pgsqlusername",

View file

@ -161,10 +161,13 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
} }
} }
sectionHasDisableQueryAuthToken := sec.HasKey("DISABLE_QUERY_AUTH_TOKEN")
// TODO: default value should be true in future releases // TODO: default value should be true in future releases
DisableQueryAuthToken = sec.Key("DISABLE_QUERY_AUTH_TOKEN").MustBool(false) DisableQueryAuthToken = sec.Key("DISABLE_QUERY_AUTH_TOKEN").MustBool(false)
if !DisableQueryAuthToken { // warn if the setting is set to false explicitly
if sectionHasDisableQueryAuthToken && !DisableQueryAuthToken {
log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will default to true in gitea 1.23 and will be removed in gitea 1.24.") log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will default to true in gitea 1.23 and will be removed in gitea 1.24.")
} }
} }

View file

@ -157,7 +157,6 @@ func NewFuncMap() template.FuncMap {
"RenderEmoji": RenderEmoji, "RenderEmoji": RenderEmoji,
"RenderEmojiPlain": emoji.ReplaceAliases, "RenderEmojiPlain": emoji.ReplaceAliases,
"ReactionToEmoji": ReactionToEmoji, "ReactionToEmoji": ReactionToEmoji,
"RenderNote": RenderNote,
"RenderMarkdownToHtml": RenderMarkdownToHtml, "RenderMarkdownToHtml": RenderMarkdownToHtml,
"RenderLabel": RenderLabel, "RenderLabel": RenderLabel,

View file

@ -24,21 +24,13 @@ import (
) )
// RenderCommitMessage renders commit message with XSS-safe and special links. // RenderCommitMessage renders commit message with XSS-safe and special links.
func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { func RenderCommitMessage(ctx context.Context, msg string, metas map[string]string) template.HTML {
return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
}
// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
// default url, handling for special links.
func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
cleanMsg := template.HTMLEscapeString(msg) cleanMsg := template.HTMLEscapeString(msg)
// we can safely assume that it will not return any error, since there // we can safely assume that it will not return any error, since there
// shouldn't be any special HTML. // shouldn't be any special HTML.
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix, Metas: metas,
DefaultLink: urlDefault,
Metas: metas,
}, cleanMsg) }, cleanMsg)
if err != nil { if err != nil {
log.Error("RenderCommitMessage: %v", err) log.Error("RenderCommitMessage: %v", err)
@ -51,9 +43,9 @@ func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault str
return template.HTML(msgLines[0]) return template.HTML(msgLines[0])
} }
// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to // RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
// the provided default url, handling for special links without email to links. // the provided default url, handling for special links without email to links.
func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML { func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML {
msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace) msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
lineEnd := strings.IndexByte(msgLine, '\n') lineEnd := strings.IndexByte(msgLine, '\n')
if lineEnd > 0 { if lineEnd > 0 {
@ -68,7 +60,6 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefa
// shouldn't be any special HTML. // shouldn't be any special HTML.
renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{ renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix,
DefaultLink: urlDefault, DefaultLink: urlDefault,
Metas: metas, Metas: metas,
}, template.HTMLEscapeString(msgLine)) }, template.HTMLEscapeString(msgLine))
@ -80,7 +71,7 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefa
} }
// RenderCommitBody extracts the body of a commit message without its title. // RenderCommitBody extracts the body of a commit message without its title.
func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { func RenderCommitBody(ctx context.Context, msg string, metas map[string]string) template.HTML {
msgLine := strings.TrimSpace(msg) msgLine := strings.TrimSpace(msg)
lineEnd := strings.IndexByte(msgLine, '\n') lineEnd := strings.IndexByte(msgLine, '\n')
if lineEnd > 0 { if lineEnd > 0 {
@ -94,9 +85,8 @@ func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[stri
} }
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix, Metas: metas,
Metas: metas,
}, template.HTMLEscapeString(msgLine)) }, template.HTMLEscapeString(msgLine))
if err != nil { if err != nil {
log.Error("RenderCommitMessage: %v", err) log.Error("RenderCommitMessage: %v", err)
@ -115,11 +105,10 @@ func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
} }
// RenderIssueTitle renders issue/pull title with defined post processors // RenderIssueTitle renders issue/pull title with defined post processors
func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML { func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) template.HTML {
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix, Metas: metas,
Metas: metas,
}, template.HTMLEscapeString(text)) }, template.HTMLEscapeString(text))
if err != nil { if err != nil {
log.Error("RenderIssueTitle: %v", err) log.Error("RenderIssueTitle: %v", err)
@ -211,25 +200,10 @@ func ReactionToEmoji(reaction string) template.HTML {
return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction))) return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
} }
// RenderNote renders the contents of a git-notes file as a commit message.
func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
cleanMsg := template.HTMLEscapeString(msg)
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
Ctx: ctx,
URLPrefix: urlPrefix,
Metas: metas,
}, cleanMsg)
if err != nil {
log.Error("RenderNote: %v", err)
return ""
}
return template.HTML(fullMessage)
}
func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive
output, err := markdown.RenderString(&markup.RenderContext{ output, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: setting.AppSubURL, Metas: map[string]string{"mode": "document"},
}, input) }, input)
if err != nil { if err != nil {
log.Error("RenderString: %v", err) log.Error("RenderString: %v", err)

View file

@ -6,17 +6,64 @@ package templates
import ( import (
"context" "context"
"html/template" "html/template"
"os"
"testing" "testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const testInput = ` space @mention-user
/just/a/path.bin
https://example.com/file.bin
[local link](file.bin)
[remote link](https://example.com)
[[local link|file.bin]]
[[remote link|https://example.com]]
![local image](image.jpg)
![remote image](https://example.com/image.jpg)
[[local image|image.jpg]]
[[remote link|https://example.com/image.jpg]]
https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
:+1:
mail@domain.com
@mention-user test
#123
space
`
var testMetas = map[string]string{
"user": "user13",
"repo": "repo11",
"repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
"mode": "comment",
}
func TestMain(m *testing.M) {
unittest.InitSettings()
if err := git.InitSimple(context.Background()); err != nil {
log.Fatal("git init failed, err: %v", err)
}
markup.Init(&markup.ProcessorHelper{
IsUsernameMentionable: func(ctx context.Context, username string) bool {
return username == "mention-user"
},
})
os.Exit(m.Run())
}
func TestRenderCommitBody(t *testing.T) { func TestRenderCommitBody(t *testing.T) {
type args struct { type args struct {
ctx context.Context ctx context.Context
msg string msg string
urlPrefix string metas map[string]string
metas map[string]string
} }
tests := []struct { tests := []struct {
name string name string
@ -50,7 +97,91 @@ func TestRenderCommitBody(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, RenderCommitBody(tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas), "RenderCommitBody(%v, %v, %v, %v)", tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas) assert.Equalf(t, tt.want, RenderCommitBody(tt.args.ctx, tt.args.msg, tt.args.metas), "RenderCommitBody(%v, %v, %v)", tt.args.ctx, tt.args.msg, tt.args.metas)
}) })
} }
expected := `/just/a/path.bin
<a href="https://example.com/file.bin" class="link">https://example.com/file.bin</a>
[local link](file.bin)
[remote link](<a href="https://example.com" class="link">https://example.com</a>)
[[local link|file.bin]]
[[remote link|<a href="https://example.com" class="link">https://example.com</a>]]
![local image](image.jpg)
![remote image](<a href="https://example.com/image.jpg" class="link">https://example.com/image.jpg</a>)
[[local image|image.jpg]]
[[remote link|<a href="https://example.com/image.jpg" class="link">https://example.com/image.jpg</a>]]
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="compare"><code class="nohighlight">88fc37a3c0...12fc37a3c0 (hash)</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit"><code class="nohighlight">88fc37a3c0</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span>
<a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a>
<a href="http://localhost:3000/mention-user" class="mention">@mention-user</a> test
<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a>
space`
assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas))
}
func TestRenderCommitMessage(t *testing.T) {
expected := `space <a href="http://localhost:3000/mention-user" class="mention">@mention-user</a> `
assert.EqualValues(t, expected, RenderCommitMessage(context.Background(), testInput, testMetas))
}
func TestRenderCommitMessageLinkSubject(t *testing.T) {
expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="http://localhost:3000/mention-user" class="mention">@mention-user</a>`
assert.EqualValues(t, expected, RenderCommitMessageLinkSubject(context.Background(), testInput, "https://example.com/link", testMetas))
}
func TestRenderIssueTitle(t *testing.T) {
expected := ` space @mention-user
/just/a/path.bin
https://example.com/file.bin
[local link](file.bin)
[remote link](https://example.com)
[[local link|file.bin]]
[[remote link|https://example.com]]
![local image](image.jpg)
![remote image](https://example.com/image.jpg)
[[local image|image.jpg]]
[[remote link|https://example.com/image.jpg]]
https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span>
mail@domain.com
@mention-user test
<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a>
space
`
assert.EqualValues(t, expected, RenderIssueTitle(context.Background(), testInput, testMetas))
}
func TestRenderMarkdownToHtml(t *testing.T) {
expected := `<p>space <a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a><br/>
/just/a/path.bin
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
<a href="/file.bin" rel="nofollow">local link</a>
<a href="https://example.com" rel="nofollow">remote link</a>
<a href="/src/file.bin" rel="nofollow">local link</a>
<a href="https://example.com" rel="nofollow">remote link</a>
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
<a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a> test
#123
space</p>
`
assert.EqualValues(t, expected, RenderMarkdownToHtml(context.Background(), testInput))
} }

View file

@ -422,6 +422,7 @@ authorization_failed_desc = The authorization failed because we detected an inva
sspi_auth_failed = SSPI authentication failed sspi_auth_failed = SSPI authentication failed
password_pwned = The password you chose is on a <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">list of stolen passwords</a> previously exposed in public data breaches. Please try again with a different password and consider changing this password elsewhere too. password_pwned = The password you chose is on a <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">list of stolen passwords</a> previously exposed in public data breaches. Please try again with a different password and consider changing this password elsewhere too.
password_pwned_err = Could not complete request to HaveIBeenPwned password_pwned_err = Could not complete request to HaveIBeenPwned
last_admin = You cannot remove the last admin. There must be at least one admin.
[mail] [mail]
view_it_on = View it on %s view_it_on = View it on %s
@ -587,6 +588,8 @@ org_still_own_packages = "This organization still owns one or more packages, del
target_branch_not_exist = Target branch does not exist. target_branch_not_exist = Target branch does not exist.
admin_cannot_delete_self = You cannot delete yourself when you are an admin. Please remove your admin privileges first.
[user] [user]
change_avatar = Change your avatar… change_avatar = Change your avatar…
joined_on = Joined on %s joined_on = Joined on %s

8
package-lock.json generated
View file

@ -11,7 +11,7 @@
"@citation-js/plugin-software-formats": "0.6.1", "@citation-js/plugin-software-formats": "0.6.1",
"@claviska/jquery-minicolors": "2.3.6", "@claviska/jquery-minicolors": "2.3.6",
"@github/markdown-toolbar-element": "2.2.1", "@github/markdown-toolbar-element": "2.2.1",
"@github/relative-time-element": "4.3.0", "@github/relative-time-element": "4.3.1",
"@github/text-expander-element": "2.5.0", "@github/text-expander-element": "2.5.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.8.0", "@primer/octicons": "19.8.0",
@ -1010,9 +1010,9 @@
"integrity": "sha512-ap+ulyqzG3aVqwKsKjbDdYwM75TQXZpPtmIuPwm+54OTgcC96267oX3cEqd1wSqGsH7O5PonZ//fE9jH7Q4JkA==" "integrity": "sha512-ap+ulyqzG3aVqwKsKjbDdYwM75TQXZpPtmIuPwm+54OTgcC96267oX3cEqd1wSqGsH7O5PonZ//fE9jH7Q4JkA=="
}, },
"node_modules/@github/relative-time-element": { "node_modules/@github/relative-time-element": {
"version": "4.3.0", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.3.0.tgz", "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.3.1.tgz",
"integrity": "sha512-+tFjX9//HRS1HnBa5cNgfEtE52arwiutYg1TOF+Trk40SPxst9Q8Rtc3BKD6aKsvfbtub68vfhipgchGjj9o7g==" "integrity": "sha512-zL79nlhZVCg7x2Pf/HT5MB0mowmErE71VXpF10/3Wy8dQwkninNO1M9aOizh2wKC5LkSpDXqNYjDZwbH0/bcSg=="
}, },
"node_modules/@github/text-expander-element": { "node_modules/@github/text-expander-element": {
"version": "2.5.0", "version": "2.5.0",

View file

@ -10,7 +10,7 @@
"@citation-js/plugin-software-formats": "0.6.1", "@citation-js/plugin-software-formats": "0.6.1",
"@claviska/jquery-minicolors": "2.3.6", "@claviska/jquery-minicolors": "2.3.6",
"@github/markdown-toolbar-element": "2.2.1", "@github/markdown-toolbar-element": "2.2.1",
"@github/relative-time-element": "4.3.0", "@github/relative-time-element": "4.3.1",
"@github/text-expander-element": "2.5.0", "@github/text-expander-element": "2.5.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.8.0", "@primer/octicons": "19.8.0",

View file

@ -257,8 +257,11 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
return return
} }
// update artifact size if zero // update artifact size if zero or not match, over write artifact size
if artifact.FileSize == 0 || artifact.FileCompressedSize == 0 { if artifact.FileSize == 0 ||
artifact.FileCompressedSize == 0 ||
artifact.FileSize != fileRealTotalSize ||
artifact.FileCompressedSize != chunksTotalSize {
artifact.FileSize = fileRealTotalSize artifact.FileSize = fileRealTotalSize
artifact.FileCompressedSize = chunksTotalSize artifact.FileCompressedSize = chunksTotalSize
artifact.ContentEncoding = ctx.Req.Header.Get("Content-Encoding") artifact.ContentEncoding = ctx.Req.Header.Get("Content-Encoding")
@ -267,6 +270,8 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
ctx.Error(http.StatusInternalServerError, "Error update artifact") ctx.Error(http.StatusInternalServerError, "Error update artifact")
return return
} }
log.Debug("[artifact] update artifact size, artifact_id: %d, size: %d, compressed size: %d",
artifact.ID, artifact.FileSize, artifact.FileCompressedSize)
} }
ctx.JSON(http.StatusOK, map[string]string{ ctx.JSON(http.StatusOK, map[string]string{

View file

@ -182,7 +182,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
}() }()
// save storage path to artifact // save storage path to artifact
log.Debug("[artifact] merge chunks to artifact: %d, %s", artifact.ID, storagePath) log.Debug("[artifact] merge chunks to artifact: %d, %s, old:%s", artifact.ID, storagePath, artifact.StoragePath)
// if artifact is already uploaded, delete the old file
if artifact.StoragePath != "" {
if err := st.Delete(artifact.StoragePath); err != nil {
log.Warn("Error deleting old artifact: %s, %v", artifact.StoragePath, err)
}
}
artifact.StoragePath = storagePath artifact.StoragePath = storagePath
artifact.Status = int64(actions.ArtifactStatusUploadConfirmed) artifact.Status = int64(actions.ArtifactStatusUploadConfirmed)
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {

View file

@ -114,11 +114,15 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
}) })
} }
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost for anonymous access) func apiUnauthorizedError(ctx *context.Context) {
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
apiErrorDefined(ctx, errUnauthorized)
}
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
func ReqContainerAccess(ctx *context.Context) { func ReqContainerAccess(ctx *context.Context) {
if ctx.Doer == nil { if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`) apiUnauthorizedError(ctx)
apiErrorDefined(ctx, errUnauthorized)
} }
} }
@ -138,10 +142,15 @@ func DetermineSupport(ctx *context.Context) {
} }
// Authenticate creates a token for the current user // Authenticate creates a token for the current user
// If the current user is anonymous, the ghost user is used // If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
func Authenticate(ctx *context.Context) { func Authenticate(ctx *context.Context) {
u := ctx.Doer u := ctx.Doer
if u == nil { if u == nil {
if setting.Service.RequireSignInView {
apiUnauthorizedError(ctx)
return
}
u = user_model.NewGhostUser() u = user_model.NewGhostUser()
} }

View file

@ -183,6 +183,8 @@ func EditUser(ctx *context.APIContext) {
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/User" // "$ref": "#/responses/User"
// "400":
// "$ref": "#/responses/error"
// "403": // "403":
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
// "422": // "422":
@ -264,6 +266,10 @@ func EditUser(ctx *context.APIContext) {
ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility] ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility]
} }
if form.Admin != nil { if form.Admin != nil {
if !*form.Admin && user_model.IsLastAdminUser(ctx, ctx.ContextUser) {
ctx.Error(http.StatusBadRequest, "LastAdmin", ctx.Tr("auth.last_admin"))
return
}
ctx.ContextUser.IsAdmin = *form.Admin ctx.ContextUser.IsAdmin = *form.Admin
} }
if form.AllowGitHook != nil { if form.AllowGitHook != nil {
@ -341,7 +347,8 @@ func DeleteUser(ctx *context.APIContext) {
if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil { if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
if models.IsErrUserOwnRepos(err) || if models.IsErrUserOwnRepos(err) ||
models.IsErrUserHasOrgs(err) || models.IsErrUserHasOrgs(err) ||
models.IsErrUserOwnPackages(err) { models.IsErrUserOwnPackages(err) ||
models.IsErrDeleteLastAdminUser(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err) ctx.Error(http.StatusUnprocessableEntity, "", err)
} else { } else {
ctx.Error(http.StatusInternalServerError, "DeleteUser", err) ctx.Error(http.StatusInternalServerError, "DeleteUser", err)

View file

@ -194,8 +194,10 @@ func getCommitStatuses(ctx *context.APIContext, sha string) {
listOptions := utils.GetListOptions(ctx) listOptions := utils.GetListOptions(ctx)
statuses, maxResults, err := git_model.GetCommitStatuses(ctx, repo, sha, &git_model.CommitStatusOptions{ statuses, maxResults, err := git_model.GetCommitStatuses(ctx, &git_model.CommitStatusOptions{
ListOptions: listOptions, ListOptions: listOptions,
RepoID: repo.ID,
SHA: sha,
SortType: ctx.FormTrim("sort"), SortType: ctx.FormTrim("sort"),
State: ctx.FormTrim("state"), State: ctx.FormTrim("state"),
}) })

View file

@ -32,8 +32,10 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
case "markdown": case "markdown":
// Raw markdown // Raw markdown
if err := markdown.RenderRaw(&markup.RenderContext{ if err := markdown.RenderRaw(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix, Links: markup.Links{
Base: urlPrefix,
},
}, strings.NewReader(text), ctx.Resp); err != nil { }, strings.NewReader(text), ctx.Resp); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error()) ctx.Error(http.StatusInternalServerError, err.Error())
} }
@ -75,8 +77,10 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
} }
if err := markup.Render(&markup.RenderContext{ if err := markup.Render(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: urlPrefix, Links: markup.Links{
Base: urlPrefix,
},
Metas: meta, Metas: meta,
IsWiki: wiki, IsWiki: wiki,
Type: markupType, Type: markupType,

View file

@ -429,6 +429,12 @@ func EditUserPost(ctx *context.Context) {
} }
// Check whether user is the last admin
if !form.Admin && user_model.IsLastAdminUser(ctx, u) {
ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
return
}
u.LoginName = form.LoginName u.LoginName = form.LoginName
u.FullName = form.FullName u.FullName = form.FullName
emailChanged := !strings.EqualFold(u.Email, form.Email) emailChanged := !strings.EqualFold(u.Email, form.Email)
@ -496,7 +502,10 @@ func DeleteUser(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid"))) ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
case models.IsErrUserOwnPackages(err): case models.IsErrUserOwnPackages(err):
ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages")) ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid")) ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
case models.IsErrDeleteLastAdminUser(err):
ctx.Flash.Error(ctx.Tr("auth.last_admin"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
default: default:
ctx.ServerError("DeleteUser", err) ctx.ServerError("DeleteUser", err)
} }

View file

@ -51,9 +51,11 @@ func toReleaseLink(ctx *context.Context, act *activities_model.Action) string {
// If rendering fails, the original markdown text is returned // If rendering fails, the original markdown text is returned
func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) string { func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) string {
markdownCtx := &markup.RenderContext{ markdownCtx := &markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: act.GetRepoLink(ctx), Links: markup.Links{
Type: markdown.MarkupName, Base: act.GetRepoLink(ctx),
},
Type: markdown.MarkupName,
Metas: map[string]string{ Metas: map[string]string{
"user": act.GetRepoUserName(ctx), "user": act.GetRepoUserName(ctx),
"repo": act.GetRepoName(ctx), "repo": act.GetRepoName(ctx),
@ -199,7 +201,6 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
switch act.OpType { switch act.OpType {
case activities_model.ActionCommitRepo, activities_model.ActionMirrorSyncPush: case activities_model.ActionCommitRepo, activities_model.ActionMirrorSyncPush:
push := templates.ActionContent2Commits(act) push := templates.ActionContent2Commits(act)
repoLink := act.GetRepoAbsoluteLink(ctx)
for _, commit := range push.Commits { for _, commit := range push.Commits {
if len(desc) != 0 { if len(desc) != 0 {
@ -208,7 +209,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
desc += fmt.Sprintf("<a href=\"%s\">%s</a>\n%s", desc += fmt.Sprintf("<a href=\"%s\">%s</a>\n%s",
html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(ctx), commit.Sha1)), html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(ctx), commit.Sha1)),
commit.Sha1, commit.Sha1,
templates.RenderCommitMessage(ctx, commit.Message, repoLink, nil), templates.RenderCommitMessage(ctx, commit.Message, nil),
) )
} }
@ -288,9 +289,11 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
link := &feeds.Link{Href: rel.HTMLURL()} link := &feeds.Link{Href: rel.HTMLURL()}
content, err = markdown.RenderString(&markup.RenderContext{ content, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: rel.Repo.Link(), Links: markup.Links{
Metas: rel.Repo.ComposeMetas(), Base: rel.Repo.Link(),
},
Metas: rel.Repo.ComposeMetas(),
}, rel.Note) }, rel.Note)
if err != nil { if err != nil {

View file

@ -42,8 +42,10 @@ func showUserFeed(ctx *context.Context, formatType string) {
} }
ctxUserDescription, err := markdown.RenderString(&markup.RenderContext{ ctxUserDescription, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: ctx.ContextUser.HTMLURL(), Links: markup.Links{
Base: ctx.ContextUser.HTMLURL(),
},
Metas: map[string]string{ Metas: map[string]string{
"user": ctx.ContextUser.GetDisplayName(), "user": ctx.ContextUser.GetDisplayName(),
}, },

View file

@ -44,10 +44,8 @@ func Home(ctx *context.Context) {
ctx.Data["Title"] = org.DisplayName() ctx.Data["Title"] = org.DisplayName()
if len(org.Description) != 0 { if len(org.Description) != 0 {
desc, err := markdown.RenderString(&markup.RenderContext{ desc, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: ctx.Repo.RepoLink, Metas: map[string]string{"mode": "document"},
Metas: map[string]string{"mode": "document"},
GitRepo: ctx.Repo.GitRepo,
}, org.Description) }, org.Description)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

View file

@ -7,7 +7,9 @@ package repo
import ( import (
"errors" "errors"
"fmt" "fmt"
"html/template"
"net/http" "net/http"
"path"
"strings" "strings"
asymkey_model "code.gitea.io/gitea/models/asymkey" asymkey_model "code.gitea.io/gitea/models/asymkey"
@ -21,7 +23,9 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitgraph" "code.gitea.io/gitea/modules/gitgraph"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
git_service "code.gitea.io/gitea/services/repository" git_service "code.gitea.io/gitea/services/repository"
) )
@ -370,9 +374,21 @@ func Diff(ctx *context.Context) {
note := &git.Note{} note := &git.Note{}
err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note) err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note)
if err == nil { if err == nil {
ctx.Data["Note"] = string(charset.ToUTF8WithFallback(note.Message))
ctx.Data["NoteCommit"] = note.Commit ctx.Data["NoteCommit"] = note.Commit
ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit)
ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(&markup.RenderContext{
Links: markup.Links{
Base: ctx.Repo.RepoLink,
BranchPath: path.Join("commit", util.PathEscapeSegments(commitID)),
},
Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message))))
if err != nil {
ctx.ServerError("RenderCommitMessage", err)
return
}
} }
ctx.Data["BranchName"], err = commit.GetBranchName() ctx.Data["BranchName"], err = commit.GetBranchName()

View file

@ -1424,12 +1424,13 @@ func ViewIssue(ctx *context.Context) {
} }
} }
ctx.Data["IssueWatch"] = iw ctx.Data["IssueWatch"] = iw
issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, issue.Content) }, issue.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -1589,10 +1590,12 @@ func ViewIssue(ctx *context.Context) {
} }
comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, comment.Content) }, comment.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -1666,10 +1669,12 @@ func ViewIssue(ctx *context.Context) {
} }
} else if comment.Type.HasContentSupport() { } else if comment.Type.HasContentSupport() {
comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, comment.Content) }, comment.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -2220,10 +2225,12 @@ func UpdateIssueContent(ctx *context.Context) {
} }
content, err := markdown.RenderString(&markup.RenderContext{ content, err := markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(), Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, issue.Content) }, issue.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -3129,10 +3136,12 @@ func UpdateCommentContent(ctx *context.Context) {
} }
content, err := markdown.RenderString(&markup.RenderContext{ content, err := markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(), Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, comment.Content) }, comment.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

View file

@ -81,10 +81,12 @@ func Milestones(ctx *context.Context) {
} }
for _, m := range miles { for _, m := range miles {
m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, m.Content) }, m.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -275,10 +277,12 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
} }
milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, milestone.Content) }, milestone.Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

View file

@ -86,10 +86,12 @@ func Projects(ctx *context.Context) {
for i := range projects { for i := range projects {
projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, projects[i].Description) }, projects[i].Description)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -353,10 +355,12 @@ func ViewProject(ctx *context.Context) {
ctx.Data["LinkedPRs"] = linkedPrsMap ctx.Data["LinkedPRs"] = linkedPrsMap
project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, project.Description) }, project.Description)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

View file

@ -175,10 +175,12 @@ func releasesOrTags(ctx *context.Context, isTagList bool) {
} }
r.Note, err = markdown.RenderString(&markup.RenderContext{ r.Note, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, r.Note) }, r.Note)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -281,10 +283,12 @@ func SingleRelease(ctx *context.Context) {
} }
} }
release.Note, err = markdown.RenderString(&markup.RenderContext{ release.Note, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, release.Note) }, release.Note)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

View file

@ -57,16 +57,15 @@ func RenderFile(ctx *context.Context) {
return return
} }
treeLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
if ctx.Repo.TreePath != "" {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts") ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
err = markup.Render(&markup.RenderContext{ err = markup.Render(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
RelativePath: ctx.Repo.TreePath, RelativePath: ctx.Repo.TreePath,
URLPrefix: path.Dir(treeLink), Links: markup.Links{
Base: ctx.Repo.RepoLink,
BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath),
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(), Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
GitRepo: ctx.Repo.GitRepo, GitRepo: ctx.Repo.GitRepo,
InStandalonePage: true, InStandalonePage: true,

View file

@ -158,7 +158,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
return "", readmeFile, nil return "", readmeFile, nil
} }
func renderDirectory(ctx *context.Context, treeLink string) { func renderDirectory(ctx *context.Context) {
entries := renderDirectoryFiles(ctx, 1*time.Second) entries := renderDirectoryFiles(ctx, 1*time.Second)
if ctx.Written() { if ctx.Written() {
return return
@ -175,7 +175,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
return return
} }
renderReadmeFile(ctx, subfolder, readmeFile, treeLink) renderReadmeFile(ctx, subfolder, readmeFile)
} }
// localizedExtensions prepends the provided language code with and without a // localizedExtensions prepends the provided language code with and without a
@ -259,7 +259,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
} }
func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry, readmeTreelink string) { func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
target := readmeFile target := readmeFile
if readmeFile != nil && readmeFile.IsLink() { if readmeFile != nil && readmeFile.IsLink() {
target, _ = readmeFile.FollowLinks() target, _ = readmeFile.FollowLinks()
@ -312,9 +312,13 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
Ctx: ctx, Ctx: ctx,
RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path). RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
URLPrefix: path.Join(readmeTreelink, subfolder), Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeDocumentMetas(), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Join(ctx.Repo.TreePath, subfolder),
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
GitRepo: ctx.Repo.GitRepo,
}, rd) }, rd)
if err != nil { if err != nil {
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err) log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
@ -333,7 +337,7 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
} }
} }
func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink string) { func renderFile(ctx *context.Context, entry *git.TreeEntry) {
ctx.Data["IsViewFile"] = true ctx.Data["IsViewFile"] = true
ctx.Data["HideRepoInfo"] = true ctx.Data["HideRepoInfo"] = true
blob := entry.Blob() blob := entry.Blob()
@ -347,7 +351,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
ctx.Data["FileIsSymlink"] = entry.IsLink() ctx.Data["FileIsSymlink"] = entry.IsLink()
ctx.Data["FileName"] = blob.Name() ctx.Data["FileName"] = blob.Name()
ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
if ctx.Repo.TreePath == ".editorconfig" { if ctx.Repo.TreePath == ".editorconfig" {
_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) _, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
@ -475,9 +479,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
Ctx: ctx, Ctx: ctx,
Type: markupType, Type: markupType,
RelativePath: ctx.Repo.TreePath, RelativePath: ctx.Repo.TreePath,
URLPrefix: path.Dir(treeLink), Links: markup.Links{
Metas: metas, Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath),
},
Metas: metas,
GitRepo: ctx.Repo.GitRepo,
}, rd) }, rd)
if err != nil { if err != nil {
ctx.ServerError("Render", err) ctx.ServerError("Render", err)
@ -576,9 +584,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
Ctx: ctx, Ctx: ctx,
RelativePath: ctx.Repo.TreePath, RelativePath: ctx.Repo.TreePath,
URLPrefix: path.Dir(treeLink), Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeDocumentMetas(), Base: ctx.Repo.RepoLink,
GitRepo: ctx.Repo.GitRepo, BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath),
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
GitRepo: ctx.Repo.GitRepo,
}, rd) }, rd)
if err != nil { if err != nil {
ctx.ServerError("Render", err) ctx.ServerError("Render", err)
@ -936,14 +948,6 @@ func renderCode(ctx *context.Context) {
} }
ctx.Data["Title"] = title ctx.Data["Title"] = title
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
treeLink := branchLink
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
if len(ctx.Repo.TreePath) > 0 {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
// Get Topics of this repo // Get Topics of this repo
renderRepoTopics(ctx) renderRepoTopics(ctx)
if ctx.Written() { if ctx.Written() {
@ -968,9 +972,9 @@ func renderCode(ctx *context.Context) {
} }
if entry.IsDir() { if entry.IsDir() {
renderDirectory(ctx, treeLink) renderDirectory(ctx)
} else { } else {
renderFile(ctx, entry, treeLink, rawLink) renderFile(ctx, entry)
} }
if ctx.Written() { if ctx.Written() {
return return
@ -1011,6 +1015,12 @@ func renderCode(ctx *context.Context) {
} }
ctx.Data["Paths"] = paths ctx.Data["Paths"] = paths
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
treeLink := branchLink
if len(ctx.Repo.TreePath) > 0 {
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
ctx.Data["TreeLink"] = treeLink ctx.Data["TreeLink"] = treeLink
ctx.Data["TreeNames"] = treeNames ctx.Data["TreeNames"] = treeNames
ctx.Data["BranchLink"] = branchLink ctx.Data["BranchLink"] = branchLink

View file

@ -238,10 +238,12 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
} }
rctx := &markup.RenderContext{ rctx := &markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: ctx.Repo.RepoLink, Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
Metas: ctx.Repo.Repository.ComposeDocumentMetas(), Links: markup.Links{
IsWiki: true, Base: ctx.Repo.RepoLink,
},
IsWiki: true,
} }
buf := &strings.Builder{} buf := &strings.Builder{}

View file

@ -44,10 +44,8 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
if len(ctx.ContextUser.Description) != 0 { if len(ctx.ContextUser.Description) != 0 {
content, err := markdown.RenderString(&markup.RenderContext{ content, err := markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink, Metas: map[string]string{"mode": "document"},
Metas: map[string]string{"mode": "document"}, Ctx: ctx,
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, ctx.ContextUser.Description) }, ctx.ContextUser.Description)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)
@ -84,7 +82,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
} }
} }
func FindUserProfileReadme(ctx *context.Context) (profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) { func FindUserProfileReadme(ctx *context.Context) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
profileDbRepo, err := repo_model.GetRepositoryByName(ctx.ContextUser.ID, ".profile") profileDbRepo, err := repo_model.GetRepositoryByName(ctx.ContextUser.ID, ".profile")
if err == nil && !profileDbRepo.IsEmpty && !profileDbRepo.IsPrivate { if err == nil && !profileDbRepo.IsEmpty && !profileDbRepo.IsPrivate {
if profileGitRepo, err = git.OpenRepository(ctx, profileDbRepo.RepoPath()); err != nil { if profileGitRepo, err = git.OpenRepository(ctx, profileDbRepo.RepoPath()); err != nil {
@ -97,7 +95,7 @@ func FindUserProfileReadme(ctx *context.Context) (profileGitRepo *git.Repository
} }
} }
} }
return profileGitRepo, profileReadmeBlob, func() { return profileDbRepo, profileGitRepo, profileReadmeBlob, func() {
if profileGitRepo != nil { if profileGitRepo != nil {
_ = profileGitRepo.Close() _ = profileGitRepo.Close()
} }
@ -107,7 +105,7 @@ func FindUserProfileReadme(ctx *context.Context) (profileGitRepo *git.Repository
func RenderUserHeader(ctx *context.Context) { func RenderUserHeader(ctx *context.Context) {
prepareContextForCommonProfile(ctx) prepareContextForCommonProfile(ctx)
_, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx) _, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx)
defer profileClose() defer profileClose()
ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil
} }

View file

@ -246,9 +246,11 @@ func Milestones(ctx *context.Context) {
} }
milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: milestones[i].Repo.Link(), Links: markup.Links{
Metas: milestones[i].Repo.ComposeMetas(), Base: milestones[i].Repo.Link(),
Ctx: ctx, },
Metas: milestones[i].Repo.ComposeMetas(),
Ctx: ctx,
}, milestones[i].Content) }, milestones[i].Content)
if err != nil { if err != nil {
ctx.ServerError("RenderString", err) ctx.ServerError("RenderString", err)

View file

@ -7,6 +7,7 @@ package user
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path"
"strings" "strings"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
@ -64,17 +65,17 @@ func userProfile(ctx *context.Context) {
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
} }
profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx) profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx)
defer profileClose() defer profileClose()
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
prepareUserProfileTabData(ctx, showPrivate, profileGitRepo, profileReadmeBlob) prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileGitRepo, profileReadmeBlob)
// call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing // call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing
shared_user.PrepareContextForProfileBigAvatar(ctx) shared_user.PrepareContextForProfileBigAvatar(ctx)
ctx.HTML(http.StatusOK, tplProfile) ctx.HTML(http.StatusOK, tplProfile)
} }
func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileGitRepo *git.Repository, profileReadme *git.Blob) { func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadme *git.Blob) {
// if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page // if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page
// if there is not a profile readme, the overview tab should be treated as the repositories tab // if there is not a profile readme, the overview tab should be treated as the repositories tab
tab := ctx.FormString("tab") tab := ctx.FormString("tab")
@ -236,7 +237,16 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileGi
if profileContent, err := markdown.RenderString(&markup.RenderContext{ if profileContent, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
GitRepo: profileGitRepo, GitRepo: profileGitRepo,
Metas: map[string]string{"mode": "document"}, Links: markup.Links{
// Give the repo link to the markdown render for the full link of media element.
// the media link usually be like /[user]/[repoName]/media/branch/[branchName],
// Eg. /Tom/.profile/media/branch/main
// The branch shown on the profile page is the default branch, this need to be in sync with doc, see:
// https://docs.gitea.com/usage/profile-readme
Base: profileDbRepo.Link(),
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
},
Metas: map[string]string{"mode": "document"},
}, bytes); err != nil { }, bytes); err != nil {
log.Error("failed to RenderString: %v", err) log.Error("failed to RenderString: %v", err)
} else { } else {

View file

@ -246,6 +246,13 @@ func DeleteAccount(ctx *context.Context) {
return return
} }
// admin should not delete themself
if ctx.Doer.IsAdmin {
ctx.Flash.Error(ctx.Tr("form.admin_cannot_delete_self"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if err := user.DeleteUser(ctx, ctx.Doer, false); err != nil { if err := user.DeleteUser(ctx, ctx.Doer, false); err != nil {
switch { switch {
case models.IsErrUserOwnRepos(err): case models.IsErrUserOwnRepos(err):
@ -257,6 +264,9 @@ func DeleteAccount(ctx *context.Context) {
case models.IsErrUserOwnPackages(err): case models.IsErrUserOwnPackages(err):
ctx.Flash.Error(ctx.Tr("form.still_own_packages")) ctx.Flash.Error(ctx.Tr("form.still_own_packages"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account") ctx.Redirect(setting.AppSubURL + "/user/settings/account")
case models.IsErrDeleteLastAdminUser(err):
ctx.Flash.Error(ctx.Tr("auth.last_admin"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
default: default:
ctx.ServerError("DeleteUser", err) ctx.ServerError("DeleteUser", err)
} }

View file

@ -152,24 +152,28 @@ func notify(ctx context.Context, input *notifyInput) error {
workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit,
input.Event, input.Event,
input.Payload, input.Payload,
input.Event == webhook_module.HookEventPush && input.Ref == input.Repo.DefaultBranch, input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch,
) )
if err != nil { if err != nil {
return fmt.Errorf("DetectWorkflows: %w", err) return fmt.Errorf("DetectWorkflows: %w", err)
} }
if len(workflows) == 0 { log.Trace("repo %s with commit %s event %s find %d workflows and %d schedules",
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID) input.Repo.RepoPath(),
} else { commit.ID,
for _, wf := range workflows { input.Event,
if actionsConfig.IsWorkflowDisabled(wf.EntryName) { len(workflows),
log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName) len(schedules),
continue )
}
if wf.TriggerEvent.Name != actions_module.GithubEventPullRequestTarget { for _, wf := range workflows {
detectedWorkflows = append(detectedWorkflows, wf) if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
} log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName)
continue
}
if wf.TriggerEvent.Name != actions_module.GithubEventPullRequestTarget {
detectedWorkflows = append(detectedWorkflows, wf)
} }
} }

View file

@ -220,9 +220,11 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
// This is the body of the new issue or comment, not the mail body // This is the body of the new issue or comment, not the mail body
body, err := markdown.RenderString(&markup.RenderContext{ body, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: ctx.Issue.Repo.HTMLURL(), Links: markup.Links{
Metas: ctx.Issue.Repo.ComposeMetas(), Base: ctx.Issue.Repo.HTMLURL(),
},
Metas: ctx.Issue.Repo.ComposeMetas(),
}, ctx.Content) }, ctx.Content)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -57,9 +57,11 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
var err error var err error
rel.RenderedNote, err = markdown.RenderString(&markup.RenderContext{ rel.RenderedNote, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: rel.Repo.Link(), Links: markup.Links{
Metas: rel.Repo.ComposeMetas(), Base: rel.Repo.HTMLURL(),
},
Metas: rel.Repo.ComposeMetas(),
}, rel.Note) }, rel.Note)
if err != nil { if err != nil {
log.Error("markdown.RenderString(%d): %v", rel.RepoID, err) log.Error("markdown.RenderString(%d): %v", rel.RepoID, err)

View file

@ -267,7 +267,7 @@ func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *re
defer t.Close() defer t.Close()
var lastCommitID string var lastCommitID string
if err := t.Clone(repo.DefaultBranch); err != nil { if err := t.Clone(repo.DefaultBranch, true); err != nil {
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
return err return err
} }

View file

@ -26,9 +26,12 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid i
} }
defer committer.Close() defer committer.Close()
if has, err := db.GetEngine(ctx).Delete(collaboration); err != nil || has == 0 { if has, err := db.GetEngine(ctx).Delete(collaboration); err != nil {
return err return err
} else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { } else if has == 0 {
return committer.Commit()
}
if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
return err return err
} }

View file

@ -31,12 +31,15 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
log.Error("%v", err) log.Error("%v", err)
} }
defer t.Close() defer t.Close()
if err := t.Clone(opts.OldBranch); err != nil { if err := t.Clone(opts.OldBranch, false); err != nil {
return nil, err return nil, err
} }
if err := t.SetDefaultIndex(); err != nil { if err := t.SetDefaultIndex(); err != nil {
return nil, err return nil, err
} }
if err := t.RefreshIndex(); err != nil {
return nil, err
}
// Get the commit of the original branch // Get the commit of the original branch
commit, err := t.GetBranchCommit(opts.OldBranch) commit, err := t.GetBranchCommit(opts.OldBranch)

View file

@ -21,7 +21,7 @@ func GetDiffPreview(ctx context.Context, repo *repo_model.Repository, branch, tr
return nil, err return nil, err
} }
defer t.Close() defer t.Close()
if err := t.Clone(branch); err != nil { if err := t.Clone(branch, true); err != nil {
return nil, err return nil, err
} }
if err := t.SetDefaultIndex(); err != nil { if err := t.SetDefaultIndex(); err != nil {

View file

@ -108,7 +108,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
log.Error("%v", err) log.Error("%v", err)
} }
defer t.Close() defer t.Close()
if err := t.Clone(opts.OldBranch); err != nil { if err := t.Clone(opts.OldBranch, true); err != nil {
return nil, err return nil, err
} }
if err := t.SetDefaultIndex(); err != nil { if err := t.SetDefaultIndex(); err != nil {

View file

@ -51,8 +51,13 @@ func (t *TemporaryUploadRepository) Close() {
} }
// Clone the base repository to our path and set branch as the HEAD // Clone the base repository to our path and set branch as the HEAD
func (t *TemporaryUploadRepository) Clone(branch string) error { func (t *TemporaryUploadRepository) Clone(branch string, bare bool) error {
if _, _, err := git.NewCommand(t.ctx, "clone", "-s", "--bare", "-b").AddDynamicArguments(branch, t.repo.RepoPath(), t.basePath).RunStdString(nil); err != nil { cmd := git.NewCommand(t.ctx, "clone", "-s", "-b").AddDynamicArguments(branch, t.repo.RepoPath(), t.basePath)
if bare {
cmd.AddArguments("--bare")
}
if _, _, err := cmd.RunStdString(nil); err != nil {
stderr := err.Error() stderr := err.Error()
if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched { if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
return git.ErrBranchNotExist{ return git.ErrBranchNotExist{
@ -98,6 +103,14 @@ func (t *TemporaryUploadRepository) SetDefaultIndex() error {
return nil return nil
} }
// RefreshIndex looks at the current index and checks to see if merges or updates are needed by checking stat() information.
func (t *TemporaryUploadRepository) RefreshIndex() error {
if _, _, err := git.NewCommand(t.ctx, "update-index", "--refresh").RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil {
return fmt.Errorf("RefreshIndex: %w", err)
}
return nil
}
// LsFiles checks if the given filename arguments are in the index // LsFiles checks if the given filename arguments are in the index
func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, error) { func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, error) {
stdOut := new(bytes.Buffer) stdOut := new(bytes.Buffer)

View file

@ -141,7 +141,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
} }
defer t.Close() defer t.Close()
hasOldBranch := true hasOldBranch := true
if err := t.Clone(opts.OldBranch); err != nil { if err := t.Clone(opts.OldBranch, true); err != nil {
for _, file := range opts.Files { for _, file := range opts.Files {
if file.Operation == "delete" { if file.Operation == "delete" {
return nil, err return nil, err

View file

@ -87,7 +87,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
defer t.Close() defer t.Close()
hasOldBranch := true hasOldBranch := true
if err = t.Clone(opts.OldBranch); err != nil { if err = t.Clone(opts.OldBranch, true); err != nil {
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
return err return err
} }

View file

@ -129,6 +129,10 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
return fmt.Errorf("%s is an organization not a user", u.Name) return fmt.Errorf("%s is an organization not a user", u.Name)
} }
if user_model.IsLastAdminUser(ctx, u) {
return models.ErrDeleteLastAdminUser{UID: u.ID}
}
if purge { if purge {
// Disable the user first // Disable the user first
// NOTE: This is deliberately not within a transaction as it must disable the user immediately to prevent any further action by the user to be purged. // NOTE: This is deliberately not within a transaction as it must disable the user immediately to prevent any further action by the user to be purged.
@ -295,7 +299,8 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
} }
if err := DeleteUser(ctx, u, false); err != nil { if err := DeleteUser(ctx, u, false); err != nil {
// Ignore users that were set inactive by admin. // Ignore users that were set inactive by admin.
if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) { if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) ||
models.IsErrUserOwnPackages(err) || models.IsErrDeleteLastAdminUser(err) {
continue continue
} }
return err return err

View file

@ -126,7 +126,7 @@
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span> <span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span>
{{.TagName}} {{.TagName}}
{{if not .IsTag}} {{if not .IsTag}}
<a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | RenderEmoji $.Context}}</a> <a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
{{end}} {{end}}
{{TimeSinceUnix .CreatedUnix ctx.Locale}} {{TimeSinceUnix .CreatedUnix ctx.Locale}}
</p> </p>
@ -146,7 +146,7 @@
{{range .Activity.MergedPRs}} {{range .Activity.MergedPRs}}
<p class="desc"> <p class="desc">
<span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span> <span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span>
#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context}}</a> #{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
{{TimeSinceUnix .MergedUnix ctx.Locale}} {{TimeSinceUnix .MergedUnix ctx.Locale}}
</p> </p>
{{end}} {{end}}
@ -165,7 +165,7 @@
{{range .Activity.OpenedPRs}} {{range .Activity.OpenedPRs}}
<p class="desc"> <p class="desc">
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span> <span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span>
#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context}}</a> #{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
{{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}} {{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
</p> </p>
{{end}} {{end}}
@ -184,7 +184,7 @@
{{range .Activity.ClosedIssues}} {{range .Activity.ClosedIssues}}
<p class="desc"> <p class="desc">
<span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span> <span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span>
#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context}}</a> #{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
{{TimeSinceUnix .ClosedUnix ctx.Locale}} {{TimeSinceUnix .ClosedUnix ctx.Locale}}
</p> </p>
{{end}} {{end}}
@ -203,7 +203,7 @@
{{range .Activity.OpenedIssues}} {{range .Activity.OpenedIssues}}
<p class="desc"> <p class="desc">
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span> <span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span>
#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context}}</a> #{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
{{TimeSinceUnix .CreatedUnix ctx.Locale}} {{TimeSinceUnix .CreatedUnix ctx.Locale}}
</p> </p>
{{end}} {{end}}
@ -221,9 +221,9 @@
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span> <span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span>
#{{.Index}} #{{.Index}}
{{if .IsPull}} {{if .IsPull}}
<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji $.Context}}</a> <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
{{else}} {{else}}
<a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context}}</a> <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
{{end}} {{end}}
{{TimeSinceUnix .UpdatedUnix ctx.Locale}} {{TimeSinceUnix .UpdatedUnix ctx.Locale}}
</p> </p>

View file

@ -25,7 +25,7 @@
<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button> <button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}} {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}}
</div> </div>
<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage .RepoLink .Repository.ComposeMetas}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p> <p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage .Repository.ComposeMetas}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
</td> </td>
<td class="right aligned middle aligned overflow-visible"> <td class="right aligned middle aligned overflow-visible">
{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}} {{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}}
@ -101,7 +101,7 @@
<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button> <button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}} {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
</div> </div>
<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage $.RepoLink $.Repository.ComposeMetas}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p> <p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage $.Repository.ComposeMetas}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
{{end}} {{end}}
</td> </td>
<td class="two wide ui"> <td class="two wide ui">

View file

@ -19,7 +19,7 @@
{{end}} {{end}}
<div class="ui top attached header clearing segment gt-relative commit-header {{$class}}"> <div class="ui top attached header clearing segment gt-relative commit-header {{$class}}">
<div class="gt-df gt-mb-4 gt-fw"> <div class="gt-df gt-mb-4 gt-fw">
<h3 class="gt-mb-0 gt-f1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses "root" $}}</h3> <h3 class="gt-mb-0 gt-f1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message $.Repository.ComposeMetas}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses "root" $}}</h3>
{{if not $.PageIsWiki}} {{if not $.PageIsWiki}}
<div> <div>
<a class="ui primary tiny button" href="{{.SourcePath}}"> <a class="ui primary tiny button" href="{{.SourcePath}}">
@ -135,7 +135,7 @@
{{end}} {{end}}
</div> </div>
{{if IsMultilineCommitMessage .Commit.Message}} {{if IsMultilineCommitMessage .Commit.Message}}
<pre class="commit-body">{{RenderCommitBody $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</pre> <pre class="commit-body">{{RenderCommitBody $.Context .Commit.Message $.Repository.ComposeMetas}}</pre>
{{end}} {{end}}
{{template "repo/commit_load_branches_and_tags" .}} {{template "repo/commit_load_branches_and_tags" .}}
</div> </div>
@ -258,7 +258,7 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{if .Note}} {{if .NoteRendered}}
<div class="ui top attached header segment git-notes"> <div class="ui top attached header segment git-notes">
{{svg "octicon-note" 16 "gt-mr-3"}} {{svg "octicon-note" 16 "gt-mr-3"}}
{{ctx.Locale.Tr "repo.diff.git-notes"}}: {{ctx.Locale.Tr "repo.diff.git-notes"}}:
@ -276,7 +276,7 @@
<span class="text grey" id="note-authored-time">{{TimeSince .NoteCommit.Author.When ctx.Locale}}</span> <span class="text grey" id="note-authored-time">{{TimeSince .NoteCommit.Author.When ctx.Locale}}</span>
</div> </div>
<div class="ui bottom attached info segment git-notes"> <div class="ui bottom attached info segment git-notes">
<pre class="commit-body">{{RenderNote $.Context .Note $.RepoLink $.Repository.ComposeMetas}}</pre> <pre class="commit-body">{{.NoteRendered | Str2html}}</pre>
</div> </div>
{{end}} {{end}}
{{template "repo/diff/box" .}} {{template "repo/diff/box" .}}

View file

@ -60,7 +60,7 @@
<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | RenderEmoji $.Context}}</span> <span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | RenderEmoji $.Context}}</span>
{{else}} {{else}}
{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}} {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitRepoLink $commitLink $.Repository.ComposeMetas}}</span> <span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitLink $.Repository.ComposeMetas}}</span>
{{end}} {{end}}
</span> </span>
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
@ -68,7 +68,7 @@
{{end}} {{end}}
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses "root" $}} {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses "root" $}}
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .Message $commitRepoLink $.Repository.ComposeMetas}}</pre> <pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .Message $.Repository.ComposeMetas}}</pre>
{{end}} {{end}}
</td> </td>
{{if .Committer}} {{if .Committer}}

View file

@ -38,12 +38,12 @@
</a> </a>
</span> </span>
<span class="gt-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) $commitLink $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</span> <span class="gt-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</span>
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button> <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
{{end}} {{end}}
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
<pre class="commit-body gt-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</pre> <pre class="commit-body gt-hidden">{{RenderCommitBody $.root.Context .Message $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</pre>
{{end}} {{end}}
</div> </div>
{{end}} {{end}}

View file

@ -194,7 +194,7 @@
<div class="twelve wide column issue-title"> <div class="twelve wide column issue-title">
{{ctx.Locale.Tr "repo.pulls.has_pull_request" (print (Escape $.RepoLink) "/pulls/" .PullRequest.Issue.Index) (Escape $.RepoRelPath) .PullRequest.Index | Safe}} {{ctx.Locale.Tr "repo.pulls.has_pull_request" (print (Escape $.RepoLink) "/pulls/" .PullRequest.Issue.Index) (Escape $.RepoRelPath) .PullRequest.Index | Safe}}
<h1> <h1>
<span id="issue-title">{{RenderIssueTitle $.Context .PullRequest.Issue.Title $.RepoLink $.Repository.ComposeMetas}}</span> <span id="issue-title">{{RenderIssueTitle $.Context .PullRequest.Issue.Title $.Repository.ComposeMetas}}</span>
<span class="index">#{{.PullRequest.Issue.Index}}</span> <span class="index">#{{.PullRequest.Issue.Index}}</span>
</h1> </h1>
</div> </div>

View file

@ -29,7 +29,7 @@
</a> </a>
</span> </span>
<span class="message gt-dib gt-ellipsis gt-mr-3"> <span class="message gt-dib gt-ellipsis gt-mr-3">
<span>{{RenderCommitMessage $.Context $commit.Subject $.RepoLink $.Repository.ComposeMetas}}</span> <span>{{RenderCommitMessage $.Context $commit.Subject $.Repository.ComposeMetas}}</span>
</span> </span>
<span class="commit-refs gt-df gt-ac gt-mr-2"> <span class="commit-refs gt-df gt-ac gt-mr-2">
{{range $commit.Refs}} {{range $commit.Refs}}

View file

@ -33,7 +33,7 @@
</div> </div>
{{if .MilestoneID}} {{if .MilestoneID}}
<div class="meta gt-my-2"> <div class="meta gt-my-2">
<a class="milestone" href="{{$.Page.RepoLink}}/milestone/{{.MilestoneID}}"> <a class="milestone" href="{{.Repo.Link}}/milestone/{{.MilestoneID}}">
{{svg "octicon-milestone" 16 "gt-mr-2 gt-vm"}} {{svg "octicon-milestone" 16 "gt-mr-2 gt-vm"}}
<span class="gt-vm">{{.Milestone.Name}}</span> <span class="gt-vm">{{.Milestone.Name}}</span>
</a> </a>
@ -42,7 +42,7 @@
{{if $.Page.LinkedPRs}} {{if $.Page.LinkedPRs}}
{{range index $.Page.LinkedPRs .ID}} {{range index $.Page.LinkedPRs .ID}}
<div class="meta gt-my-2"> <div class="meta gt-my-2">
<a href="{{$.Page.RepoLink}}/pulls/{{.Index}}"> <a href="{{$.Issue.Repo.Link}}/pulls/{{.Index}}">
<span class="gt-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "gt-mr-2 gt-vm"}}</span> <span class="gt-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "gt-mr-2 gt-vm"}}</span>
<span class="gt-vm">{{.Title}} <span class="text light grey">#{{.Index}}</span></span> <span class="gt-vm">{{.Title}} <span class="text light grey">#{{.Index}}</span></span>
</a> </a>
@ -54,7 +54,7 @@
{{if or .Labels .Assignees}} {{if or .Labels .Assignees}}
<div class="extra content labels-list gt-p-0 gt-pt-2"> <div class="extra content labels-list gt-p-0 gt-pt-2">
{{range .Labels}} {{range .Labels}}
<a target="_blank" href="{{$.Page.RepoLink}}/issues?labels={{.ID}}">{{RenderLabel ctx .}}</a> <a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx .}}</a>
{{end}} {{end}}
<div class="right floated"> <div class="right floated">
{{range .Assignees}} {{range .Assignees}}

View file

@ -6,7 +6,7 @@
<div class="issue-title-header"> <div class="issue-title-header">
<div class="issue-title" id="issue-title-wrapper"> <div class="issue-title" id="issue-title-wrapper">
<h1 class="gt-word-break"> <h1 class="gt-word-break">
<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title $.RepoLink $.Repository.ComposeMetas | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span> <span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title $.Repository.ComposeMetas | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
</span> </span>
<div id="edit-title-input" class="ui input gt-f1 gt-hidden"> <div id="edit-title-input" class="ui input gt-f1 gt-hidden">
<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off"> <input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">

View file

@ -26,10 +26,10 @@
</a> </a>
{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses "root" $}} {{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses "root" $}}
{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}} {{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}}
<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}}</span> <span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $commitLink $.Repository.ComposeMetas}}</span>
{{if IsMultilineCommitMessage .LatestCommit.Message}} {{if IsMultilineCommitMessage .LatestCommit.Message}}
<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button> <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .LatestCommit.Message $.RepoLink $.Repository.ComposeMetas}}</pre> <pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .LatestCommit.Message $.Repository.ComposeMetas}}</pre>
{{end}} {{end}}
</span> </span>
{{end}} {{end}}
@ -83,7 +83,7 @@
<span class="truncate"> <span class="truncate">
{{if $commit}} {{if $commit}}
{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}} {{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}}
{{RenderCommitMessageLinkSubject $.Context $commit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}} {{RenderCommitMessageLinkSubject $.Context $commit.Message $commitLink $.Repository.ComposeMetas}}
{{else}} {{else}}
<div class="ui active tiny slow centered inline">…</div> <div class="ui active tiny slow centered inline">…</div>
{{end}} {{end}}

View file

@ -660,6 +660,9 @@
"200": { "200": {
"$ref": "#/responses/User" "$ref": "#/responses/User"
}, },
"400": {
"$ref": "#/responses/error"
},
"403": { "403": {
"$ref": "#/responses/forbidden" "$ref": "#/responses/forbidden"
}, },

View file

@ -90,7 +90,7 @@
<img class="ui avatar" src="{{$push.AvatarLink $.Context .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16"> <img class="ui avatar" src="{{$push.AvatarLink $.Context .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16">
<a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a> <a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a>
<span class="text truncate"> <span class="text truncate">
{{RenderCommitMessage $.Context .Message $repoLink $.ComposeMetas}} {{RenderCommitMessage $.Context .Message $.ComposeMetas}}
</span> </span>
</div> </div>
{{end}} {{end}}

View file

@ -15,8 +15,10 @@ import (
) )
var renderContext = markup.RenderContext{ var renderContext = markup.RenderContext{
Ctx: context.Background(), Ctx: context.Background(),
URLPrefix: "https://example.com/go-gitea/gitea", Links: markup.Links{
Base: "https://example.com/go-gitea/gitea",
},
Metas: map[string]string{ Metas: map[string]string{
"user": "go-gitea", "user": "go-gitea",
"repo": "gitea", "repo": "gitea",

View file

@ -0,0 +1 @@
ref: refs/heads/main

View file

@ -0,0 +1,8 @@
[core]
repositoryformatversion = 0
filemode = true
bare = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://try.gitea.io/me-heer/test_commit_revert.git

View file

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View file

@ -0,0 +1,6 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

View file

@ -0,0 +1,3 @@
# pack-refs with: peeled fully-peeled sorted
46aa6ab2c881ae90e15d9ccfc947d1625c892ce5 refs/heads/develop
deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7 refs/heads/main

View file

@ -0,0 +1 @@
deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7

View file

@ -290,3 +290,93 @@ func TestActionsArtifactUploadWithRetentionDays(t *testing.T) {
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a") req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
} }
func TestActionsArtifactOverwrite(t *testing.T) {
defer tests.PrepareTestEnv(t)()
{
// download old artifact uploaded by tests above, it should 1024 A
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp := MakeRequest(t, req, http.StatusOK)
var listResp listArtifactsResponse
DecodeJSON(t, resp, &listResp)
idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
url := listResp.Value[0].FileContainerResourceURL[idx+1:] + "?itemPath=artifact"
req = NewRequest(t, "GET", url)
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp = MakeRequest(t, req, http.StatusOK)
var downloadResp downloadArtifactResponse
DecodeJSON(t, resp, &downloadResp)
idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
url = downloadResp.Value[0].ContentLocation[idx:]
req = NewRequest(t, "GET", url)
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp = MakeRequest(t, req, http.StatusOK)
body := strings.Repeat("A", 1024)
assert.Equal(t, resp.Body.String(), body)
}
{
// upload same artifact, it uses 4096 B
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
Type: "actions_storage",
Name: "artifact",
})
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp uploadArtifactResponse
DecodeJSON(t, resp, &uploadResp)
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact/abc.txt"
body := strings.Repeat("B", 4096)
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
req.Header.Add("Content-Range", "bytes 0-4095/4096")
req.Header.Add("x-tfs-filelength", "4096")
req.Header.Add("x-actions-results-md5", "wUypcJFeZCK5T6r4lfqzqg==") // base64(md5(body))
MakeRequest(t, req, http.StatusOK)
// confirm artifact upload
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact")
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
MakeRequest(t, req, http.StatusOK)
}
{
// download artifact again, it should 4096 B
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp := MakeRequest(t, req, http.StatusOK)
var listResp listArtifactsResponse
DecodeJSON(t, resp, &listResp)
var uploadedItem listArtifactsResponseItem
for _, item := range listResp.Value {
if item.Name == "artifact" {
uploadedItem = item
break
}
}
assert.Equal(t, uploadedItem.Name, "artifact")
idx := strings.Index(uploadedItem.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
url := uploadedItem.FileContainerResourceURL[idx+1:] + "?itemPath=artifact"
req = NewRequest(t, "GET", url)
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp = MakeRequest(t, req, http.StatusOK)
var downloadResp downloadArtifactResponse
DecodeJSON(t, resp, &downloadResp)
idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
url = downloadResp.Value[0].ContentLocation[idx:]
req = NewRequest(t, "GET", url)
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp = MakeRequest(t, req, http.StatusOK)
body := strings.Repeat("B", 4096)
assert.Equal(t, resp.Body.String(), body)
}
}

View file

@ -21,6 +21,7 @@ import (
container_module "code.gitea.io/gitea/modules/packages/container" container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/minio/sha256-simd" "github.com/minio/sha256-simd"
@ -106,6 +107,14 @@ func TestPackageContainer(t *testing.T) {
req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)) req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
addTokenAuthHeader(req, anonymousToken) addTokenAuthHeader(req, anonymousToken)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()
req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
MakeRequest(t, req, http.StatusUnauthorized)
req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL))
MakeRequest(t, req, http.StatusUnauthorized)
}) })
t.Run("User", func(t *testing.T) { t.Run("User", func(t *testing.T) {

View file

@ -0,0 +1,34 @@
package integration
import (
"net/http"
"testing"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestRepoMergeCommitRevert(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/test_commit_revert/_cherrypick/deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7/main?ref=main&refType=branch&cherry-pick-type=revert")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
req = NewRequestWithValues(t, "POST", "/user2/test_commit_revert/_cherrypick/deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7/main", map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"last_commit": "deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7",
"page_has_posted": "true",
"revert": "true",
"commit_summary": "reverting test commit",
"commit_message": "test message",
"commit_choice": "direct",
"new_branch_name": "test-revert-branch-1",
})
resp = session.MakeRequest(t, req, http.StatusSeeOther)
// A successful revert redirects to the main branch
assert.EqualValues(t, "/user2/test_commit_revert/src/branch/main", resp.Header().Get("Location"))
}

View file

@ -8,10 +8,21 @@ __webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;
export function showGlobalErrorMessage(msg) { export function showGlobalErrorMessage(msg) {
const pageContent = document.querySelector('.page-content'); const pageContent = document.querySelector('.page-content');
if (!pageContent) return; if (!pageContent) return;
const el = document.createElement('div');
el.innerHTML = `<div class="ui container negative message center aligned js-global-error" style="white-space: pre-line;"></div>`; // compact the message to a data attribute to avoid too many duplicated messages
el.childNodes[0].textContent = msg; const msgCompact = msg.replace(/\W/g, '').trim();
pageContent.prepend(el.childNodes[0]); let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
if (!msgDiv) {
const el = document.createElement('div');
el.innerHTML = `<div class="ui container negative message center aligned js-global-error" style="white-space: pre-line;"></div>`;
msgDiv = el.childNodes[0];
}
// merge duplicated messages into "the message (count)" format
const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1;
msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact);
msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString());
msgDiv.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : '');
pageContent.prepend(msgDiv);
} }
/** /**

12
web_src/js/bootstrap.test.js vendored Normal file
View file

@ -0,0 +1,12 @@
import {showGlobalErrorMessage} from './bootstrap.js';
test('showGlobalErrorMessage', () => {
document.body.innerHTML = '<div class="page-content"></div>';
showGlobalErrorMessage('test msg 1');
showGlobalErrorMessage('test msg 2');
showGlobalErrorMessage('test msg 1'); // duplicated
expect(document.body.innerHTML).toContain('>test msg 1 (2)<');
expect(document.body.innerHTML).toContain('>test msg 2<');
expect(document.querySelectorAll('.js-global-error').length).toEqual(2);
});

View file

@ -1,3 +1,5 @@
window.__webpack_public_path__ = '';
window.config = { window.config = {
csrfToken: 'test-csrf-token-123456', csrfToken: 'test-csrf-token-123456',
pageData: {}, pageData: {},