Compare commits

..

46 commits

Author SHA1 Message Date
7adcc0971a gitea.nulo.in: Use Alpine 3.16
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending
2022-09-06 19:57:33 -03:00
1c7676e699 Add .woodpecker.yml for gitea.nulo.in 2022-09-06 19:57:33 -03:00
b27dfdcf5d Allow hidden .READMEs 2022-09-06 19:57:33 -03:00
Lunny Xiao
32eef4aa2e
Add changelog for v1.17.2 (#21089)
Co-authored-by: John Olheiser <john+github@jolheiser.com>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
2022-09-06 17:32:20 -04:00
Tyrone Yeh
449b39ea0e
Fix sub folder in repository missing add file dropdown (#21069) (#21083)
Backport #21069

In repository sub folder missing add file dropdown menu, Probably broken since #20602
2022-09-06 09:42:05 +01:00
zeripath
06f968d662
Fix hard-coded timeout and error panic in API archive download endpoint (#20925) (#21051)
Backport #20925

This commit updates the `GET /api/v1/repos/{owner}/{repo}/archive/{archive}`
endpoint which prior to this PR had a couple of issues.

1. The endpoint had a hard-coded 20s timeout for the archiver to complete after
   which a 500 (Internal Server Error) was returned to client. For a scripted
   API client there was no clear way of telling that the operation timed out and
   that it should retry.

2. Whenever the timeout _did occur_, the code used to panic. This was caused by
   the API endpoint "delegating" to the same call path as the web, which uses a
   slightly different way of reporting errors (HTML rather than JSON for
   example).

   More specifically, `api/v1/repo/file.go#GetArchive` just called through to
   `web/repo/repo.go#Download`, which expects the `Context` to have a `Render`
   field set, but which is `nil` for API calls. Hence, a `nil` pointer error.

The code addresses (1) by dropping the hard-coded timeout. Instead, any
timeout/cancelation on the incoming `Context` is used.

The code addresses (2) by updating the API endpoint to use a separate call path
for the API-triggered archive download. This avoids producing HTML-errors on
errors (it now produces JSON errors).

Signed-off-by: Peter Gardfjäll <peter.gardfjall.work@gmail.com>

Signed-off-by: Peter Gardfjäll <peter.gardfjall.work@gmail.com>
Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: Peter Gardfjäll <peter.gardfjall.work@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2022-09-06 07:54:47 +01:00
Lunny Xiao
084797b4dc
Fix delete user missed some comments (#21067) (#21068) 2022-09-06 06:48:57 +08:00
zeripath
7888a55e8c
Delete unreferenced packages when deleting a package version (#20977) (#21060)
Backport #20977

Delete a package if its last version got deleted. Otherwise removing the owner works only after the clean up job ran.

Fix #20969

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
2022-09-04 12:17:48 -04:00
zeripath
ea416d7d0e
Redirect if user does not exist on admin pages (#20981) (#21059)
Backport #20981

When on /admin/users/ endpoints if the user is no longer in the DB,
redirect instead of causing a http 500.

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
2022-09-04 12:17:35 -04:00
zeripath
0db6add5c0
Set uploadpack.allowFilter etc on gitea serv to enable partial clones with ssh (#20902) (#21058)
Backport #20902

When setting.Git.DisablePartialClone is set to false then the web server will add filter support to web http. It does this by using`-c` command arguments but this will not work on gitea serv as the upload-pack and receive-pack commands do not support this.

Instead we move these options into the .gitconfig instead.

Fix #20400

Signed-off-by: Andrew Thornton <art27@cantab.net>

Signed-off-by: Andrew Thornton <art27@cantab.net>
2022-09-04 12:17:27 -04:00
qwerty287
0ecbb71bee
Fix 500 on time in timeline API (#21052) (#21057)
Backport #21052

Before converting a TrackedTime for the API we need to load its attributes - otherwise we get an NPE.

Fix #21041
2022-09-04 16:12:37 +01:00
Jason Song
ea38455e1f
Fill the specified ref in webhook test payload (#20961) (#21055)
Backport #20961

The webhook payload should use the right ref when it‘s specified in the testing request.

The compare URL should not be empty, a URL like `compare/A...A` seems useless in most cases but is helpful when testing.
2022-09-04 16:12:01 +01:00
zeripath
8fc80b34a0
Add another index for Action table on postgres (#21033) (#21054)
Backport #21033

In #21031 we have discovered that on very big tables postgres will use a
search involving the sort term in preference to the restrictive index.

Therefore we add another index for postgres and update the original migration.

Fix #21031

Signed-off-by: Andrew Thornton <art27@cantab.net>
2022-09-04 16:11:02 +01:00
zeripath
71aa64ae25
fix broken insecureskipverify handling in rediss connection uris (#20967) (#21053)
Backport #20967

Currently, it's impossible to connect to self-signed TLS encrypted redis instances. The problem lies in inproper error handling, when building redis tls options - only invalid booleans are allowed to be used in `tlsConfig` builder. The problem is, when `strconv.ParseBool(...)` returns error, it always defaults to false - meaning it's impossible to set `tlsOptions.InsecureSkipVerify` to true.

Fixes #19213

Co-authored-by: Igor Rzegocki <ajgon@users.noreply.github.com>
2022-09-04 14:59:20 +01:00
zeripath
3aba72c613
Add more checks in migration code (#21011) (#21050)
Backport #21011

When migrating add several more important sanity checks:

* SHAs must be SHAs
* Refs must be valid Refs
* URLs must be reasonable

Signed-off-by: Andrew Thornton <art27@cantab.net>
2022-09-04 08:41:21 -05:00
José Carlos
bd1412c3af
Add Dev, Peer and Optional dependencies to npm PackageMetadataVersion (#21017) (#21044)
Backport #21017

Set DevDependencies, PeerDependencies & OptionalDependencies in npm package metadatas

Fix https://github.com/go-gitea/gitea/issues/21013
2022-09-03 21:11:03 +02:00
silverwind
3973ce36d9
Improve arc-green code theme (#21039) (#21042)
Backport #21039

- Increase contrasts overall
- Add various missing theme classes
- Ensure strings and constants are colored the same across languages
2022-09-03 19:51:09 +02:00
Tyrone Yeh
fbde31fb1e
Add down key check has tribute container (#21016) (#21038)
Backport #21016 

Fixes an issue where users would not be able to select by pressing the down arrow when using @TAG above a message

Bug videos:

https://user-images.githubusercontent.com/1255041/188095999-c4ccde18-e53b-4251-8a14-d90c4042d768.mp4
2022-09-03 14:36:27 +01:00
zeripath
2f0a1eb0d5
Do not add links to Posters or Assignees with ID < 0 (#20577) (#21037)
Backport #20577

There are several places in templates/repo/issue/view_content/comments.tmpl where links are made to Posters or Assignees who are Ghosts or have IDs <0.

Fix #20559

Signed-off-by: Andrew Thornton <art27@cantab.net>

Signed-off-by: Andrew Thornton <art27@cantab.net>
2022-09-03 20:51:10 +08:00
Lucas Azevedo
e3697efbb0
Fix modified due date message (#20388) (#21032)
Backport #20388
2022-09-02 15:18:34 -04:00
Lunny Xiao
989dd5502c
Fix missed sort bug (#21006)
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2022-08-31 23:40:29 +08:00
John Olheiser
54c0fe62cc
Fix input.value attr for RequiredClaimName/Value (#20946) (#21001)
Values set for RequiredClaimName and RequiredClaimValue do not show up on UI.
Fix typo `values` to `value`.

Co-authored-by: soumyadey <soumya.dey@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2022-08-31 09:57:03 -05:00
Jason Song
2e2133d33f
fix: remove redundant if (#20997) 2022-08-30 16:16:33 -05:00
John Olheiser
0d869c574e
Translations for repo buttons (#20834)
* Translations

Signed-off-by: jolheiser <john.olheiser@gmail.com>

* Update locale_de-DE.ini

* Update locales

Signed-off-by: jolheiser <john.olheiser@gmail.com>

Signed-off-by: jolheiser <john.olheiser@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: 6543 <6543@obermui.de>
2022-08-29 16:37:53 +02:00
Lunny Xiao
04105dbb7c
Fix download archiver of a commit (#20962) (#20971) 2022-08-28 13:29:34 +02:00
silverwind
0a0cd75071
Change review buttons to icons to make space for text (#20934) (#20978)
The layout on the review code view was broken depending on length of the text. Change all three buttons to icons with tooltip to make more space for these long texts.

Fixes: #20922
2022-08-27 23:52:00 +03:00
silverwind
85f829fb3c
Enable contenthash in filename for dynamic assets (#20813) (#20932)
This should solve the main problem of dynamic assets getting stale after
a version upgrade. Everything not affected will use query-string based
cache busting, which includes files loaded via HTML or worker scripts.
2022-08-25 07:16:20 +01:00
zeripath
5ebd26d306
Return 404 NotFound if requested attachment does not exist (#20886) (#20941)
Backport #20886

Add code to test if GetAttachmentByID returns an ErrAttachmentNotExist error
and return NotFound instead of InternalServerError

Fix #20884

Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>

Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2022-08-25 11:30:41 +08:00
zeripath
bc7a4375be
Disable doctor logging on panic (#20847) (#20898)
Backport #20847

If permissions are incorrect for writing to the doctor log simply disable the log file
instead of panicing.

Related #20570

Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: delvh <dev.lh@web.de>
2022-08-24 15:04:41 +01:00
zeripath
fbcb42488f
Set no-tags in git fetch on compare (#20893) (#20936)
Backport #20893

In the compare endpoint the git fetch is restricted to a certain branch however,
this does not completely prevent tag acquisition/pollution as git fetch will collect
any tags on that branch.

This causes pollution of the tag namespace and could cause confusion by users.

This PR adds `--no-tags` to the `git fetch` call.

Signed-off-by: Andrew Thornton <art27@cantab.net>

Signed-off-by: Andrew Thornton <art27@cantab.net>
2022-08-23 16:42:55 -04:00
zeripath
0230f1e1aa
In PushMirrorsIterate and MirrorsIterate if limit is negative do not set it (#20837) (#20899)
Backport #20837

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
2022-08-23 12:38:52 -04:00
zeripath
6779c351b1
Fix mirror address setting not working (#20850) (#20904)
Backport #20850

This patch fixes the issue that the mirror address field is ignored from the repo setting form.

Co-authored-by: Gary Wang <wzc782970009@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2022-08-23 10:14:02 -04:00
KN4CK3R
c1889f5b01
Allow multiple metadata files for Maven packages (#20674) (#20916)
* Allow file overwrite for meta files.

* Added tests.

* lint

Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>

Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
2022-08-23 10:00:58 +08:00
Gusted
c0754e9d19
Don't open new page for ext wiki on same repository (#20725) (#20910)
* Don't open new page for ext wiki on same repository (#20725)

- Backport of #20725
  - When the external wiki has been set to a file on the repository, don't open the page on a tab.
  - Resolves #20657

* Gofmt

* Fix line

Co-authored-by: zeripath <art27@cantab.net>
2022-08-22 19:58:11 -04:00
zeripath
bf41958c16
Pad GPG Key ID with preceding zeroes (#20878) (#20885)
Backport #20878

The go crypto library does not pad keyIDs to 16 characters with preceding zeroes. This
is a somewhat confusing thing for most users who expect these to have preceding zeroes.

This PR prefixes any sub 16 length KeyID with preceding zeroes and removes preceding
zeroes from KeyIDs inputted on the API.

Fix #20876

Signed-off-by: Andrew Thornton <art27@cantab.net>
2022-08-22 19:35:18 +01:00
zeripath
033178f2fc
Increase Content field size of gpg_key and public_key to MEDIUMTEXT (#20896) (#20911)
Backport #20896

Unfortunately some keys are too big to fix within the 65535 limit of TEXT on MySQL
this causes issues with these large keys.

Therefore increase these fields to MEDIUMTEXT.

Unfortunately the migration in #20896 cannot be backported to 1.17 so
affected users will have to use `gitea doctor recreate-table gpg_key public_key`

Fix #20894

Signed-off-by: Andrew Thornton <art27@cantab.net>

Signed-off-by: Andrew Thornton <art27@cantab.net>
2022-08-22 15:17:21 +01:00
zeripath
ebc8801fb2
Fix push mirror address backend get error Address cause setting page display error (#20593) (#20901) 2022-08-22 02:14:48 -04:00
zeripath
37458bffbf
Fix panic when an invalid oauth2 name is passed (#20820) (#20900) 2022-08-21 23:23:48 -04:00
zeripath
ec9b43ba16
Remove calls to load Mirrors in user.Dashboard (#20855) (#20897)
Backport #20855

Whilst looking at #20840 I noticed that the Mirrors data doesn't appear
to be being used therefore we can remove this and in fact none of the
related code is used elsewhere so it can also be removed.

Related #20840
Related #20804

Signed-off-by: Andrew Thornton <art27@cantab.net>

Signed-off-by: Andrew Thornton <art27@cantab.net>

Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2022-08-22 09:46:56 +08:00
Gusted
e6ec411491
Fix SQL Query for SearchTeam (#20844) (#20872)
Backport #20844

Currently the function takes in the UserID option, but isn't being used within the SQL query. This patch fixes that by checking that only teams are being returned that the user belongs to.
 
Fix  #20829
2022-08-21 19:31:51 +01:00
silverwind
17d3a474e0
Update codemirror to 5.65.8 (#20875)
Includes fix for https://github.com/codemirror/codemirror5/issues/6893.
2022-08-21 17:25:46 +01:00
zeripath
9e8b1c6630
Double check CloneURL is acceptable (#20869) (#20892)
Backport #20869

Some Migration Downloaders provide re-writing of CloneURLs that may point to
unallowed urls. Recheck after the CloneURL is rewritten.

Signed-off-by: Andrew Thornton <art27@cantab.net>

Signed-off-by: Andrew Thornton <art27@cantab.net>
2022-08-21 09:42:48 -05:00
JonRB
eee51d8366
Ensure that graceful start-up is informed of unused SSH listener (#20877)
The graceful manager waits for 4 listeners to be created or to be told that they are not needed. If it is not told about them it will indefinitely and timeout. 

This leads to SVC hosts not being told of being in the readyState but on Unix would lead to the termination of the process.

There was an unfortunate regression in #20299 which missed this subtly and in the case whereby SSH is disabled the `builtinUnused()` is not called.

This PR adds a call to `builtinUnused()` when not using the builtin ssh to allow `createServerWaitGroup.Done()` to be called. 

In addition it was noted that the if/else clauses for timeout informing of the SVC host were in the wrong order. These have been swapped.

Fix #20609
2022-08-21 20:18:22 +08:00
silverwind
c61ed6fad4
Rework repo buttons (#20602, #20718) (#20719)
* Rework repo buttons (#20602)

* Rework repo buttons

- Replace "New PR" and "Go to File" button with Icon Button
- Move all "Add File" actions into a dropdown button
- Remove most custom styling of clone buttons
- Margin and wiki tweaks

Buttons are now all equal height, mobile layout wraps gracefully.

Fixes: https://github.com/go-gitea/gitea/issues/13671
Replaces: https://github.com/go-gitea/gitea/pull/20375

* Restore history button and hide add button when unable to add (#20718)

Fix two regressions from #20602:

- Restore the 'History' button that was previously unable to render
  because it's show condition was never hit
- Hide the 'Add File' button when there would be no items in the
  dropdown.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2022-08-20 15:49:16 +01:00
wxiaoguang
b88a4b4854
Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867) 2022-08-19 23:54:12 -04:00
wxiaoguang
399917a2d4
Fix UI mis-align for PR commit history (#20845) (#20859) 2022-08-19 16:54:33 +08:00
132 changed files with 2686 additions and 4017 deletions

View file

@ -4,6 +4,50 @@ 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.io). been added to each release, please refer to the [blog](https://blog.gitea.io).
## [1.17.2](https://github.com/go-gitea/gitea/releases/tag/v1.17.2) - 2022-09-06
* SECURITY
* Double check CloneURL is acceptable (#20869) (#20892)
* Add more checks in migration code (#21011) (#21050)
* ENHANCEMENTS
* Fix hard-coded timeout and error panic in API archive download endpoint (#20925) (#21051)
* Improve arc-green code theme (#21039) (#21042)
* Enable contenthash in filename for dynamic assets (#20813) (#20932)
* Don't open new page for ext wiki on same repository (#20725) (#20910)
* Disable doctor logging on panic (#20847) (#20898)
* Remove calls to load Mirrors in user.Dashboard (#20855) (#20897)
* Update codemirror to 5.65.8 (#20875)
* Rework repo buttons (#20602, #20718) (#20719)
* BUGFIXES
* Ensure delete user deletes all comments (#21067) (#21068)
* Delete unreferenced packages when deleting a package version (#20977) (#21060)
* Redirect if user does not exist on admin pages (#20981) (#21059)
* Set uploadpack.allowFilter etc on gitea serv to enable partial clones with ssh (#20902) (#21058)
* Fix 500 on time in timeline API (#21052) (#21057)
* Fill the specified ref in webhook test payload (#20961) (#21055)
* Add another index for Action table on postgres (#21033) (#21054)
* Fix broken insecureskipverify handling in redis connection uris (#20967) (#21053)
* Add Dev, Peer and Optional dependencies to npm PackageMetadataVersion (#21017) (#21044)
* Do not add links to Posters or Assignees with ID < 0 (#20577) (#21037)
* Fix modified due date message (#20388) (#21032)
* Fix missed sort bug (#21006)
* Fix input.value attr for RequiredClaimName/Value (#20946) (#21001)
* Change review buttons to icons to make space for text (#20934) (#20978)
* Fix download archiver of a commit (#20962) (#20971)
* Return 404 NotFound if requested attachment does not exist (#20886) (#20941)
* Set no-tags in git fetch on compare (#20893) (#20936)
* Allow multiple metadata files for Maven packages (#20674) (#20916)
* Increase Content field size of gpg_key and public_key to MEDIUMTEXT (#20896) (#20911)
* Fix mirror address setting not working (#20850) (#20904)
* Fix push mirror address backend get error Address cause setting page display error (#20593) (#20901)
* Fix panic when an invalid oauth2 name is passed (#20820) (#20900)
* In PushMirrorsIterate and MirrorsIterate if limit is negative do not set it (#20837) (#20899)
* Ensure that graceful start-up is informed of unused SSH listener (#20877) (#20888)
* Pad GPG Key ID with preceding zeroes (#20878) (#20885)
* Fix SQL Query for `SearchTeam` (#20844) (#20872)
* Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867)
* Fix UI mis-align for PR commit history (#20845) (#20859)
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/1.17.1) - 2022-08-17 ## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/1.17.1) - 2022-08-17
* SECURITY * SECURITY

View file

@ -5,6 +5,7 @@
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
golog "log" golog "log"
"os" "os"
@ -123,6 +124,47 @@ func runRecreateTable(ctx *cli.Context) error {
}) })
} }
func setDoctorLogger(ctx *cli.Context) {
logFile := ctx.String("log-file")
if !ctx.IsSet("log-file") {
logFile = "doctor.log"
}
colorize := log.CanColorStdout
if ctx.IsSet("color") {
colorize = ctx.Bool("color")
}
if len(logFile) == 0 {
log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"NONE","stacktracelevel":"NONE","colorize":%t}`, colorize))
return
}
defer func() {
recovered := recover()
if recovered == nil {
return
}
err, ok := recovered.(error)
if !ok {
panic(recovered)
}
if errors.Is(err, os.ErrPermission) {
fmt.Fprintf(os.Stderr, "ERROR: Unable to write logs to provided file due to permissions error: %s\n %v\n", logFile, err)
} else {
fmt.Fprintf(os.Stderr, "ERROR: Unable to write logs to provided file: %s\n %v\n", logFile, err)
}
fmt.Fprintf(os.Stderr, "WARN: Logging will be disabled\n Use `--log-file` to configure log file location\n")
log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"NONE","stacktracelevel":"NONE","colorize":%t}`, colorize))
}()
if logFile == "-" {
log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"trace","stacktracelevel":"NONE","colorize":%t}`, colorize))
} else {
log.NewLogger(1000, "doctor", "file", fmt.Sprintf(`{"filename":%q,"level":"trace","stacktracelevel":"NONE"}`, logFile))
}
}
func runDoctor(ctx *cli.Context) error { func runDoctor(ctx *cli.Context) error {
// Silence the default loggers // Silence the default loggers
log.DelNamedLogger("console") log.DelNamedLogger("console")
@ -132,24 +174,13 @@ func runDoctor(ctx *cli.Context) error {
defer cancel() defer cancel()
// Now setup our own // Now setup our own
logFile := ctx.String("log-file") setDoctorLogger(ctx)
if !ctx.IsSet("log-file") {
logFile = "doctor.log"
}
colorize := log.CanColorStdout colorize := log.CanColorStdout
if ctx.IsSet("color") { if ctx.IsSet("color") {
colorize = ctx.Bool("color") colorize = ctx.Bool("color")
} }
if len(logFile) == 0 {
log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"NONE","stacktracelevel":"NONE","colorize":%t}`, colorize))
} else if logFile == "-" {
log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"trace","stacktracelevel":"NONE","colorize":%t}`, colorize))
} else {
log.NewLogger(1000, "doctor", "file", fmt.Sprintf(`{"filename":%q,"level":"trace","stacktracelevel":"NONE"}`, logFile))
}
// Finally redirect the default golog to here // Finally redirect the default golog to here
golog.SetFlags(0) golog.SetFlags(0)
golog.SetPrefix("") golog.SetPrefix("")

View file

@ -112,11 +112,8 @@ func migrateRepoAvatars(ctx context.Context, dstStorage storage.ObjectStorage) e
func migrateRepoArchivers(ctx context.Context, dstStorage storage.ObjectStorage) error { func migrateRepoArchivers(ctx context.Context, dstStorage storage.ObjectStorage) error {
return db.IterateObjects(ctx, func(archiver *repo_model.RepoArchiver) error { return db.IterateObjects(ctx, func(archiver *repo_model.RepoArchiver) error {
p, err := archiver.RelativePath() p := archiver.RelativePath()
if err != nil { _, err := storage.Copy(dstStorage, p, storage.RepoArchives, p)
return err
}
_, err = storage.Copy(dstStorage, p, storage.RepoArchives, p)
return err return err
}) })
} }

View file

@ -5,7 +5,7 @@ mkdir -p ${HOME} && chmod 0700 ${HOME}
if [ ! -w ${HOME} ]; then echo "${HOME} is not writable"; exit 1; fi if [ ! -w ${HOME} ]; then echo "${HOME} is not writable"; exit 1; fi
# Prepare custom folder # Prepare custom folder
mkdir -p ${GITEA_CUSTOM} && chmod 0500 ${GITEA_CUSTOM} mkdir -p ${GITEA_CUSTOM} && chmod 0700 ${GITEA_CUSTOM}
# Prepare temp folder # Prepare temp folder
mkdir -p ${GITEA_TEMP} && chmod 0700 ${GITEA_TEMP} mkdir -p ${GITEA_TEMP} && chmod 0700 ${GITEA_TEMP}

View file

@ -42,6 +42,7 @@ func TestPackageMaven(t *testing.T) {
defer PrintCurrentTest(t)() defer PrintCurrentTest(t)()
putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusCreated) putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusCreated)
putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusBadRequest)
putFile(t, "/maven-metadata.xml", "test", http.StatusOK) putFile(t, "/maven-metadata.xml", "test", http.StatusOK)
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
@ -135,12 +136,14 @@ func TestPackageMaven(t *testing.T) {
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, pfs, 2) assert.Len(t, pfs, 2)
i := 0 for _, pf := range pfs {
if strings.HasSuffix(pfs[1].Name, ".pom") { if strings.HasSuffix(pf.Name, ".pom") {
i = 1 assert.Equal(t, filename+".pom", pf.Name)
assert.True(t, pf.IsLead)
} else {
assert.False(t, pf.IsLead)
}
} }
assert.Equal(t, filename+".pom", pfs[i].Name)
assert.True(t, pfs[i].IsLead)
}) })
t.Run("DownloadPOM", func(t *testing.T) { t.Run("DownloadPOM", func(t *testing.T) {
@ -202,4 +205,13 @@ func TestPackageMaven(t *testing.T) {
assert.Equal(t, checksum, resp.Body.String()) assert.Equal(t, checksum, resp.Body.String())
} }
}) })
t.Run("UploadSnapshot", func(t *testing.T) {
snapshotVersion := packageVersion + "-SNAPSHOT"
putFile(t, fmt.Sprintf("/%s/%s", snapshotVersion, filename), "test", http.StatusCreated)
putFile(t, "/maven-metadata.xml", "test", http.StatusOK)
putFile(t, fmt.Sprintf("/%s/maven-metadata.xml", snapshotVersion), "test", http.StatusCreated)
putFile(t, fmt.Sprintf("/%s/maven-metadata.xml", snapshotVersion), "test-overwrite", http.StatusCreated)
})
} }

View file

@ -223,7 +223,7 @@ func TestAPITeamSearch(t *testing.T) {
defer prepareTestEnv(t)() defer prepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User) org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17}).(*user_model.User)
var results TeamSearchResults var results TeamSearchResults

View file

@ -26,8 +26,19 @@ func TestUserOrgs(t *testing.T) {
orgs := getUserOrgs(t, adminUsername, normalUsername) orgs := getUserOrgs(t, adminUsername, normalUsername)
user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user3"}).(*user_model.User) user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user3"}).(*user_model.User)
user17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user17"}).(*user_model.User)
assert.Equal(t, []*api.Organization{ assert.Equal(t, []*api.Organization{
{
ID: 17,
UserName: user17.Name,
FullName: user17.FullName,
AvatarURL: user17.AvatarLink(),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
{ {
ID: 3, ID: 3,
UserName: user3.Name, UserName: user3.Name,
@ -82,8 +93,19 @@ func TestMyOrgs(t *testing.T) {
var orgs []*api.Organization var orgs []*api.Organization
DecodeJSON(t, resp, &orgs) DecodeJSON(t, resp, &orgs)
user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user3"}).(*user_model.User) user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user3"}).(*user_model.User)
user17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user17"}).(*user_model.User)
assert.Equal(t, []*api.Organization{ assert.Equal(t, []*api.Organization{
{
ID: 17,
UserName: user17.Name,
FullName: user17.FullName,
AvatarURL: user17.AvatarLink(),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
{ {
ID: 3, ID: 3,
UserName: user3.Name, UserName: user3.Name,

View file

@ -179,8 +179,8 @@ func TestOrgRestrictedUser(t *testing.T) {
func TestTeamSearch(t *testing.T) { func TestTeamSearch(t *testing.T) {
defer prepareTestEnv(t)() defer prepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}).(*user_model.User)
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User) org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17}).(*user_model.User)
var results TeamSearchResults var results TeamSearchResults
@ -190,9 +190,9 @@ func TestTeamSearch(t *testing.T) {
req.Header.Add("X-Csrf-Token", csrf) req.Header.Add("X-Csrf-Token", csrf)
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &results) DecodeJSON(t, resp, &results)
assert.NotEmpty(t, results.Data) assert.Len(t, results.Data, 2)
assert.Len(t, results.Data, 1) assert.Equal(t, "review_team", results.Data[0].Name)
assert.Equal(t, "test_team", results.Data[0].Name) assert.Equal(t, "test_team", results.Data[1].Name)
// no access if not organization member // no access if not organization member
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}).(*user_model.User) user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}).(*user_model.User)

View file

@ -1,7 +1,7 @@
export default { export default {
rootDir: 'web_src', rootDir: 'web_src',
setupFilesAfterEnv: ['jest-extended/all'], setupFilesAfterEnv: ['jest-extended/all'],
testEnvironment: '@happy-dom/jest-environment', testEnvironment: 'jest-environment-jsdom',
testMatch: ['<rootDir>/**/*.test.js'], testMatch: ['<rootDir>/**/*.test.js'],
testTimeout: 20000, testTimeout: 20000,
transform: { transform: {

View file

@ -98,7 +98,14 @@ func (a *Action) TableIndices() []*schemas.Index {
actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType) actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType)
actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted") actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted")
return []*schemas.Index{actUserIndex, repoIndex} indices := []*schemas.Index{actUserIndex, repoIndex}
if setting.Database.UsePostgreSQL {
cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType)
cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
indices = append(indices, cudIndex)
}
return indices
} }
// GetOpType gets the ActionType of this action. // GetOpType gets the ActionType of this action.
@ -275,7 +282,7 @@ func (a *Action) GetRefLink() string {
return a.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.BranchPrefix)) return a.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.BranchPrefix))
case strings.HasPrefix(a.RefName, git.TagPrefix): case strings.HasPrefix(a.RefName, git.TagPrefix):
return a.GetRepoLink() + "/src/tag/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.TagPrefix)) return a.GetRepoLink() + "/src/tag/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.TagPrefix))
case len(a.RefName) == 40 && git.SHAPattern.MatchString(a.RefName): case len(a.RefName) == 40 && git.IsValidSHAPattern(a.RefName):
return a.GetRepoLink() + "/src/commit/" + a.RefName return a.GetRepoLink() + "/src/commit/" + a.RefName
default: default:
// FIXME: we will just assume it's a branch - this was the old way - at some point we may want to enforce that there is always a ref here. // FIXME: we will just assume it's a branch - this was the old way - at some point we may want to enforce that there is always a ref here.

View file

@ -33,7 +33,7 @@ type GPGKey struct {
OwnerID int64 `xorm:"INDEX NOT NULL"` OwnerID int64 `xorm:"INDEX NOT NULL"`
KeyID string `xorm:"INDEX CHAR(16) NOT NULL"` KeyID string `xorm:"INDEX CHAR(16) NOT NULL"`
PrimaryKeyID string `xorm:"CHAR(16)"` PrimaryKeyID string `xorm:"CHAR(16)"`
Content string `xorm:"TEXT NOT NULL"` Content string `xorm:"MEDIUMTEXT NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created"` CreatedUnix timeutil.TimeStamp `xorm:"created"`
ExpiredUnix timeutil.TimeStamp ExpiredUnix timeutil.TimeStamp
AddedUnix timeutil.TimeStamp AddedUnix timeutil.TimeStamp
@ -63,6 +63,15 @@ func (key *GPGKey) AfterLoad(session *xorm.Session) {
} }
} }
// PaddedKeyID show KeyID padded to 16 characters
func (key *GPGKey) PaddedKeyID() string {
if len(key.KeyID) > 15 {
return key.KeyID
}
zeros := "0000000000000000"
return zeros[0:16-len(key.KeyID)] + key.KeyID
}
// ListGPGKeys returns a list of public keys belongs to given user. // ListGPGKeys returns a list of public keys belongs to given user.
func ListGPGKeys(ctx context.Context, uid int64, listOptions db.ListOptions) ([]*GPGKey, error) { func ListGPGKeys(ctx context.Context, uid int64, listOptions db.ListOptions) ([]*GPGKey, error) {
sess := db.GetEngine(ctx).Table(&GPGKey{}).Where("owner_id=? AND primary_key_id=''", uid) sess := db.GetEngine(ctx).Table(&GPGKey{}).Where("owner_id=? AND primary_key_id=''", uid)

View file

@ -41,7 +41,7 @@ type PublicKey struct {
OwnerID int64 `xorm:"INDEX NOT NULL"` OwnerID int64 `xorm:"INDEX NOT NULL"`
Name string `xorm:"NOT NULL"` Name string `xorm:"NOT NULL"`
Fingerprint string `xorm:"INDEX NOT NULL"` Fingerprint string `xorm:"INDEX NOT NULL"`
Content string `xorm:"TEXT NOT NULL"` Content string `xorm:"MEDIUMTEXT NOT NULL"`
Mode perm.AccessMode `xorm:"NOT NULL DEFAULT 2"` Mode perm.AccessMode `xorm:"NOT NULL DEFAULT 2"`
Type KeyType `xorm:"NOT NULL DEFAULT 1"` Type KeyType `xorm:"NOT NULL DEFAULT 1"`
LoginSourceID int64 `xorm:"NOT NULL DEFAULT 0"` LoginSourceID int64 `xorm:"NOT NULL DEFAULT 0"`

View file

@ -512,10 +512,14 @@ func GetActiveOAuth2ProviderSources() ([]*Source, error) {
func GetActiveOAuth2SourceByName(name string) (*Source, error) { func GetActiveOAuth2SourceByName(name string) (*Source, error) {
authSource := new(Source) authSource := new(Source)
has, err := db.GetEngine(db.DefaultContext).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource) has, err := db.GetEngine(db.DefaultContext).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource)
if !has || err != nil { if err != nil {
return nil, err return nil, err
} }
if !has {
return nil, fmt.Errorf("oauth2 source not found, name: %q", name)
}
return authSource, nil return authSource, nil
} }

View file

@ -63,3 +63,9 @@
uid: 29 uid: 29
org_id: 17 org_id: 17
is_public: true is_public: true
-
id: 12
uid: 2
org_id: 17
is_public: true

View file

@ -309,7 +309,7 @@
avatar_email: user17@example.com avatar_email: user17@example.com
num_repos: 2 num_repos: 2
is_active: true is_active: true
num_members: 3 num_members: 4
num_teams: 3 num_teams: 3
- -

View file

@ -68,6 +68,7 @@ func LoadIssuesFromBoard(b *project_model.Board) (IssueList, error) {
issues, err := Issues(&IssuesOptions{ issues, err := Issues(&IssuesOptions{
ProjectBoardID: b.ID, ProjectBoardID: b.ID,
ProjectID: b.ProjectID, ProjectID: b.ProjectID,
SortType: "project-column-sorting",
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -79,6 +80,7 @@ func LoadIssuesFromBoard(b *project_model.Board) (IssueList, error) {
issues, err := Issues(&IssuesOptions{ issues, err := Issues(&IssuesOptions{
ProjectBoardID: -1, // Issues without ProjectBoardID ProjectBoardID: -1, // Issues without ProjectBoardID
ProjectID: b.ProjectID, ProjectID: b.ProjectID,
SortType: "project-column-sorting",
}) })
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -5,6 +5,7 @@
package migrations package migrations
import ( import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm" "xorm.io/xorm"
@ -37,8 +38,14 @@ func (*improveActionTableIndicesAction) TableIndices() []*schemas.Index {
actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType) actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType)
actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted") actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted")
indices := []*schemas.Index{actUserIndex, repoIndex}
if setting.Database.UsePostgreSQL {
cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType)
cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
indices = append(indices, cudIndex)
}
return []*schemas.Index{actUserIndex, repoIndex} return indices
} }
func improveActionTableIndices(x *xorm.Engine) error { func improveActionTableIndices(x *xorm.Engine) error {

View file

@ -96,16 +96,7 @@ type SearchTeamOptions struct {
IncludeDesc bool IncludeDesc bool
} }
// SearchTeam search for teams. Caller is responsible to check permissions. func (opts *SearchTeamOptions) toCond() builder.Cond {
func SearchTeam(opts *SearchTeamOptions) ([]*Team, int64, error) {
if opts.Page <= 0 {
opts.Page = 1
}
if opts.PageSize == 0 {
// Default limit
opts.PageSize = 10
}
cond := builder.NewCond() cond := builder.NewCond()
if len(opts.Keyword) > 0 { if len(opts.Keyword) > 0 {
@ -117,10 +108,28 @@ func SearchTeam(opts *SearchTeamOptions) ([]*Team, int64, error) {
cond = cond.And(keywordCond) cond = cond.And(keywordCond)
} }
cond = cond.And(builder.Eq{"org_id": opts.OrgID}) if opts.OrgID > 0 {
cond = cond.And(builder.Eq{"`team`.org_id": opts.OrgID})
}
if opts.UserID > 0 {
cond = cond.And(builder.Eq{"team_user.uid": opts.UserID})
}
return cond
}
// SearchTeam search for teams. Caller is responsible to check permissions.
func SearchTeam(opts *SearchTeamOptions) ([]*Team, int64, error) {
sess := db.GetEngine(db.DefaultContext) sess := db.GetEngine(db.DefaultContext)
opts.SetDefaultValues()
cond := opts.toCond()
if opts.UserID > 0 {
sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
}
count, err := sess. count, err := sess.
Where(cond). Where(cond).
Count(new(Team)) Count(new(Team))
@ -128,6 +137,10 @@ func SearchTeam(opts *SearchTeamOptions) ([]*Team, int64, error) {
return nil, 0, err return nil, 0, err
} }
if opts.UserID > 0 {
sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
}
sess = sess.Where(cond) sess = sess.Where(cond)
if opts.PageSize == -1 { if opts.PageSize == -1 {
opts.PageSize = int(count) opts.PageSize = int(count)
@ -137,6 +150,7 @@ func SearchTeam(opts *SearchTeamOptions) ([]*Team, int64, error) {
teams := make([]*Team, 0, opts.PageSize) teams := make([]*Team, 0, opts.PageSize)
if err = sess. if err = sess.
Where(cond).
OrderBy("lower_name"). OrderBy("lower_name").
Find(&teams); err != nil { Find(&teams); err != nil {
return nil, 0, err return nil, 0, err

View file

@ -214,9 +214,16 @@ func FindUnreferencedPackages(ctx context.Context) ([]*Package, error) {
Find(&ps) Find(&ps)
} }
// HasOwnerPackages tests if a user/org has packages // HasOwnerPackages tests if a user/org has accessible packages
func HasOwnerPackages(ctx context.Context, ownerID int64) (bool, error) { func HasOwnerPackages(ctx context.Context, ownerID int64) (bool, error) {
return db.GetEngine(ctx).Where("owner_id = ?", ownerID).Exist(&Package{}) return db.GetEngine(ctx).
Table("package_version").
Join("INNER", "package", "package.id = package_version.package_id").
Where(builder.Eq{
"package_version.is_internal": false,
"package.owner_id": ownerID,
}).
Exist(&PackageVersion{})
} }
// HasRepositoryPackages tests if a repository has packages // HasRepositoryPackages tests if a repository has packages

View file

@ -0,0 +1,69 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package packages_test
import (
"path/filepath"
"testing"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
_ "code.gitea.io/gitea/models"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
GiteaRootPath: filepath.Join("..", ".."),
})
}
func TestHasOwnerPackages(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
p, err := packages_model.TryInsertPackage(db.DefaultContext, &packages_model.Package{
OwnerID: owner.ID,
LowerName: "package",
})
assert.NotNil(t, p)
assert.NoError(t, err)
// A package without package versions gets automatically cleaned up and should return false
has, err := packages_model.HasOwnerPackages(db.DefaultContext, owner.ID)
assert.False(t, has)
assert.NoError(t, err)
pv, err := packages_model.GetOrInsertVersion(db.DefaultContext, &packages_model.PackageVersion{
PackageID: p.ID,
LowerVersion: "internal",
IsInternal: true,
})
assert.NotNil(t, pv)
assert.NoError(t, err)
// A package with an internal package version gets automaticaly cleaned up and should return false
has, err = packages_model.HasOwnerPackages(db.DefaultContext, owner.ID)
assert.False(t, has)
assert.NoError(t, err)
pv, err = packages_model.GetOrInsertVersion(db.DefaultContext, &packages_model.PackageVersion{
PackageID: p.ID,
LowerVersion: "normal",
IsInternal: false,
})
assert.NotNil(t, pv)
assert.NoError(t, err)
// A package with a normal package version should return true
has, err = packages_model.HasOwnerPackages(db.DefaultContext, owner.ID)
assert.True(t, has)
assert.NoError(t, err)
}

View file

@ -385,8 +385,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
archivePaths := make([]string, 0, len(archives)) archivePaths := make([]string, 0, len(archives))
for _, v := range archives { for _, v := range archives {
p, _ := v.RelativePath() archivePaths = append(archivePaths, v.RelativePath())
archivePaths = append(archivePaths, p)
} }
if _, err := db.DeleteByBean(ctx, &repo_model.RepoArchiver{RepoID: repoID}); err != nil { if _, err := db.DeleteByBean(ctx, &repo_model.RepoArchiver{RepoID: repoID}); err != nil {

View file

@ -39,9 +39,9 @@ func init() {
db.RegisterModel(new(RepoArchiver)) db.RegisterModel(new(RepoArchiver))
} }
// RelativePath returns relative path // RelativePath returns the archive path relative to the archive storage root.
func (archiver *RepoArchiver) RelativePath() (string, error) { func (archiver *RepoArchiver) RelativePath() string {
return fmt.Sprintf("%d/%s/%s.%s", archiver.RepoID, archiver.CommitID[:2], archiver.CommitID, archiver.Type.String()), nil return fmt.Sprintf("%d/%s/%s.%s", archiver.RepoID, archiver.CommitID[:2], archiver.CommitID, archiver.Type.String())
} }
var delRepoArchiver = new(RepoArchiver) var delRepoArchiver = new(RepoArchiver)

View file

@ -8,7 +8,6 @@ package repo
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -108,12 +107,14 @@ func DeleteMirrorByRepoID(repoID int64) error {
// MirrorsIterate iterates all mirror repositories. // MirrorsIterate iterates all mirror repositories.
func MirrorsIterate(limit int, f func(idx int, bean interface{}) error) error { func MirrorsIterate(limit int, f func(idx int, bean interface{}) error) error {
return db.GetEngine(db.DefaultContext). sess := db.GetEngine(db.DefaultContext).
Where("next_update_unix<=?", time.Now().Unix()). Where("next_update_unix<=?", time.Now().Unix()).
And("next_update_unix!=0"). And("next_update_unix!=0").
OrderBy("updated_unix ASC"). OrderBy("updated_unix ASC")
Limit(limit). if limit > 0 {
Iterate(new(Mirror), f) sess = sess.Limit(limit)
}
return sess.Iterate(new(Mirror), f)
} }
// InsertMirror inserts a mirror to database // InsertMirror inserts a mirror to database
@ -121,55 +122,3 @@ func InsertMirror(ctx context.Context, mirror *Mirror) error {
_, err := db.GetEngine(ctx).Insert(mirror) _, err := db.GetEngine(ctx).Insert(mirror)
return err return err
} }
// MirrorRepositoryList contains the mirror repositories
type MirrorRepositoryList []*Repository
func (repos MirrorRepositoryList) loadAttributes(ctx context.Context) error {
if len(repos) == 0 {
return nil
}
// Load mirrors.
repoIDs := make([]int64, 0, len(repos))
for i := range repos {
if !repos[i].IsMirror {
continue
}
repoIDs = append(repoIDs, repos[i].ID)
}
mirrors := make([]*Mirror, 0, len(repoIDs))
if err := db.GetEngine(ctx).
Where("id > 0").
In("repo_id", repoIDs).
Find(&mirrors); err != nil {
return fmt.Errorf("find mirrors: %v", err)
}
set := make(map[int64]*Mirror)
for i := range mirrors {
set[mirrors[i].RepoID] = mirrors[i]
}
for i := range repos {
repos[i].Mirror = set[repos[i].ID]
if repos[i].Mirror != nil {
repos[i].Mirror.Repo = repos[i]
}
}
return nil
}
// LoadAttributes loads the attributes for the given MirrorRepositoryList
func (repos MirrorRepositoryList) LoadAttributes() error {
return repos.loadAttributes(db.DefaultContext)
}
// GetUserMirrorRepositories returns a list of mirror repositories of given user.
func GetUserMirrorRepositories(userID int64) ([]*Repository, error) {
repos := make([]*Repository, 0, 10)
return repos, db.GetEngine(db.DefaultContext).
Where("owner_id = ?", userID).
And("is_mirror = ?", true).
Find(&repos)
}

View file

@ -95,10 +95,12 @@ func GetPushMirrorsByRepoID(repoID int64) ([]*PushMirror, error) {
// PushMirrorsIterate iterates all push-mirror repositories. // PushMirrorsIterate iterates all push-mirror repositories.
func PushMirrorsIterate(limit int, f func(idx int, bean interface{}) error) error { func PushMirrorsIterate(limit int, f func(idx int, bean interface{}) error) error {
return db.GetEngine(db.DefaultContext). sess := db.GetEngine(db.DefaultContext).
Where("last_update + (`interval` / ?) <= ?", time.Second, time.Now().Unix()). Where("last_update + (`interval` / ?) <= ?", time.Second, time.Now().Unix()).
And("`interval` != 0"). And("`interval` != 0").
OrderBy("last_update ASC"). OrderBy("last_update ASC")
Limit(limit). if limit > 0 {
Iterate(new(PushMirror), f) sess = sess.Limit(limit)
}
return sess.Iterate(new(PushMirror), f)
} }

View file

@ -100,9 +100,9 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
// Delete Comments // Delete Comments
const batchSize = 50 const batchSize = 50
for start := 0; ; start += batchSize { for {
comments := make([]*issues_model.Comment, 0, batchSize) comments := make([]*issues_model.Comment, 0, batchSize)
if err = e.Where("type=? AND poster_id=?", issues_model.CommentTypeComment, u.ID).Limit(batchSize, start).Find(&comments); err != nil { if err = e.Where("type=? AND poster_id=?", issues_model.CommentTypeComment, u.ID).Limit(batchSize, 0).Find(&comments); err != nil {
return err return err
} }
if len(comments) == 0 { if len(comments) == 0 {
@ -200,7 +200,7 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
// ***** END: ExternalLoginUser ***** // ***** END: ExternalLoginUser *****
if _, err = e.ID(u.ID).Delete(new(user_model.User)); err != nil { if _, err = e.ID(u.ID).Delete(new(user_model.User)); err != nil {
return fmt.Errorf("Delete: %v", err) return fmt.Errorf("delete: %v", err)
} }
return nil return nil

View file

@ -986,6 +986,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
} }
ctx.Data["BranchName"] = ctx.Repo.BranchName ctx.Data["BranchName"] = ctx.Repo.BranchName
ctx.Data["RefName"] = ctx.Repo.RefName
ctx.Data["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() ctx.Data["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
ctx.Data["TagName"] = ctx.Repo.TagName ctx.Data["TagName"] = ctx.Repo.TagName
ctx.Data["CommitID"] = ctx.Repo.CommitID ctx.Data["CommitID"] = ctx.Repo.CommitID

View file

@ -101,6 +101,12 @@ func ToTimelineComment(c *issues_model.Comment, doer *user_model.User) *api.Time
} }
if c.Time != nil { if c.Time != nil {
err = c.Time.LoadAttributes()
if err != nil {
log.Error("Time.LoadAttributes: %v", err)
return nil
}
comment.TrackedTime = ToTrackedTime(c.Time) comment.TrackedTime = ToTrackedTime(c.Time)
} }

View file

@ -287,7 +287,20 @@ func syncGitConfig() (err error) {
} }
} }
return nil // By default partial clones are disabled, enable them from git v2.22
if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil {
if err = configSet("uploadpack.allowfilter", "true"); err != nil {
return err
}
err = configSet("uploadpack.allowAnySHA1InWant", "true")
} else {
if err = configUnsetAll("uploadpack.allowfilter", "true"); err != nil {
return err
}
err = configUnsetAll("uploadpack.allowAnySHA1InWant", "true")
}
return err
} }
// CheckGitVersionAtLeast check git version is at least the constraint version // CheckGitVersionAtLeast check git version is at least the constraint version

View file

@ -4,7 +4,10 @@
package git package git
import "strings" import (
"regexp"
"strings"
)
const ( const (
// RemotePrefix is the base directory of the remotes information of git. // RemotePrefix is the base directory of the remotes information of git.
@ -15,6 +18,29 @@ const (
pullLen = len(PullPrefix) pullLen = len(PullPrefix)
) )
// refNamePatternInvalid is regular expression with unallowed characters in git reference name
// They cannot have ASCII control characters (i.e. bytes whose values are lower than \040, or \177 DEL), space, tilde ~, caret ^, or colon : anywhere.
// They cannot have question-mark ?, asterisk *, or open bracket [ anywhere
var refNamePatternInvalid = regexp.MustCompile(
`[\000-\037\177 \\~^:?*[]|` + // No absolutely invalid characters
`(?:^[/.])|` + // Not HasPrefix("/") or "."
`(?:/\.)|` + // no "/."
`(?:\.lock$)|(?:\.lock/)|` + // No ".lock/"" or ".lock" at the end
`(?:\.\.)|` + // no ".." anywhere
`(?://)|` + // no "//" anywhere
`(?:@{)|` + // no "@{"
`(?:[/.]$)|` + // no terminal '/' or '.'
`(?:^@$)`) // Not "@"
// IsValidRefPattern ensures that the provided string could be a valid reference
func IsValidRefPattern(name string) bool {
return !refNamePatternInvalid.MatchString(name)
}
func SanitizeRefPattern(name string) string {
return refNamePatternInvalid.ReplaceAllString(name, "_")
}
// Reference represents a Git ref. // Reference represents a Git ref.
type Reference struct { type Reference struct {
Name string Name string

View file

@ -138,7 +138,7 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id SHA1) (*Co
// ConvertToSHA1 returns a Hash object from a potential ID string // ConvertToSHA1 returns a Hash object from a potential ID string
func (repo *Repository) ConvertToSHA1(commitID string) (SHA1, error) { func (repo *Repository) ConvertToSHA1(commitID string) (SHA1, error) {
if len(commitID) == 40 && SHAPattern.MatchString(commitID) { if len(commitID) == 40 && IsValidSHAPattern(commitID) {
sha1, err := NewIDFromString(commitID) sha1, err := NewIDFromString(commitID)
if err == nil { if err == nil {
return sha1, nil return sha1, nil

View file

@ -40,7 +40,7 @@ func (repo *Repository) GetMergeBase(tmpRemote, base, head string) (string, stri
if tmpRemote != "origin" { if tmpRemote != "origin" {
tmpBaseName := RemotePrefix + tmpRemote + "/tmp_" + base tmpBaseName := RemotePrefix + tmpRemote + "/tmp_" + base
// Fetch commit into a temporary branch in order to be able to handle commits and tags // Fetch commit into a temporary branch in order to be able to handle commits and tags
_, _, err := NewCommand(repo.Ctx, "fetch", tmpRemote, base+":"+tmpBaseName).RunStdString(&RunOpts{Dir: repo.Path}) _, _, err := NewCommand(repo.Ctx, "fetch", "--no-tags", tmpRemote, "--", base+":"+tmpBaseName).RunStdString(&RunOpts{Dir: repo.Path})
if err == nil { if err == nil {
base = tmpBaseName base = tmpBaseName
} }

View file

@ -19,7 +19,12 @@ const EmptySHA = "0000000000000000000000000000000000000000"
const EmptyTreeSHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" const EmptyTreeSHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
// SHAPattern can be used to determine if a string is an valid sha // SHAPattern can be used to determine if a string is an valid sha
var SHAPattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`) var shaPattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
// IsValidSHAPattern will check if the provided string matches the SHA Pattern
func IsValidSHAPattern(sha string) bool {
return shaPattern.MatchString(sha)
}
// MustID always creates a new SHA1 from a [20]byte array with no validation of input. // MustID always creates a new SHA1 from a [20]byte array with no validation of input.
func MustID(b []byte) SHA1 { func MustID(b []byte) SHA1 {

View file

@ -114,9 +114,9 @@ func (g *Manager) start() {
// Execute makes Manager implement svc.Handler // Execute makes Manager implement svc.Handler
func (g *Manager) Execute(args []string, changes <-chan svc.ChangeRequest, status chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) { func (g *Manager) Execute(args []string, changes <-chan svc.ChangeRequest, status chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) {
if setting.StartupTimeout > 0 { if setting.StartupTimeout > 0 {
status <- svc.Status{State: svc.StartPending}
} else {
status <- svc.Status{State: svc.StartPending, WaitHint: uint32(setting.StartupTimeout / time.Millisecond)} status <- svc.Status{State: svc.StartPending, WaitHint: uint32(setting.StartupTimeout / time.Millisecond)}
} else {
status <- svc.Status{State: svc.StartPending}
} }
log.Trace("Awaiting server start-up") log.Trace("Awaiting server start-up")

View file

@ -33,7 +33,7 @@ func newLogger(name string, buffer int64) *MultiChannelledLogger {
func (l *MultiChannelledLogger) SetLogger(name, provider, config string) error { func (l *MultiChannelledLogger) SetLogger(name, provider, config string) error {
eventLogger, err := NewChannelledLog(l.ctx, name, provider, config, l.bufferLength) eventLogger, err := NewChannelledLog(l.ctx, name, provider, config, l.bufferLength)
if err != nil { if err != nil {
return fmt.Errorf("Failed to create sublogger (%s): %v", name, err) return fmt.Errorf("failed to create sublogger (%s): %w", name, err)
} }
l.MultiChannelledLog.DelLogger(name) l.MultiChannelledLog.DelLogger(name)
@ -41,9 +41,9 @@ func (l *MultiChannelledLogger) SetLogger(name, provider, config string) error {
err = l.MultiChannelledLog.AddLogger(eventLogger) err = l.MultiChannelledLog.AddLogger(eventLogger)
if err != nil { if err != nil {
if IsErrDuplicateName(err) { if IsErrDuplicateName(err) {
return fmt.Errorf("Duplicate named sublogger %s %v", name, l.MultiChannelledLog.GetEventLoggerNames()) return fmt.Errorf("%w other names: %v", err, l.MultiChannelledLog.GetEventLoggerNames())
} }
return fmt.Errorf("Failed to add sublogger (%s): %v", name, err) return fmt.Errorf("failed to add sublogger (%s): %w", name, err)
} }
return nil return nil

View file

@ -26,7 +26,7 @@ type PullRequest struct {
Updated time.Time Updated time.Time
Closed *time.Time Closed *time.Time
Labels []*Label Labels []*Label
PatchURL string `yaml:"patch_url"` PatchURL string `yaml:"patch_url"` // SECURITY: This must be safe to download directly from
Merged bool Merged bool
MergedTime *time.Time `yaml:"merged_time"` MergedTime *time.Time `yaml:"merged_time"`
MergeCommitSHA string `yaml:"merge_commit_sha"` MergeCommitSHA string `yaml:"merge_commit_sha"`
@ -37,6 +37,7 @@ type PullRequest struct {
Reactions []*Reaction Reactions []*Reaction
ForeignIndex int64 ForeignIndex int64
Context DownloaderContext `yaml:"-"` Context DownloaderContext `yaml:"-"`
EnsuredSafe bool `yaml:"ensured_safe"`
} }
func (p *PullRequest) GetLocalIndex() int64 { return p.Number } func (p *PullRequest) GetLocalIndex() int64 { return p.Number }
@ -55,9 +56,9 @@ func (p PullRequest) GetGitRefName() string {
// PullRequestBranch represents a pull request branch // PullRequestBranch represents a pull request branch
type PullRequestBranch struct { type PullRequestBranch struct {
CloneURL string `yaml:"clone_url"` CloneURL string `yaml:"clone_url"` // SECURITY: This must be safe to download from
Ref string Ref string // SECURITY: this must be a git.IsValidRefPattern
SHA string SHA string // SECURITY: this must be a git.IsValidSHAPattern
RepoName string `yaml:"repo_name"` RepoName string `yaml:"repo_name"`
OwnerName string `yaml:"owner_name"` OwnerName string `yaml:"owner_name"`
} }

View file

@ -18,15 +18,16 @@ type ReleaseAsset struct {
DownloadCount *int `yaml:"download_count"` DownloadCount *int `yaml:"download_count"`
Created time.Time Created time.Time
Updated time.Time Updated time.Time
DownloadURL *string `yaml:"download_url"`
DownloadURL *string `yaml:"download_url"` // SECURITY: It is the responsibility of downloader to make sure this is safe
// if DownloadURL is nil, the function should be invoked // if DownloadURL is nil, the function should be invoked
DownloadFunc func() (io.ReadCloser, error) `yaml:"-"` DownloadFunc func() (io.ReadCloser, error) `yaml:"-"` // SECURITY: It is the responsibility of downloader to make sure this is safe
} }
// Release represents a release // Release represents a release
type Release struct { type Release struct {
TagName string `yaml:"tag_name"` TagName string `yaml:"tag_name"` // SECURITY: This must pass git.IsValidRefPattern
TargetCommitish string `yaml:"target_commitish"` TargetCommitish string `yaml:"target_commitish"` // SECURITY: This must pass git.IsValidRefPattern
Name string Name string
Body string Body string
Draft bool Draft bool

View file

@ -12,7 +12,7 @@ type Repository struct {
IsPrivate bool `yaml:"is_private"` IsPrivate bool `yaml:"is_private"`
IsMirror bool `yaml:"is_mirror"` IsMirror bool `yaml:"is_mirror"`
Description string Description string
CloneURL string `yaml:"clone_url"` CloneURL string `yaml:"clone_url"` // SECURITY: This must be checked to ensure that is safe to be used
OriginalURL string `yaml:"original_url"` OriginalURL string `yaml:"original_url"`
DefaultBranch string DefaultBranch string
} }

View file

@ -245,7 +245,7 @@ func getRedisTLSOptions(uri *url.URL) *tls.Config {
if len(skipverify) > 0 { if len(skipverify) > 0 {
skipverify, err := strconv.ParseBool(skipverify) skipverify, err := strconv.ParseBool(skipverify)
if err != nil { if err == nil {
tlsConfig.InsecureSkipVerify = skipverify tlsConfig.InsecureSkipVerify = skipverify
} }
} }
@ -254,7 +254,7 @@ func getRedisTLSOptions(uri *url.URL) *tls.Config {
if len(insecureskipverify) > 0 { if len(insecureskipverify) > 0 {
insecureskipverify, err := strconv.ParseBool(insecureskipverify) insecureskipverify, err := strconv.ParseBool(insecureskipverify)
if err != nil { if err == nil {
tlsConfig.InsecureSkipVerify = insecureskipverify tlsConfig.InsecureSkipVerify = insecureskipverify
} }
} }

View file

@ -27,6 +27,24 @@ func TestRedisPasswordOpt(t *testing.T) {
} }
} }
func TestSkipVerifyOpt(t *testing.T) {
uri, _ := url.Parse("rediss://myredis/0?skipverify=true")
tlsConfig := getRedisTLSOptions(uri)
if !tlsConfig.InsecureSkipVerify {
t.Fail()
}
}
func TestInsecureSkipVerifyOpt(t *testing.T) {
uri, _ := url.Parse("rediss://myredis/0?insecureskipverify=true")
tlsConfig := getRedisTLSOptions(uri)
if !tlsConfig.InsecureSkipVerify {
t.Fail()
}
}
func TestRedisSentinelUsernameOpt(t *testing.T) { func TestRedisSentinelUsernameOpt(t *testing.T) {
uri, _ := url.Parse("redis+sentinel://redis:password@myredis/0?sentinelusername=suser&sentinelpassword=spass") uri, _ := url.Parse("redis+sentinel://redis:password@myredis/0?sentinelusername=suser&sentinelpassword=spass")
opts := getRedisOptions(uri).Failover() opts := getRedisOptions(uri).Failover()

View file

@ -91,6 +91,8 @@ var (
// LocalURL is the url for locally running applications to contact Gitea. It always has a '/' suffix // LocalURL is the url for locally running applications to contact Gitea. It always has a '/' suffix
// It maps to ini:"LOCAL_ROOT_URL" // It maps to ini:"LOCAL_ROOT_URL"
LocalURL string LocalURL string
// AssetVersion holds a opaque value that is used for cache-busting assets
AssetVersion string
// Server settings // Server settings
Protocol Scheme Protocol Scheme
@ -749,6 +751,7 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
} }
AbsoluteAssetURL = MakeAbsoluteAssetURL(AppURL, StaticURLPrefix) AbsoluteAssetURL = MakeAbsoluteAssetURL(AppURL, StaticURLPrefix)
AssetVersion = strings.ReplaceAll(AppVer, "+", "~") // make sure the version string is clear (no real escaping is needed)
manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL) manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL)
ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes) ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes)

View file

@ -18,6 +18,7 @@ import (
func Init() error { func Init() error {
if setting.SSH.Disabled { if setting.SSH.Disabled {
builtinUnused()
return nil return nil
} }

View file

@ -81,6 +81,9 @@ func NewFuncMap() []template.FuncMap {
"AppDomain": func() string { "AppDomain": func() string {
return setting.Domain return setting.Domain
}, },
"AssetVersion": func() string {
return setting.AssetVersion
},
"DisableGravatar": func() bool { "DisableGravatar": func() bool {
return setting.DisableGravatar return setting.DisableGravatar
}, },
@ -151,7 +154,6 @@ func NewFuncMap() []template.FuncMap {
"DiffTypeToStr": DiffTypeToStr, "DiffTypeToStr": DiffTypeToStr,
"DiffLineTypeToStr": DiffLineTypeToStr, "DiffLineTypeToStr": DiffLineTypeToStr,
"ShortSha": base.ShortSha, "ShortSha": base.ShortSha,
"MD5": base.EncodeMD5,
"ActionContent2Commits": ActionContent2Commits, "ActionContent2Commits": ActionContent2Commits,
"PathEscape": url.PathEscape, "PathEscape": url.PathEscape,
"PathEscapeSegments": util.PathEscapeSegments, "PathEscapeSegments": util.PathEscapeSegments,
@ -454,6 +456,7 @@ func NewFuncMap() []template.FuncMap {
} }
return items return items
}, },
"HasPrefix": strings.HasPrefix,
}} }}
} }
@ -974,11 +977,11 @@ type remoteAddress struct {
Password string Password string
} }
func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string) remoteAddress { func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
a := remoteAddress{} a := remoteAddress{}
remoteURL := m.OriginalURL remoteURL := m.OriginalURL
if remoteURL == "" { if ignoreOriginalURL || remoteURL == "" {
var err error var err error
remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName) remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
if err != nil { if err != nil {

View file

@ -54,6 +54,11 @@ func (ts TimeStamp) AsTime() (tm time.Time) {
return ts.AsTimeInLocation(setting.DefaultUILocation) return ts.AsTimeInLocation(setting.DefaultUILocation)
} }
// AsLocalTime convert timestamp as time.Time in local location
func (ts TimeStamp) AsLocalTime() time.Time {
return time.Unix(int64(ts), 0)
}
// AsTimeInLocation convert timestamp as time.Time in Local locale // AsTimeInLocation convert timestamp as time.Time in Local locale
func (ts TimeStamp) AsTimeInLocation(loc *time.Location) (tm time.Time) { func (ts TimeStamp) AsTimeInLocation(loc *time.Location) (tm time.Time) {
tm = time.Unix(int64(ts), 0).In(loc) tm = time.Unix(int64(ts), 0).In(loc)

View file

@ -9,6 +9,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"code.gitea.io/gitea/modules/git"
"gitea.com/go-chi/binding" "gitea.com/go-chi/binding"
"github.com/gobwas/glob" "github.com/gobwas/glob"
) )
@ -24,30 +26,6 @@ const (
ErrRegexPattern = "RegexPattern" ErrRegexPattern = "RegexPattern"
) )
// GitRefNamePatternInvalid is regular expression with unallowed characters in git reference name
// They cannot have ASCII control characters (i.e. bytes whose values are lower than \040, or \177 DEL), space, tilde ~, caret ^, or colon : anywhere.
// They cannot have question-mark ?, asterisk *, or open bracket [ anywhere
var GitRefNamePatternInvalid = regexp.MustCompile(`[\000-\037\177 \\~^:?*[]+`)
// CheckGitRefAdditionalRulesValid check name is valid on additional rules
func CheckGitRefAdditionalRulesValid(name string) bool {
// Additional rules as described at https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
if strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") ||
strings.HasSuffix(name, ".") || strings.Contains(name, "..") ||
strings.Contains(name, "//") || strings.Contains(name, "@{") ||
name == "@" {
return false
}
parts := strings.Split(name, "/")
for _, part := range parts {
if strings.HasSuffix(part, ".lock") || strings.HasPrefix(part, ".") {
return false
}
}
return true
}
// AddBindingRules adds additional binding rules // AddBindingRules adds additional binding rules
func AddBindingRules() { func AddBindingRules() {
addGitRefNameBindingRule() addGitRefNameBindingRule()
@ -67,16 +45,10 @@ func addGitRefNameBindingRule() {
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
str := fmt.Sprintf("%v", val) str := fmt.Sprintf("%v", val)
if GitRefNamePatternInvalid.MatchString(str) { if !git.IsValidRefPattern(str) {
errs.Add([]string{name}, ErrGitRefName, "GitRefName") errs.Add([]string{name}, ErrGitRefName, "GitRefName")
return false, errs return false, errs
} }
if !CheckGitRefAdditionalRulesValid(str) {
errs.Add([]string{name}, ErrGitRefName, "GitRefName")
return false, errs
}
return true, errs return true, errs
}, },
}) })

View file

@ -1061,6 +1061,7 @@ normal_view=Normale Ansicht
line=zeile line=zeile
lines=Zeilen lines=Zeilen
editor.add_file=Datei hinzufügen
editor.new_file=Neue Datei editor.new_file=Neue Datei
editor.upload_file=Datei hochladen editor.upload_file=Datei hochladen
editor.edit_file=Datei bearbeiten editor.edit_file=Datei bearbeiten

View file

@ -1061,6 +1061,7 @@ normal_view = Normal View
line = line line = line
lines = lines lines = lines
editor.add_file = Add File
editor.new_file = New File editor.new_file = New File
editor.upload_file = Upload File editor.upload_file = Upload File
editor.edit_file = Edit File editor.edit_file = Edit File
@ -1419,7 +1420,7 @@ issues.due_date_form_remove = "Remove"
issues.due_date_not_writer = "You need repository write access to update an issue's due date." issues.due_date_not_writer = "You need repository write access to update an issue's due date."
issues.due_date_not_set = "No due date set." issues.due_date_not_set = "No due date set."
issues.due_date_added = "added the due date %s %s" issues.due_date_added = "added the due date %s %s"
issues.due_date_modified = "modified the due date to %s from %s %s" issues.due_date_modified = "modified the due date from %[2]s to %[1]s %[3]s"
issues.due_date_remove = "removed the due date %s %s" issues.due_date_remove = "removed the due date %s %s"
issues.due_date_overdue = "Overdue" issues.due_date_overdue = "Overdue"
issues.due_date_invalid = "The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'." issues.due_date_invalid = "The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'."

View file

@ -1061,6 +1061,7 @@ normal_view=Vista normal
line=línea line=línea
lines=líneas lines=líneas
editor.add_file=Añadir archivo
editor.new_file=Nuevo Archivo editor.new_file=Nuevo Archivo
editor.upload_file=Subir archivo editor.upload_file=Subir archivo
editor.edit_file=Editar Archivo editor.edit_file=Editar Archivo

View file

@ -820,6 +820,7 @@ normal_view=Vista normale
line=riga line=riga
lines=righe lines=righe
editor.add_file=Aggiungi file
editor.new_file=Nuovo file editor.new_file=Nuovo file
editor.upload_file=Carica File editor.upload_file=Carica File
editor.edit_file=Modifica File editor.edit_file=Modifica File

View file

@ -1061,6 +1061,7 @@ normal_view=Parastais skats
line=rinda line=rinda
lines=rindas lines=rindas
editor.add_file=Pievienot
editor.new_file=Jauna datne editor.new_file=Jauna datne
editor.upload_file=Augšupielādēt failu editor.upload_file=Augšupielādēt failu
editor.edit_file=Labot failu editor.edit_file=Labot failu

View file

@ -838,6 +838,7 @@ normal_view=Normale weergave
line=regel line=regel
lines=regels lines=regels
editor.add_file=Bestand toevoegen
editor.new_file=Nieuw bestand editor.new_file=Nieuw bestand
editor.upload_file=Upload bestand editor.upload_file=Upload bestand
editor.edit_file=Bewerk bestand editor.edit_file=Bewerk bestand

View file

@ -1061,6 +1061,7 @@ normal_view=Vista normal
line=linha line=linha
lines=linhas lines=linhas
editor.add_file=Adicionar ficheiro
editor.new_file=Novo ficheiro editor.new_file=Novo ficheiro
editor.upload_file=Carregar ficheiro editor.upload_file=Carregar ficheiro
editor.edit_file=Editar ficheiro editor.edit_file=Editar ficheiro

View file

@ -948,6 +948,7 @@ normal_view=Normal Görünüm
line=satır line=satır
lines=satır lines=satır
editor.add_file=Dosya Ekle
editor.new_file=Yeni dosya editor.new_file=Yeni dosya
editor.upload_file=Dosya Yükle editor.upload_file=Dosya Yükle
editor.edit_file=Dosyayı Düzenle editor.edit_file=Dosyayı Düzenle

View file

@ -1061,6 +1061,7 @@ normal_view=普通视图
line= line=
lines= lines=
editor.add_file=添加文件
editor.new_file=新建文件 editor.new_file=新建文件
editor.upload_file=上传文件 editor.upload_file=上传文件
editor.edit_file=编辑文件 editor.edit_file=编辑文件

View file

@ -1061,6 +1061,7 @@ normal_view=標準檢視
line= line=
lines= lines=
editor.add_file=加入檔案
editor.new_file=新增文件 editor.new_file=新增文件
editor.upload_file=上傳文件 editor.upload_file=上傳文件
editor.edit_file=編輯文件 editor.edit_file=編輯文件

3980
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -44,7 +44,6 @@
"wrap-ansi": "8.0.1" "wrap-ansi": "8.0.1"
}, },
"devDependencies": { "devDependencies": {
"@happy-dom/jest-environment": "4.0.1",
"eslint": "8.15.0", "eslint": "8.15.0",
"eslint-plugin-html": "6.2.0", "eslint-plugin-html": "6.2.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.26.0",
@ -52,6 +51,7 @@
"eslint-plugin-unicorn": "42.0.0", "eslint-plugin-unicorn": "42.0.0",
"eslint-plugin-vue": "9.0.1", "eslint-plugin-vue": "9.0.1",
"jest": "28.1.0", "jest": "28.1.0",
"jest-environment-jsdom": "28.1.3",
"jest-extended": "2.0.0", "jest-extended": "2.0.0",
"postcss-less": "6.0.0", "postcss-less": "6.0.0",
"stylelint": "14.8.2", "stylelint": "14.8.2",

View file

@ -0,0 +1 @@
<svg viewBox="0 0 16 16" class="svg gitea-join" width="16" height="16" aria-hidden="true"><path d="M14 10.9V8.75h1.25a.75.75 0 0 0 0-1.5H14V5.1a.25.25 0 0 0-.43-.17l-2.9 2.9a.25.25 0 0 0 0 .35l2.9 2.9a.25.25 0 0 0 .43-.18ZM.75 8.75a.75.75 0 0 1 0-1.5H2V5.1a.25.25 0 0 1 .43-.17l2.9 2.9a.25.25 0 0 1 0 .35l-2.9 2.9A.25.25 0 0 1 2 10.9V8.75Zm6.5-6.5a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0zM8 6a.75.75 0 0 1-.75-.75v-.5a.75.75 0 0 1 1.5 0v.5A.75.75 0 0 1 8 6Zm-.75 2.25a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0zM8 12a.75.75 0 0 1-.75-.75v-.5a.75.75 0 0 1 1.5 0v.5A.75.75 0 0 1 8 12Zm-.75 2.25a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0z"/></svg>

After

Width:  |  Height:  |  Size: 645 B

View file

@ -0,0 +1 @@
<svg viewBox="0 0 16 16" class="svg gitea-split" width="16" height="16" aria-hidden="true"><path d="M7.25 14.25a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0zM8 12a.75.75 0 0 1-.75-.75v-.5a.75.75 0 0 1 1.5 0v.5A.75.75 0 0 1 8 12Zm-.75-3.75a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0zM8 6a.75.75 0 0 1-.75-.75v-.5a.75.75 0 0 1 1.5 0v.5A.75.75 0 0 1 8 6Zm-.75-3.75a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0zm4.1 6.5a.75.75 0 0 1 0-1.5h1.25V5.1a.25.25 0 0 1 .43-.17l2.9 2.9a.25.25 0 0 1 0 .35l-2.9 2.9a.25.25 0 0 1-.43-.18V8.75ZM3.4 10.9V8.75h1.25a.75.75 0 0 0 0-1.5H3.4V5.1a.25.25 0 0 0-.43-.17l-2.9 2.9a.25.25 0 0 0 0 .35l2.9 2.9a.25.25 0 0 0 .43-.18z"/></svg>

After

Width:  |  Height:  |  Size: 654 B

View file

@ -0,0 +1 @@
<svg viewBox="0 0 15 15" class="svg gitea-whitespace" width="16" height="16" aria-hidden="true"><path d="m2.5 7.5.35.35a.5.5 0 0 0 0-.7l-.35.35ZM3 4h12V3H3v1Zm4 4h8V7H7v1Zm-4 4h12v-1H3v1ZM.85 9.85l2-2-.7-.7-2 2 .7.7Zm2-2.7-2-2-.7.7 2 2 .7-.7Z"/></svg>

After

Width:  |  Height:  |  Size: 251 B

View file

@ -268,6 +268,7 @@ func UploadPackageFile(ctx *context.Context) {
}, },
Data: buf, Data: buf,
IsLead: false, IsLead: false,
OverwriteExisting: params.IsMeta,
} }
// If it's the package pom file extract the metadata // If it's the package pom file extract the metadata

View file

@ -63,6 +63,9 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
Homepage: metadata.ProjectURL, Homepage: metadata.ProjectURL,
License: metadata.License, License: metadata.License,
Dependencies: metadata.Dependencies, Dependencies: metadata.Dependencies,
DevDependencies: metadata.DevelopmentDependencies,
PeerDependencies: metadata.PeerDependencies,
OptionalDependencies: metadata.OptionalDependencies,
Readme: metadata.Readme, Readme: metadata.Readme,
Dist: npm_module.PackageDistribution{ Dist: npm_module.PackageDistribution{
Shasum: pd.Files[0].Blob.HashSHA1, Shasum: pd.Files[0].Blob.HashSHA1,

View file

@ -17,7 +17,6 @@ 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"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
) )
@ -53,7 +52,7 @@ func GetSingleCommit(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
sha := ctx.Params(":sha") sha := ctx.Params(":sha")
if (validation.GitRefNamePatternInvalid.MatchString(sha) || !validation.CheckGitRefAdditionalRulesValid(sha)) && !git.SHAPattern.MatchString(sha) { if !git.IsValidRefPattern(sha) {
ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha)) ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha))
return return
} }

View file

@ -8,6 +8,7 @@ package repo
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -29,7 +30,7 @@ import (
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/web/repo" archiver_service "code.gitea.io/gitea/services/repository/archiver"
files_service "code.gitea.io/gitea/services/repository/files" files_service "code.gitea.io/gitea/services/repository/files"
) )
@ -294,7 +295,53 @@ func GetArchive(ctx *context.APIContext) {
defer gitRepo.Close() defer gitRepo.Close()
} }
repo.Download(ctx.Context) archiveDownload(ctx)
}
func archiveDownload(ctx *context.APIContext) {
uri := ctx.Params("*")
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
if err != nil {
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
ctx.Error(http.StatusBadRequest, "unknown archive format", err)
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
ctx.Error(http.StatusNotFound, "unrecognized reference", err)
} else {
ctx.ServerError("archiver_service.NewRequest", err)
}
return
}
archiver, err := aReq.Await(ctx)
if err != nil {
ctx.ServerError("archiver.Await", err)
return
}
download(ctx, aReq.GetArchiveName(), archiver)
}
func download(ctx *context.APIContext, archiveName string, archiver *repo_model.RepoArchiver) {
downloadName := ctx.Repo.Repository.Name + "-" + archiveName
rPath := archiver.RelativePath()
if setting.RepoArchive.ServeDirect {
// If we have a signed url (S3, object storage), redirect to this directly.
u, err := storage.RepoArchives.URL(rPath, downloadName)
if u != nil && err == nil {
ctx.Redirect(u.String())
return
}
}
// If we have matched and access to release or issue
fr, err := storage.RepoArchives.Open(rPath)
if err != nil {
ctx.ServerError("Open", err)
return
}
defer fr.Close()
ctx.ServeContent(downloadName, fr, archiver.CreatedUnix.AsLocalTime())
} }
// GetEditorconfig get editor config of a repository // GetEditorconfig get editor config of a repository

View file

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"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/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
@ -140,7 +141,7 @@ func TestHook(ctx *context.APIContext) {
// required: true // required: true
// - name: ref // - name: ref
// in: query // in: query
// description: "The name of the commit/branch/tag. Default the repositorys default branch (usually master)" // description: "The name of the commit/branch/tag, indicates which commit will be loaded to the webhook payload."
// type: string // type: string
// required: false // required: false
// responses: // responses:
@ -153,6 +154,11 @@ func TestHook(ctx *context.APIContext) {
return return
} }
ref := git.BranchPrefix + ctx.Repo.Repository.DefaultBranch
if r := ctx.FormTrim("ref"); r != "" {
ref = r
}
hookID := ctx.ParamsInt64(":id") hookID := ctx.ParamsInt64(":id")
hook, err := utils.GetRepoHook(ctx, ctx.Repo.Repository.ID, hookID) hook, err := utils.GetRepoHook(ctx, ctx.Repo.Repository.ID, hookID)
if err != nil { if err != nil {
@ -161,10 +167,12 @@ func TestHook(ctx *context.APIContext) {
commit := convert.ToPayloadCommit(ctx.Repo.Repository, ctx.Repo.Commit) commit := convert.ToPayloadCommit(ctx.Repo.Repository, ctx.Repo.Commit)
commitID := ctx.Repo.Commit.ID.String()
if err := webhook_service.PrepareWebhook(hook, ctx.Repo.Repository, webhook.HookEventPush, &api.PushPayload{ if err := webhook_service.PrepareWebhook(hook, ctx.Repo.Repository, webhook.HookEventPush, &api.PushPayload{
Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch, Ref: ref,
Before: ctx.Repo.Commit.ID.String(), Before: commitID,
After: ctx.Repo.Commit.ID.String(), After: commitID,
CompareURL: setting.AppURL + ctx.Repo.Repository.ComposeCompareURL(commitID, commitID),
Commits: []*api.PayloadCommit{commit}, Commits: []*api.PayloadCommit{commit},
HeadCommit: commit, HeadCommit: commit,
Repo: convert.ToRepo(ctx.Repo.Repository, perm.AccessModeNone), Repo: convert.ToRepo(ctx.Repo.Repository, perm.AccessModeNone),

View file

@ -12,7 +12,6 @@ import (
"code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/validation"
) )
// GetNote Get a note corresponding to a single commit from a repository // GetNote Get a note corresponding to a single commit from a repository
@ -47,7 +46,7 @@ func GetNote(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
sha := ctx.Params(":sha") sha := ctx.Params(":sha")
if (validation.GitRefNamePatternInvalid.MatchString(sha) || !validation.CheckGitRefAdditionalRulesValid(sha)) && !git.SHAPattern.MatchString(sha) { if !git.IsValidRefPattern(sha) {
ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha)) ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha))
return return
} }

View file

@ -57,6 +57,10 @@ func GetReleaseAttachment(ctx *context.APIContext) {
attachID := ctx.ParamsInt64(":asset") attachID := ctx.ParamsInt64(":asset")
attach, err := repo_model.GetAttachmentByID(ctx, attachID) attach, err := repo_model.GetAttachmentByID(ctx, attachID)
if err != nil { if err != nil {
if repo_model.IsErrAttachmentNotExist(err) {
ctx.NotFound()
return
}
ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err) ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err)
return return
} }
@ -100,6 +104,10 @@ func ListReleaseAttachments(ctx *context.APIContext) {
releaseID := ctx.ParamsInt64(":id") releaseID := ctx.ParamsInt64(":id")
release, err := models.GetReleaseByID(ctx, releaseID) release, err := models.GetReleaseByID(ctx, releaseID)
if err != nil { if err != nil {
if models.IsErrReleaseNotExist(err) {
ctx.NotFound()
return
}
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err) ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
return return
} }
@ -166,6 +174,10 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
releaseID := ctx.ParamsInt64(":id") releaseID := ctx.ParamsInt64(":id")
release, err := models.GetReleaseByID(ctx, releaseID) release, err := models.GetReleaseByID(ctx, releaseID)
if err != nil { if err != nil {
if models.IsErrReleaseNotExist(err) {
ctx.NotFound()
return
}
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err) ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
return return
} }
@ -244,6 +256,10 @@ func EditReleaseAttachment(ctx *context.APIContext) {
attachID := ctx.ParamsInt64(":asset") attachID := ctx.ParamsInt64(":asset")
attach, err := repo_model.GetAttachmentByID(ctx, attachID) attach, err := repo_model.GetAttachmentByID(ctx, attachID)
if err != nil { if err != nil {
if repo_model.IsErrAttachmentNotExist(err) {
ctx.NotFound()
return
}
ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err) ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err)
return return
} }
@ -302,6 +318,10 @@ func DeleteReleaseAttachment(ctx *context.APIContext) {
attachID := ctx.ParamsInt64(":asset") attachID := ctx.ParamsInt64(":asset")
attach, err := repo_model.GetAttachmentByID(ctx, attachID) attach, err := repo_model.GetAttachmentByID(ctx, attachID)
if err != nil { if err != nil {
if repo_model.IsErrAttachmentNotExist(err) {
ctx.NotFound()
return
}
ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err) ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err)
return return
} }

View file

@ -7,6 +7,7 @@ package user
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
asymkey_model "code.gitea.io/gitea/models/asymkey" asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -177,6 +178,12 @@ func VerifyUserGPGKey(ctx *context.APIContext) {
token := asymkey_model.VerificationToken(ctx.Doer, 1) token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0) lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
form.KeyID = strings.TrimLeft(form.KeyID, "0")
if form.KeyID == "" {
ctx.NotFound()
return
}
_, err := asymkey_model.VerifyGPGKey(ctx.Doer.ID, form.KeyID, token, form.Signature) _, err := asymkey_model.VerifyGPGKey(ctx.Doer.ID, form.KeyID, token, form.Signature)
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) { if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
_, err = asymkey_model.VerifyGPGKey(ctx.Doer.ID, form.KeyID, lastToken, form.Signature) _, err = asymkey_model.VerifyGPGKey(ctx.Doer.ID, form.KeyID, lastToken, form.Signature)

View file

@ -209,7 +209,11 @@ func NewUserPost(ctx *context.Context) {
func prepareUserInfo(ctx *context.Context) *user_model.User { func prepareUserInfo(ctx *context.Context) *user_model.User {
u, err := user_model.GetUserByID(ctx.ParamsInt64(":userid")) u, err := user_model.GetUserByID(ctx.ParamsInt64(":userid"))
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Redirect(setting.AppSubURL + "/admin/users")
} else {
ctx.ServerError("GetUserByID", err) ctx.ServerError("GetUserByID", err)
}
return nil return nil
} }
ctx.Data["User"] = u ctx.Data["User"] = u

View file

@ -339,7 +339,7 @@ func SearchTeam(ctx *context.Context) {
} }
opts := &organization.SearchTeamOptions{ opts := &organization.SearchTeamOptions{
UserID: ctx.Doer.ID, // UserID is not set because the router already requires the doer to be an org admin. Thus, we don't need to restrict to teams that the user belongs in
Keyword: ctx.FormTrim("q"), Keyword: ctx.FormTrim("q"),
OrgID: ctx.Org.Organization.ID, OrgID: ctx.Org.Organization.ID,
IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"), IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"),

View file

@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"time"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -21,7 +20,6 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -389,68 +387,27 @@ func Download(ctx *context.Context) {
if err != nil { if err != nil {
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
ctx.Error(http.StatusBadRequest, err.Error()) ctx.Error(http.StatusBadRequest, err.Error())
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
ctx.Error(http.StatusNotFound, err.Error())
} else { } else {
ctx.ServerError("archiver_service.NewRequest", err) ctx.ServerError("archiver_service.NewRequest", err)
} }
return return
} }
if aReq == nil {
ctx.Error(http.StatusNotFound)
return
}
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID) archiver, err := aReq.Await(ctx)
if err != nil { if err != nil {
ctx.ServerError("models.GetRepoArchiver", err) ctx.ServerError("archiver.Await", err)
return return
} }
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
download(ctx, aReq.GetArchiveName(), archiver) download(ctx, aReq.GetArchiveName(), archiver)
return
}
if err := archiver_service.StartArchive(aReq); err != nil {
ctx.ServerError("archiver_service.StartArchive", err)
return
}
var times int
t := time.NewTicker(time.Second * 1)
defer t.Stop()
for {
select {
case <-graceful.GetManager().HammerContext().Done():
log.Warn("exit archive download because system stop")
return
case <-t.C:
if times > 20 {
ctx.ServerError("wait download timeout", nil)
return
}
times++
archiver, err = repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
if err != nil {
ctx.ServerError("archiver_service.StartArchive", err)
return
}
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
download(ctx, aReq.GetArchiveName(), archiver)
return
}
}
}
} }
func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) { func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) {
downloadName := ctx.Repo.Repository.Name + "-" + archiveName downloadName := ctx.Repo.Repository.Name + "-" + archiveName
rPath, err := archiver.RelativePath() rPath := archiver.RelativePath()
if err != nil {
ctx.ServerError("archiver.RelativePath", err)
return
}
if setting.RepoArchive.ServeDirect { if setting.RepoArchive.ServeDirect {
// If we have a signed url (S3, object storage), redirect to this directly. // If we have a signed url (S3, object storage), redirect to this directly.
u, err := storage.RepoArchives.URL(rPath, downloadName) u, err := storage.RepoArchives.URL(rPath, downloadName)

View file

@ -227,14 +227,17 @@ func SettingsPost(ctx *context.Context) {
form.MirrorPassword, _ = u.User.Password() form.MirrorPassword, _ = u.User.Password()
} }
err = migrations.IsMigrateURLAllowed(u.String(), ctx.Doer) address, err := forms.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
if err == nil {
err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
}
if err != nil { if err != nil {
ctx.Data["Err_MirrorAddress"] = true ctx.Data["Err_MirrorAddress"] = true
handleSettingRemoteAddrError(ctx, err, form) handleSettingRemoteAddrError(ctx, err, form)
return return
} }
if err := mirror_service.UpdateAddress(ctx, ctx.Repo.Mirror, u.String()); err != nil { if err := mirror_service.UpdateAddress(ctx, ctx.Repo.Mirror, address); err != nil {
ctx.ServerError("UpdateAddress", err) ctx.ServerError("UpdateAddress", err)
return return
} }

View file

@ -1271,10 +1271,12 @@ func TestWebhook(ctx *context.Context) {
}, },
} }
commitID := commit.ID.String()
p := &api.PushPayload{ p := &api.PushPayload{
Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch, Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch,
Before: commit.ID.String(), Before: commitID,
After: commit.ID.String(), After: commitID,
CompareURL: setting.AppURL + ctx.Repo.Repository.ComposeCompareURL(commitID, commitID),
Commits: []*api.PayloadCommit{apiCommit}, Commits: []*api.PayloadCommit{apiCommit},
HeadCommit: apiCommit, HeadCommit: apiCommit,
Repo: convert.ToRepo(ctx.Repo.Repository, perm.AccessModeNone), Repo: convert.ToRepo(ctx.Repo.Repository, perm.AccessModeNone),

View file

@ -100,39 +100,6 @@ func Dashboard(ctx *context.Context) {
} }
var err error var err error
var mirrors []*repo_model.Repository
if ctxUser.IsOrganization() {
var env organization.AccessibleReposEnvironment
if ctx.Org.Team != nil {
env = organization.OrgFromUser(ctxUser).AccessibleTeamReposEnv(ctx.Org.Team)
} else {
env, err = organization.AccessibleReposEnv(ctx, organization.OrgFromUser(ctxUser), ctx.Doer.ID)
if err != nil {
ctx.ServerError("AccessibleReposEnv", err)
return
}
}
mirrors, err = env.MirrorRepos()
if err != nil {
ctx.ServerError("env.MirrorRepos", err)
return
}
} else {
mirrors, err = repo_model.GetUserMirrorRepositories(ctxUser.ID)
if err != nil {
ctx.ServerError("GetUserMirrorRepositories", err)
return
}
}
ctx.Data["MaxShowRepoNum"] = setting.UI.User.RepoPagingNum
if err := repo_model.MirrorRepositoryList(mirrors).LoadAttributes(); err != nil {
ctx.ServerError("MirrorRepositoryList.LoadAttributes", err)
return
}
ctx.Data["MirrorCount"] = len(mirrors)
ctx.Data["Mirrors"] = mirrors
ctx.Data["Feeds"], err = models.GetFeeds(ctx, models.GetFeedsOptions{ ctx.Data["Feeds"], err = models.GetFeeds(ctx, models.GetFeedsOptions{
RequestedUser: ctxUser, RequestedUser: ctxUser,
RequestedTeam: ctx.Org.Team, RequestedTeam: ctx.Org.Team,

View file

@ -107,9 +107,24 @@ func NewCodebaseDownloader(ctx context.Context, projectURL *url.URL, project, re
commitMap: make(map[string]string), commitMap: make(map[string]string),
} }
log.Trace("Create Codebase downloader. BaseURL: %s Project: %s RepoName: %s", baseURL, project, repoName)
return downloader return downloader
} }
// String implements Stringer
func (d *CodebaseDownloader) String() string {
return fmt.Sprintf("migration from codebase server %s %s/%s", d.baseURL, d.project, d.repoName)
}
// ColorFormat provides a basic color format for a GogsDownloader
func (d *CodebaseDownloader) ColorFormat(s fmt.State) {
if d == nil {
log.ColorFprintf(s, "<nil: CodebaseDownloader>")
return
}
log.ColorFprintf(s, "migration from codebase server %s %s/%s", d.baseURL, d.project, d.repoName)
}
// FormatCloneURL add authentication into remote URLs // FormatCloneURL add authentication into remote URLs
func (d *CodebaseDownloader) FormatCloneURL(opts base.MigrateOptions, remoteAddr string) (string, error) { func (d *CodebaseDownloader) FormatCloneURL(opts base.MigrateOptions, remoteAddr string) (string, error) {
return opts.CloneAddr, nil return opts.CloneAddr, nil
@ -451,8 +466,8 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq
Value int64 `xml:",chardata"` Value int64 `xml:",chardata"`
Type string `xml:"type,attr"` Type string `xml:"type,attr"`
} `xml:"id"` } `xml:"id"`
SourceRef string `xml:"source-ref"` SourceRef string `xml:"source-ref"` // NOTE: from the documentation these are actually just branches NOT full refs
TargetRef string `xml:"target-ref"` TargetRef string `xml:"target-ref"` // NOTE: from the documentation these are actually just branches NOT full refs
Subject string `xml:"subject"` Subject string `xml:"subject"`
Status string `xml:"status"` Status string `xml:"status"`
UserID struct { UserID struct {
@ -564,6 +579,9 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq
Comments: comments[1:], Comments: comments[1:],
}, },
}) })
// SECURITY: Ensure that the PR is safe
_ = CheckAndEnsureSafePR(pullRequests[len(pullRequests)-1], d.baseURL.String(), d)
} }
return pullRequests, true, nil return pullRequests, true, nil

View file

@ -0,0 +1,82 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"strings"
admin_model "code.gitea.io/gitea/models/admin"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
)
// WarnAndNotice will log the provided message and send a repository notice
func WarnAndNotice(fmtStr string, args ...interface{}) {
log.Warn(fmtStr, args...)
if err := admin_model.CreateRepositoryNotice(fmt.Sprintf(fmtStr, args...)); err != nil {
log.Error("create repository notice failed: ", err)
}
}
func hasBaseURL(toCheck, baseURL string) bool {
if len(baseURL) > 0 && baseURL[len(baseURL)-1] != '/' {
baseURL += "/"
}
return strings.HasPrefix(toCheck, baseURL)
}
// CheckAndEnsureSafePR will check that a given PR is safe to download
func CheckAndEnsureSafePR(pr *base.PullRequest, commonCloneBaseURL string, g base.Downloader) bool {
valid := true
// SECURITY: the patchURL must be checked to have the same baseURL as the current to prevent open redirect
if pr.PatchURL != "" && !hasBaseURL(pr.PatchURL, commonCloneBaseURL) {
// TODO: Should we check that this url has the expected format for a patch url?
WarnAndNotice("PR #%d in %s has invalid PatchURL: %s baseURL: %s", pr.Number, g, pr.PatchURL, commonCloneBaseURL)
pr.PatchURL = ""
valid = false
}
// SECURITY: the headCloneURL must be checked to have the same baseURL as the current to prevent open redirect
if pr.Head.CloneURL != "" && !hasBaseURL(pr.Head.CloneURL, commonCloneBaseURL) {
// TODO: Should we check that this url has the expected format for a patch url?
WarnAndNotice("PR #%d in %s has invalid HeadCloneURL: %s baseURL: %s", pr.Number, g, pr.Head.CloneURL, commonCloneBaseURL)
pr.Head.CloneURL = ""
valid = false
}
// SECURITY: SHAs Must be a SHA
if pr.MergeCommitSHA != "" && !git.IsValidSHAPattern(pr.MergeCommitSHA) {
WarnAndNotice("PR #%d in %s has invalid MergeCommitSHA: %s", pr.Number, g, pr.MergeCommitSHA)
pr.MergeCommitSHA = ""
}
if pr.Head.SHA != "" && !git.IsValidSHAPattern(pr.Head.SHA) {
WarnAndNotice("PR #%d in %s has invalid HeadSHA: %s", pr.Number, g, pr.Head.SHA)
pr.Head.SHA = ""
valid = false
}
if pr.Base.SHA != "" && !git.IsValidSHAPattern(pr.Base.SHA) {
WarnAndNotice("PR #%d in %s has invalid BaseSHA: %s", pr.Number, g, pr.Base.SHA)
pr.Base.SHA = ""
valid = false
}
// SECURITY: Refs must be valid refs or SHAs
if pr.Head.Ref != "" && !git.IsValidRefPattern(pr.Head.Ref) {
WarnAndNotice("PR #%d in %s has invalid HeadRef: %s", pr.Number, g, pr.Head.Ref)
pr.Head.Ref = ""
valid = false
}
if pr.Base.Ref != "" && !git.IsValidRefPattern(pr.Base.Ref) {
WarnAndNotice("PR #%d in %s has invalid BaseRef: %s", pr.Number, g, pr.Base.Ref)
pr.Base.Ref = ""
valid = false
}
pr.EnsuredSafe = true
return valid
}

View file

@ -12,7 +12,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -26,6 +25,8 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"github.com/google/uuid"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -47,7 +48,7 @@ type RepositoryDumper struct {
reviewFiles map[int64]*os.File reviewFiles map[int64]*os.File
gitRepo *git.Repository gitRepo *git.Repository
prHeadCache map[string]struct{} prHeadCache map[string]string
} }
// NewRepositoryDumper creates an gitea Uploader // NewRepositoryDumper creates an gitea Uploader
@ -62,7 +63,7 @@ func NewRepositoryDumper(ctx context.Context, baseDir, repoOwner, repoName strin
baseDir: baseDir, baseDir: baseDir,
repoOwner: repoOwner, repoOwner: repoOwner,
repoName: repoName, repoName: repoName,
prHeadCache: make(map[string]struct{}), prHeadCache: make(map[string]string),
commentFiles: make(map[int64]*os.File), commentFiles: make(map[int64]*os.File),
reviewFiles: make(map[int64]*os.File), reviewFiles: make(map[int64]*os.File),
}, nil }, nil
@ -296,8 +297,10 @@ func (g *RepositoryDumper) CreateReleases(releases ...*base.Release) error {
} }
for _, asset := range release.Assets { for _, asset := range release.Assets {
attachLocalPath := filepath.Join(attachDir, asset.Name) attachLocalPath := filepath.Join(attachDir, asset.Name)
// download attachment
// SECURITY: We cannot check the DownloadURL and DownloadFunc are safe here
// ... we must assume that they are safe and simply download the attachment
// download attachment
err := func(attachPath string) error { err := func(attachPath string) error {
var rc io.ReadCloser var rc io.ReadCloser
var err error var err error
@ -317,7 +320,7 @@ func (g *RepositoryDumper) CreateReleases(releases ...*base.Release) error {
fw, err := os.Create(attachPath) fw, err := os.Create(attachPath)
if err != nil { if err != nil {
return fmt.Errorf("Create: %v", err) return fmt.Errorf("create: %w", err)
} }
defer fw.Close() defer fw.Close()
@ -385,9 +388,18 @@ func (g *RepositoryDumper) createItems(dir string, itemFiles map[int64]*os.File,
} }
for number, items := range itemsMap { for number, items := range itemsMap {
var err error if err := g.encodeItems(number, items, dir, itemFiles); err != nil {
return err
}
}
return nil
}
func (g *RepositoryDumper) encodeItems(number int64, items []interface{}, dir string, itemFiles map[int64]*os.File) error {
itemFile := itemFiles[number] itemFile := itemFiles[number]
if itemFile == nil { if itemFile == nil {
var err error
itemFile, err = os.Create(filepath.Join(dir, fmt.Sprintf("%d.yml", number))) itemFile, err = os.Create(filepath.Join(dir, fmt.Sprintf("%d.yml", number)))
if err != nil { if err != nil {
return err return err
@ -395,17 +407,10 @@ func (g *RepositoryDumper) createItems(dir string, itemFiles map[int64]*os.File,
itemFiles[number] = itemFile itemFiles[number] = itemFile
} }
bs, err := yaml.Marshal(items) encoder := yaml.NewEncoder(itemFile)
if err != nil { defer encoder.Close()
return err
}
if _, err := itemFile.Write(bs); err != nil { return encoder.Encode(items)
return err
}
}
return nil
} }
// CreateComments creates comments of issues // CreateComments creates comments of issues
@ -418,16 +423,30 @@ func (g *RepositoryDumper) CreateComments(comments ...*base.Comment) error {
return g.createItems(g.commentDir(), g.commentFiles, commentsMap) return g.createItems(g.commentDir(), g.commentFiles, commentsMap)
} }
// CreatePullRequests creates pull requests func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error {
func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error { // SECURITY: this pr must have been ensured safe
for _, pr := range prs { if !pr.EnsuredSafe {
// download patch file log.Error("PR #%d in %s/%s has not been checked for safety ... We will ignore this.", pr.Number, g.repoOwner, g.repoName)
return fmt.Errorf("unsafe PR #%d", pr.Number)
}
// First we download the patch file
err := func() error { err := func() error {
// if the patchURL is empty there is nothing to download
if pr.PatchURL == "" {
return nil
}
// SECURITY: We will assume that the pr.PatchURL has been checked
// pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe
u, err := g.setURLToken(pr.PatchURL) u, err := g.setURLToken(pr.PatchURL)
if err != nil { if err != nil {
return err return err
} }
resp, err := http.Get(u)
// SECURITY: We will assume that the pr.PatchURL has been checked
// pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe
resp, err := http.Get(u) // TODO: This probably needs to use the downloader as there may be rate limiting issues here
if err != nil { if err != nil {
return err return err
} }
@ -442,6 +461,8 @@ func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error {
return err return err
} }
defer f.Close() defer f.Close()
// TODO: Should there be limits on the size of this file?
if _, err = io.Copy(f, resp.Body); err != nil { if _, err = io.Copy(f, resp.Body); err != nil {
return err return err
} }
@ -450,70 +471,127 @@ func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error {
return nil return nil
}() }()
if err != nil { if err != nil {
log.Error("PR #%d in %s/%s unable to download patch: %v", pr.Number, g.repoOwner, g.repoName, err)
return err return err
} }
// set head information isFork := pr.IsForkPullRequest()
pullHead := filepath.Join(g.gitPath(), "refs", "pull", fmt.Sprintf("%d", pr.Number))
if err := os.MkdirAll(pullHead, os.ModePerm); err != nil { // Even if it's a forked repo PR, we have to change head info as the same as the base info
return err oldHeadOwnerName := pr.Head.OwnerName
} pr.Head.OwnerName, pr.Head.RepoName = pr.Base.OwnerName, pr.Base.RepoName
p, err := os.Create(filepath.Join(pullHead, "head"))
if err != nil { if !isFork || pr.State == "closed" {
return err return nil
}
_, err = p.WriteString(pr.Head.SHA)
p.Close()
if err != nil {
return err
} }
if pr.IsForkPullRequest() && pr.State != "closed" { // OK we want to fetch the current head as a branch from its CloneURL
if pr.Head.OwnerName != "" {
remote := pr.Head.OwnerName // 1. Is there a head clone URL available?
_, ok := g.prHeadCache[remote] // 2. Is there a head ref available?
if pr.Head.CloneURL == "" || pr.Head.Ref == "" {
// Set head information if pr.Head.SHA is available
if pr.Head.SHA != "" {
_, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref", pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
if err != nil {
log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err)
}
}
return nil
}
// 3. We need to create a remote for this clone url
// ... maybe we already have a name for this remote
remote, ok := g.prHeadCache[pr.Head.CloneURL+":"]
if !ok { if !ok {
// git remote add // ... let's try ownername as a reasonable name
// TODO: how to handle private CloneURL? remote = oldHeadOwnerName
if !git.IsValidRefPattern(remote) {
// ... let's try something less nice
remote = "head-pr-" + strconv.FormatInt(pr.Number, 10)
}
// ... now add the remote
err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true) err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
if err != nil { if err != nil {
log.Error("AddRemote failed: %s", err) log.Error("PR #%d in %s/%s AddRemote[%s] failed: %v", pr.Number, g.repoOwner, g.repoName, remote, err)
} else { } else {
g.prHeadCache[remote] = struct{}{} g.prHeadCache[pr.Head.CloneURL+":"] = remote
ok = true ok = true
} }
} }
if !ok {
// Set head information if pr.Head.SHA is available
if pr.Head.SHA != "" {
_, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref", pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
if err != nil {
log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err)
}
}
if ok { return nil
_, _, err = git.NewCommand(g.ctx, "fetch", remote, pr.Head.Ref).RunStdString(&git.RunOpts{Dir: g.gitPath()}) }
// 4. Check if we already have this ref?
localRef, ok := g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref]
if !ok {
// ... We would normally name this migrated branch as <OwnerName>/<HeadRef> but we need to ensure that is safe
localRef = git.SanitizeRefPattern(oldHeadOwnerName + "/" + pr.Head.Ref)
// ... Now we must assert that this does not exist
if g.gitRepo.IsBranchExist(localRef) {
localRef = "head-pr-" + strconv.FormatInt(pr.Number, 10) + "/" + localRef
i := 0
for g.gitRepo.IsBranchExist(localRef) {
if i > 5 {
// ... We tried, we really tried but this is just a seriously unfriendly repo
return fmt.Errorf("unable to create unique local reference from %s", pr.Head.Ref)
}
// OK just try some uuids!
localRef = git.SanitizeRefPattern("head-pr-" + strconv.FormatInt(pr.Number, 10) + uuid.New().String())
i++
}
}
fetchArg := pr.Head.Ref + ":" + git.BranchPrefix + localRef
if strings.HasPrefix(fetchArg, "-") {
fetchArg = git.BranchPrefix + fetchArg
}
_, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags", "--", remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.gitPath()})
if err != nil { if err != nil {
log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err) log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err)
// We need to continue here so that the Head.Ref is reset and we attempt to set the gitref for the PR
// (This last step will likely fail but we should try to do as much as we can.)
} else { } else {
// a new branch name with <original_owner_name/original_branchname> will be created to as new head branch // Cache the localRef as the Head.Ref - if we've failed we can always try again.
ref := path.Join(pr.Head.OwnerName, pr.Head.Ref) g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref] = localRef
headBranch := filepath.Join(g.gitPath(), "refs", "heads", ref)
if err := os.MkdirAll(filepath.Dir(headBranch), os.ModePerm); err != nil {
return err
} }
b, err := os.Create(headBranch)
if err != nil {
return err
}
_, err = b.WriteString(pr.Head.SHA)
b.Close()
if err != nil {
return err
}
pr.Head.Ref = ref
}
}
}
}
// whatever it's a forked repo PR, we have to change head info as the same as the base info
pr.Head.OwnerName = pr.Base.OwnerName
pr.Head.RepoName = pr.Base.RepoName
} }
// Set the pr.Head.Ref to the localRef
pr.Head.Ref = localRef
// 5. Now if pr.Head.SHA == "" we should recover this to the head of this branch
if pr.Head.SHA == "" {
headSha, err := g.gitRepo.GetBranchCommitID(localRef)
if err != nil {
log.Error("unable to get head SHA of local head for PR #%d from %s in %s/%s. Error: %v", pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err)
return nil
}
pr.Head.SHA = headSha
}
if pr.Head.SHA != "" {
_, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref", pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
if err != nil {
log.Error("unable to set %s as the local head for PR #%d from %s in %s/%s. Error: %v", pr.Head.SHA, pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err)
}
}
return nil
}
// CreatePullRequests creates pull requests
func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error {
var err error var err error
if g.pullrequestFile == nil { if g.pullrequestFile == nil {
if err := os.MkdirAll(g.baseDir, os.ModePerm); err != nil { if err := os.MkdirAll(g.baseDir, os.ModePerm); err != nil {
@ -525,16 +603,22 @@ func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error {
} }
} }
bs, err := yaml.Marshal(prs) encoder := yaml.NewEncoder(g.pullrequestFile)
if err != nil { defer encoder.Close()
return err
}
if _, err := g.pullrequestFile.Write(bs); err != nil { count := 0
return err for i := 0; i < len(prs); i++ {
pr := prs[i]
if err := g.handlePullRequest(pr); err != nil {
log.Error("PR #%d in %s/%s failed - skipping", pr.Number, g.repoOwner, g.repoName, err)
continue
} }
prs[count] = pr
count++
}
prs = prs[:count]
return nil return encoder.Encode(prs)
} }
// CreateReviews create pull request reviews // CreateReviews create pull request reviews
@ -560,6 +644,10 @@ func (g *RepositoryDumper) Finish() error {
// DumpRepository dump repository according MigrateOptions to a local directory // DumpRepository dump repository according MigrateOptions to a local directory
func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.MigrateOptions) error { func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.MigrateOptions) error {
doer, err := user_model.GetAdminUser()
if err != nil {
return err
}
downloader, err := newDownloader(ctx, ownerName, opts) downloader, err := newDownloader(ctx, ownerName, opts)
if err != nil { if err != nil {
return err return err
@ -569,7 +657,7 @@ func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.Mi
return err return err
} }
if err := migrateRepository(downloader, uploader, opts, nil); err != nil { if err := migrateRepository(doer, downloader, uploader, opts, nil); err != nil {
if err1 := uploader.Rollback(); err1 != nil { if err1 := uploader.Rollback(); err1 != nil {
log.Error("rollback failed: %v", err1) log.Error("rollback failed: %v", err1)
} }
@ -641,7 +729,7 @@ func RestoreRepository(ctx context.Context, baseDir, ownerName, repoName string,
return err return err
} }
if err = migrateRepository(downloader, uploader, migrateOpts, nil); err != nil { if err = migrateRepository(doer, downloader, uploader, migrateOpts, nil); err != nil {
if err1 := uploader.Rollback(); err1 != nil { if err1 := uploader.Rollback(); err1 != nil {
log.Error("rollback failed: %v", err1) log.Error("rollback failed: %v", err1)
} }

View file

@ -6,9 +6,11 @@ package migrations
import ( import (
"context" "context"
"fmt"
"net/url" "net/url"
"strings" "strings"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration" base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
) )
@ -37,6 +39,7 @@ func (f *GitBucketDownloaderFactory) New(ctx context.Context, opts base.MigrateO
oldOwner := fields[1] oldOwner := fields[1]
oldName := strings.TrimSuffix(fields[2], ".git") oldName := strings.TrimSuffix(fields[2], ".git")
log.Trace("Create GitBucket downloader. BaseURL: %s RepoOwner: %s RepoName: %s", baseURL, oldOwner, oldName)
return NewGitBucketDownloader(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil return NewGitBucketDownloader(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil
} }
@ -51,6 +54,20 @@ type GitBucketDownloader struct {
*GithubDownloaderV3 *GithubDownloaderV3
} }
// String implements Stringer
func (g *GitBucketDownloader) String() string {
return fmt.Sprintf("migration from gitbucket server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
}
// ColorFormat provides a basic color format for a GitBucketDownloader
func (g *GitBucketDownloader) ColorFormat(s fmt.State) {
if g == nil {
log.ColorFprintf(s, "<nil: GitBucketDownloader>")
return
}
log.ColorFprintf(s, "migration from gitbucket server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
}
// NewGitBucketDownloader creates a GitBucket downloader // NewGitBucketDownloader creates a GitBucket downloader
func NewGitBucketDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GitBucketDownloader { func NewGitBucketDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GitBucketDownloader {
githubDownloader := NewGithubDownloaderV3(ctx, baseURL, userName, password, token, repoOwner, repoName) githubDownloader := NewGithubDownloaderV3(ctx, baseURL, userName, password, token, repoOwner, repoName)

View file

@ -14,7 +14,6 @@ import (
"strings" "strings"
"time" "time"
admin_model "code.gitea.io/gitea/models/admin"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration" base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
@ -71,6 +70,7 @@ type GiteaDownloader struct {
base.NullDownloader base.NullDownloader
ctx context.Context ctx context.Context
client *gitea_sdk.Client client *gitea_sdk.Client
baseURL string
repoOwner string repoOwner string
repoName string repoName string
pagination bool pagination bool
@ -116,6 +116,7 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo
return &GiteaDownloader{ return &GiteaDownloader{
ctx: ctx, ctx: ctx,
client: giteaClient, client: giteaClient,
baseURL: baseURL,
repoOwner: path[0], repoOwner: path[0],
repoName: path[1], repoName: path[1],
pagination: paginationSupport, pagination: paginationSupport,
@ -128,6 +129,20 @@ func (g *GiteaDownloader) SetContext(ctx context.Context) {
g.ctx = ctx g.ctx = ctx
} }
// String implements Stringer
func (g *GiteaDownloader) String() string {
return fmt.Sprintf("migration from gitea server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
}
// ColorFormat provides a basic color format for a GiteaDownloader
func (g *GiteaDownloader) ColorFormat(s fmt.State) {
if g == nil {
log.ColorFprintf(s, "<nil: GiteaDownloader>")
return
}
log.ColorFprintf(s, "migration from gitea server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
}
// GetRepoInfo returns a repository information // GetRepoInfo returns a repository information
func (g *GiteaDownloader) GetRepoInfo() (*base.Repository, error) { func (g *GiteaDownloader) GetRepoInfo() (*base.Repository, error) {
if g == nil { if g == nil {
@ -283,6 +298,12 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !hasBaseURL(asset.DownloadURL, g.baseURL) {
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, asset.DownloadURL)
return io.NopCloser(strings.NewReader(asset.DownloadURL)), nil
}
// FIXME: for a private download? // FIXME: for a private download?
req, err := http.NewRequest("GET", asset.DownloadURL, nil) req, err := http.NewRequest("GET", asset.DownloadURL, nil)
if err != nil { if err != nil {
@ -402,11 +423,7 @@ func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, err
reactions, err := g.getIssueReactions(issue.Index) reactions, err := g.getIssueReactions(issue.Index)
if err != nil { if err != nil {
log.Warn("Unable to load reactions during migrating issue #%d to %s/%s. Error: %v", issue.Index, g.repoOwner, g.repoName, err) WarnAndNotice("Unable to load reactions during migrating issue #%d in %s. Error: %v", issue.Index, g, err)
if err2 := admin_model.CreateRepositoryNotice(
fmt.Sprintf("Unable to load reactions during migrating issue #%d to %s/%s. Error: %v", issue.Index, g.repoOwner, g.repoName, err)); err2 != nil {
log.Error("create repository notice failed: ", err2)
}
} }
var assignees []string var assignees []string
@ -464,11 +481,7 @@ func (g *GiteaDownloader) GetComments(commentable base.Commentable) ([]*base.Com
for _, comment := range comments { for _, comment := range comments {
reactions, err := g.getCommentReactions(comment.ID) reactions, err := g.getCommentReactions(comment.ID)
if err != nil { if err != nil {
log.Warn("Unable to load comment reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", commentable.GetForeignIndex(), comment.ID, g.repoOwner, g.repoName, err) WarnAndNotice("Unable to load comment reactions during migrating issue #%d for comment %d in %s. Error: %v", commentable.GetForeignIndex(), comment.ID, g, err)
if err2 := admin_model.CreateRepositoryNotice(
fmt.Sprintf("Unable to load reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", commentable.GetForeignIndex(), comment.ID, g.repoOwner, g.repoName, err)); err2 != nil {
log.Error("create repository notice failed: ", err2)
}
} }
allComments = append(allComments, &base.Comment{ allComments = append(allComments, &base.Comment{
@ -543,11 +556,7 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques
reactions, err := g.getIssueReactions(pr.Index) reactions, err := g.getIssueReactions(pr.Index)
if err != nil { if err != nil {
log.Warn("Unable to load reactions during migrating pull #%d to %s/%s. Error: %v", pr.Index, g.repoOwner, g.repoName, err) WarnAndNotice("Unable to load reactions during migrating pull #%d in %s. Error: %v", pr.Index, g, err)
if err2 := admin_model.CreateRepositoryNotice(
fmt.Sprintf("Unable to load reactions during migrating pull #%d to %s/%s. Error: %v", pr.Index, g.repoOwner, g.repoName, err)); err2 != nil {
log.Error("create repository notice failed: ", err2)
}
} }
var assignees []string var assignees []string
@ -604,6 +613,8 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques
}, },
ForeignIndex: pr.Index, ForeignIndex: pr.Index,
}) })
// SECURITY: Ensure that the PR is safe
_ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g)
} }
isEnd := len(prs) < perPage isEnd := len(prs) < perPage

View file

@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -32,7 +33,7 @@ import (
"code.gitea.io/gitea/modules/uri" "code.gitea.io/gitea/modules/uri"
"code.gitea.io/gitea/services/pull" "code.gitea.io/gitea/services/pull"
gouuid "github.com/google/uuid" "github.com/google/uuid"
) )
var _ base.Uploader = &GiteaLocalUploader{} var _ base.Uploader = &GiteaLocalUploader{}
@ -48,7 +49,7 @@ type GiteaLocalUploader struct {
milestones map[string]int64 milestones map[string]int64
issues map[int64]*issues_model.Issue issues map[int64]*issues_model.Issue
gitRepo *git.Repository gitRepo *git.Repository
prHeadCache map[string]struct{} prHeadCache map[string]string
sameApp bool sameApp bool
userMap map[int64]int64 // external user id mapping to user id userMap map[int64]int64 // external user id mapping to user id
prCache map[int64]*issues_model.PullRequest prCache map[int64]*issues_model.PullRequest
@ -65,7 +66,7 @@ func NewGiteaLocalUploader(ctx context.Context, doer *user_model.User, repoOwner
labels: make(map[string]*issues_model.Label), labels: make(map[string]*issues_model.Label),
milestones: make(map[string]int64), milestones: make(map[string]int64),
issues: make(map[int64]*issues_model.Issue), issues: make(map[int64]*issues_model.Issue),
prHeadCache: make(map[string]struct{}), prHeadCache: make(map[string]string),
userMap: make(map[int64]int64), userMap: make(map[int64]int64),
prCache: make(map[int64]*issues_model.PullRequest), prCache: make(map[int64]*issues_model.PullRequest),
} }
@ -125,7 +126,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
Mirror: repo.IsMirror, Mirror: repo.IsMirror,
LFS: opts.LFS, LFS: opts.LFS,
LFSEndpoint: opts.LFSEndpoint, LFSEndpoint: opts.LFSEndpoint,
CloneAddr: repo.CloneURL, CloneAddr: repo.CloneURL, // SECURITY: we will assume that this has already been checked
Private: repo.IsPrivate, Private: repo.IsPrivate,
Wiki: opts.Wiki, Wiki: opts.Wiki,
Releases: opts.Releases, // if didn't get releases, then sync them from tags Releases: opts.Releases, // if didn't get releases, then sync them from tags
@ -150,13 +151,15 @@ func (g *GiteaLocalUploader) Close() {
// CreateTopics creates topics // CreateTopics creates topics
func (g *GiteaLocalUploader) CreateTopics(topics ...string) error { func (g *GiteaLocalUploader) CreateTopics(topics ...string) error {
// ignore topics to long for the db // Ignore topics too long for the db
c := 0 c := 0
for i := range topics { for _, topic := range topics {
if len(topics[i]) <= 50 { if len(topic) > 50 {
topics[c] = topics[i] continue
c++
} }
topics[c] = topic
c++
} }
topics = topics[:c] topics = topics[:c]
return repo_model.SaveTopics(g.repo.ID, topics...) return repo_model.SaveTopics(g.repo.ID, topics...)
@ -217,11 +220,17 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err
func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error {
lbs := make([]*issues_model.Label, 0, len(labels)) lbs := make([]*issues_model.Label, 0, len(labels))
for _, label := range labels { for _, label := range labels {
// We must validate color here:
if !issues_model.LabelColorPattern.MatchString("#" + label.Color) {
log.Warn("Invalid label color: #%s for label: %s in migration to %s/%s", label.Color, label.Name, g.repoOwner, g.repoName)
label.Color = "ffffff"
}
lbs = append(lbs, &issues_model.Label{ lbs = append(lbs, &issues_model.Label{
RepoID: g.repo.ID, RepoID: g.repo.ID,
Name: label.Name, Name: label.Name,
Description: label.Description, Description: label.Description,
Color: fmt.Sprintf("#%s", label.Color), Color: "#" + label.Color,
}) })
} }
@ -247,6 +256,16 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
} }
} }
// SECURITY: The TagName must be a valid git ref
if release.TagName != "" && !git.IsValidRefPattern(release.TagName) {
release.TagName = ""
}
// SECURITY: The TargetCommitish must be a valid git ref
if release.TargetCommitish != "" && !git.IsValidRefPattern(release.TargetCommitish) {
release.TargetCommitish = ""
}
rel := models.Release{ rel := models.Release{
RepoID: g.repo.ID, RepoID: g.repo.ID,
TagName: release.TagName, TagName: release.TagName,
@ -288,14 +307,15 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
} }
} }
attach := repo_model.Attachment{ attach := repo_model.Attachment{
UUID: gouuid.New().String(), UUID: uuid.New().String(),
Name: asset.Name, Name: asset.Name,
DownloadCount: int64(*asset.DownloadCount), DownloadCount: int64(*asset.DownloadCount),
Size: int64(*asset.Size), Size: int64(*asset.Size),
CreatedUnix: timeutil.TimeStamp(asset.Created.Unix()), CreatedUnix: timeutil.TimeStamp(asset.Created.Unix()),
} }
// download attachment // SECURITY: We cannot check the DownloadURL and DownloadFunc are safe here
// ... we must assume that they are safe and simply download the attachment
err := func() error { err := func() error {
// asset.DownloadURL maybe a local file // asset.DownloadURL maybe a local file
var rc io.ReadCloser var rc io.ReadCloser
@ -365,6 +385,12 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
} }
} }
// SECURITY: issue.Ref needs to be a valid reference
if !git.IsValidRefPattern(issue.Ref) {
log.Warn("Invalid issue.Ref[%s] in issue #%d in %s/%s", issue.Ref, issue.Number, g.repoOwner, g.repoName)
issue.Ref = ""
}
is := issues_model.Issue{ is := issues_model.Issue{
RepoID: g.repo.ID, RepoID: g.repo.ID,
Repo: g.repo, Repo: g.repo,
@ -496,102 +522,151 @@ func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error
} }
func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head string, err error) { func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head string, err error) {
// download patch file // SECURITY: this pr must have been must have been ensured safe
if !pr.EnsuredSafe {
log.Error("PR #%d in %s/%s has not been checked for safety.", pr.Number, g.repoOwner, g.repoName)
return "", fmt.Errorf("the PR[%d] was not checked for safety", pr.Number)
}
// Anonymous function to download the patch file (allows us to use defer)
err = func() error { err = func() error {
// if the patchURL is empty there is nothing to download
if pr.PatchURL == "" { if pr.PatchURL == "" {
return nil return nil
} }
// pr.PatchURL maybe a local file
ret, err := uri.Open(pr.PatchURL) // SECURITY: We will assume that the pr.PatchURL has been checked
// pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe
ret, err := uri.Open(pr.PatchURL) // TODO: This probably needs to use the downloader as there may be rate limiting issues here
if err != nil { if err != nil {
return err return err
} }
defer ret.Close() defer ret.Close()
pullDir := filepath.Join(g.repo.RepoPath(), "pulls") pullDir := filepath.Join(g.repo.RepoPath(), "pulls")
if err = os.MkdirAll(pullDir, os.ModePerm); err != nil { if err = os.MkdirAll(pullDir, os.ModePerm); err != nil {
return err return err
} }
f, err := os.Create(filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number))) f, err := os.Create(filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number)))
if err != nil { if err != nil {
return err return err
} }
defer f.Close() defer f.Close()
// TODO: Should there be limits on the size of this file?
_, err = io.Copy(f, ret) _, err = io.Copy(f, ret)
return err return err
}() }()
if err != nil { if err != nil {
return "", err return "", err
} }
// set head information
pullHead := filepath.Join(g.repo.RepoPath(), "refs", "pull", fmt.Sprintf("%d", pr.Number))
if err := os.MkdirAll(pullHead, os.ModePerm); err != nil {
return "", err
}
p, err := os.Create(filepath.Join(pullHead, "head"))
if err != nil {
return "", err
}
_, err = p.WriteString(pr.Head.SHA)
p.Close()
if err != nil {
return "", err
}
head = "unknown repository" head = "unknown repository"
if pr.IsForkPullRequest() && pr.State != "closed" { if pr.IsForkPullRequest() && pr.State != "closed" {
if pr.Head.OwnerName != "" { // OK we want to fetch the current head as a branch from its CloneURL
remote := pr.Head.OwnerName
_, ok := g.prHeadCache[remote] // 1. Is there a head clone URL available?
// 2. Is there a head ref available?
if pr.Head.CloneURL == "" || pr.Head.Ref == "" {
return head, nil
}
// 3. We need to create a remote for this clone url
// ... maybe we already have a name for this remote
remote, ok := g.prHeadCache[pr.Head.CloneURL+":"]
if !ok { if !ok {
// git remote add // ... let's try ownername as a reasonable name
remote = pr.Head.OwnerName
if !git.IsValidRefPattern(remote) {
// ... let's try something less nice
remote = "head-pr-" + strconv.FormatInt(pr.Number, 10)
}
// ... now add the remote
err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true) err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
if err != nil { if err != nil {
log.Error("AddRemote failed: %s", err) log.Error("PR #%d in %s/%s AddRemote[%s] failed: %v", pr.Number, g.repoOwner, g.repoName, remote, err)
} else { } else {
g.prHeadCache[remote] = struct{}{} g.prHeadCache[pr.Head.CloneURL+":"] = remote
ok = true ok = true
} }
} }
if !ok {
return head, nil
}
if ok { // 4. Check if we already have this ref?
_, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags", "--", remote, pr.Head.Ref).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) localRef, ok := g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref]
if !ok {
// ... We would normally name this migrated branch as <OwnerName>/<HeadRef> but we need to ensure that is safe
localRef = git.SanitizeRefPattern(pr.Head.OwnerName + "/" + pr.Head.Ref)
// ... Now we must assert that this does not exist
if g.gitRepo.IsBranchExist(localRef) {
localRef = "head-pr-" + strconv.FormatInt(pr.Number, 10) + "/" + localRef
i := 0
for g.gitRepo.IsBranchExist(localRef) {
if i > 5 {
// ... We tried, we really tried but this is just a seriously unfriendly repo
return head, nil
}
// OK just try some uuids!
localRef = git.SanitizeRefPattern("head-pr-" + strconv.FormatInt(pr.Number, 10) + uuid.New().String())
i++
}
}
fetchArg := pr.Head.Ref + ":" + git.BranchPrefix + localRef
if strings.HasPrefix(fetchArg, "-") {
fetchArg = git.BranchPrefix + fetchArg
}
_, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags", "--", remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
if err != nil { if err != nil {
log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err) log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err)
} else { return head, nil
headBranch := filepath.Join(g.repo.RepoPath(), "refs", "heads", pr.Head.OwnerName, pr.Head.Ref)
if err := os.MkdirAll(filepath.Dir(headBranch), os.ModePerm); err != nil {
return "", err
} }
b, err := os.Create(headBranch) g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref] = localRef
head = localRef
}
// 5. Now if pr.Head.SHA == "" we should recover this to the head of this branch
if pr.Head.SHA == "" {
headSha, err := g.gitRepo.GetBranchCommitID(localRef)
if err != nil {
log.Error("unable to get head SHA of local head for PR #%d from %s in %s/%s. Error: %v", pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err)
return head, nil
}
pr.Head.SHA = headSha
}
_, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref", pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
if err != nil { if err != nil {
return "", err return "", err
} }
_, err = b.WriteString(pr.Head.SHA)
b.Close() return head, nil
if err != nil {
return "", err
} }
head = pr.Head.OwnerName + "/" + pr.Head.Ref
} if pr.Head.Ref != "" {
}
}
} else {
head = pr.Head.Ref head = pr.Head.Ref
}
// Ensure the closed PR SHA still points to an existing ref // Ensure the closed PR SHA still points to an existing ref
if pr.Head.SHA == "" {
// The SHA is empty
log.Warn("Empty reference, no pull head for PR #%d in %s/%s", pr.Number, g.repoOwner, g.repoName)
} else {
_, _, err = git.NewCommand(g.ctx, "rev-list", "--quiet", "-1", pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) _, _, err = git.NewCommand(g.ctx, "rev-list", "--quiet", "-1", pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
if err != nil { if err != nil {
if pr.Head.SHA != "" {
// Git update-ref remove bad references with a relative path // Git update-ref remove bad references with a relative path
log.Warn("Deprecated local head, removing : %v", pr.Head.SHA) log.Warn("Deprecated local head %s for PR #%d in %s/%s, removing %s", pr.Head.SHA, pr.Number, g.repoOwner, g.repoName, pr.GetGitRefName())
err = g.gitRepo.RemoveReference(pr.GetGitRefName())
} else { } else {
// The SHA is empty, remove the head file // set head information
log.Warn("Empty reference, removing : %v", pullHead) _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref", pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
err = os.Remove(filepath.Join(pullHead, "head"))
}
if err != nil { if err != nil {
log.Error("Cannot remove local head ref, %v", err) log.Error("unable to set %s as the local head for PR #%d from %s in %s/%s. Error: %v", pr.Head.SHA, pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err)
} }
} }
} }
@ -615,6 +690,20 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model
return nil, fmt.Errorf("updateGitForPullRequest: %w", err) return nil, fmt.Errorf("updateGitForPullRequest: %w", err)
} }
// Now we may need to fix the mergebase
if pr.Base.SHA == "" {
if pr.Base.Ref != "" && pr.Head.SHA != "" {
// A PR against a tag base does not make sense - therefore pr.Base.Ref must be a branch
// TODO: should we be checking for the refs/heads/ prefix on the pr.Base.Ref? (i.e. are these actually branches or refs)
pr.Base.SHA, _, err = g.gitRepo.GetMergeBase("", git.BranchPrefix+pr.Base.Ref, pr.Head.SHA)
if err != nil {
log.Error("Cannot determine the merge base for PR #%d in %s/%s. Error: %v", pr.Number, g.repoOwner, g.repoName, err)
}
} else {
log.Error("Cannot determine the merge base for PR #%d in %s/%s. Not enough information", pr.Number, g.repoOwner, g.repoName)
}
}
if pr.Created.IsZero() { if pr.Created.IsZero() {
if pr.Closed != nil { if pr.Closed != nil {
pr.Created = *pr.Closed pr.Created = *pr.Closed
@ -728,6 +817,8 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
return err return err
} }
cms = append(cms, &cm)
// get pr // get pr
pr, ok := g.prCache[issue.ID] pr, ok := g.prCache[issue.ID]
if !ok { if !ok {
@ -738,6 +829,17 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
} }
g.prCache[issue.ID] = pr g.prCache[issue.ID] = pr
} }
if pr.MergeBase == "" {
// No mergebase -> no basis for any patches
log.Warn("PR #%d in %s/%s: does not have a merge base, all review comments will be ignored", pr.Index, g.repoOwner, g.repoName)
continue
}
headCommitID, err := g.gitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil {
log.Warn("PR #%d GetRefCommitID[%s] in %s/%s: %v, all review comments will be ignored", pr.Index, pr.GetGitRefName(), g.repoOwner, g.repoName, err)
continue
}
for _, comment := range review.Comments { for _, comment := range review.Comments {
line := comment.Line line := comment.Line
@ -746,11 +848,9 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
} else { } else {
_, _, line, _ = git.ParseDiffHunkString(comment.DiffHunk) _, _, line, _ = git.ParseDiffHunkString(comment.DiffHunk)
} }
headCommitID, err := g.gitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil { // SECURITY: The TreePath must be cleaned!
log.Warn("GetRefCommitID[%s]: %v, the review comment will be ignored", pr.GetGitRefName(), err) comment.TreePath = path.Clean("/" + comment.TreePath)[1:]
continue
}
var patch string var patch string
reader, writer := io.Pipe() reader, writer := io.Pipe()
@ -775,6 +875,11 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
comment.UpdatedAt = comment.CreatedAt comment.UpdatedAt = comment.CreatedAt
} }
if !git.IsValidSHAPattern(comment.CommitID) {
log.Warn("Invalid comment CommitID[%s] on comment[%d] in PR #%d of %s/%s replaced with %s", comment.CommitID, pr.Index, g.repoOwner, g.repoName, headCommitID)
comment.CommitID = headCommitID
}
c := issues_model.Comment{ c := issues_model.Comment{
Type: issues_model.CommentTypeCode, Type: issues_model.CommentTypeCode,
IssueID: issue.ID, IssueID: issue.ID,
@ -793,8 +898,6 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
cm.Comments = append(cm.Comments, &c) cm.Comments = append(cm.Comments, &c)
} }
cms = append(cms, &cm)
} }
return issues_model.InsertReviews(cms) return issues_model.InsertReviews(cms)

View file

@ -46,7 +46,7 @@ func TestGiteaUploadRepo(t *testing.T) {
uploader = NewGiteaLocalUploader(graceful.GetManager().HammerContext(), user, user.Name, repoName) uploader = NewGiteaLocalUploader(graceful.GetManager().HammerContext(), user, user.Name, repoName)
) )
err := migrateRepository(downloader, uploader, base.MigrateOptions{ err := migrateRepository(user, downloader, uploader, base.MigrateOptions{
CloneAddr: "https://github.com/go-xorm/builder", CloneAddr: "https://github.com/go-xorm/builder",
RepoName: repoName, RepoName: repoName,
AuthUsername: "", AuthUsername: "",
@ -391,7 +391,7 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
}, },
}, },
assertContent: func(t *testing.T, content string) { assertContent: func(t *testing.T, content string) {
assert.Contains(t, content, "AddRemote failed") assert.Contains(t, content, "AddRemote")
}, },
}, },
{ {
@ -440,7 +440,7 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
}, },
}, },
assertContent: func(t *testing.T, content string) { assertContent: func(t *testing.T, content string) {
assert.Contains(t, content, "Empty reference, removing") assert.Contains(t, content, "Empty reference")
assert.NotContains(t, content, "Cannot remove local head") assert.NotContains(t, content, "Cannot remove local head")
}, },
}, },
@ -468,7 +468,6 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
}, },
assertContent: func(t *testing.T, content string) { assertContent: func(t *testing.T, content string) {
assert.Contains(t, content, "Deprecated local head") assert.Contains(t, content, "Deprecated local head")
assert.Contains(t, content, "Cannot remove local head")
}, },
}, },
{ {
@ -505,6 +504,8 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
logger.SetLogger("buffer", "buffer", "{}") logger.SetLogger("buffer", "buffer", "{}")
defer logger.DelLogger("buffer") defer logger.DelLogger("buffer")
testCase.pr.EnsuredSafe = true
head, err := uploader.updateGitForPullRequest(&testCase.pr) head, err := uploader.updateGitForPullRequest(&testCase.pr)
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, testCase.head, head) assert.EqualValues(t, testCase.head, head)

View file

@ -51,7 +51,7 @@ func (f *GithubDownloaderV3Factory) New(ctx context.Context, opts base.MigrateOp
oldOwner := fields[1] oldOwner := fields[1]
oldName := strings.TrimSuffix(fields[2], ".git") oldName := strings.TrimSuffix(fields[2], ".git")
log.Trace("Create github downloader: %s/%s", oldOwner, oldName) log.Trace("Create github downloader BaseURL: %s %s/%s", baseURL, oldOwner, oldName)
return NewGithubDownloaderV3(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil return NewGithubDownloaderV3(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil
} }
@ -67,6 +67,7 @@ type GithubDownloaderV3 struct {
base.NullDownloader base.NullDownloader
ctx context.Context ctx context.Context
clients []*github.Client clients []*github.Client
baseURL string
repoOwner string repoOwner string
repoName string repoName string
userName string userName string
@ -81,6 +82,7 @@ type GithubDownloaderV3 struct {
func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 { func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 {
downloader := GithubDownloaderV3{ downloader := GithubDownloaderV3{
userName: userName, userName: userName,
baseURL: baseURL,
password: password, password: password,
ctx: ctx, ctx: ctx,
repoOwner: repoOwner, repoOwner: repoOwner,
@ -118,6 +120,20 @@ func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, tok
return &downloader return &downloader
} }
// String implements Stringer
func (g *GithubDownloaderV3) String() string {
return fmt.Sprintf("migration from github server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
}
// ColorFormat provides a basic color format for a GithubDownloader
func (g *GithubDownloaderV3) ColorFormat(s fmt.State) {
if g == nil {
log.ColorFprintf(s, "<nil: GithubDownloaderV3>")
return
}
log.ColorFprintf(s, "migration from github server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
}
func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) { func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) {
githubClient := github.NewClient(client) githubClient := github.NewClient(client)
if baseURL != "https://github.com" { if baseURL != "https://github.com" {
@ -322,15 +338,30 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease)
Updated: asset.UpdatedAt.Time, Updated: asset.UpdatedAt.Time,
DownloadFunc: func() (io.ReadCloser, error) { DownloadFunc: func() (io.ReadCloser, error) {
g.waitAndPickClient() g.waitAndPickClient()
asset, redirectURL, err := g.getClient().Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, assetID, nil) readCloser, redirectURL, err := g.getClient().Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, assetID, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := g.RefreshRate(); err != nil { if err := g.RefreshRate(); err != nil {
log.Error("g.getClient().RateLimits: %s", err) log.Error("g.getClient().RateLimits: %s", err)
} }
if asset == nil {
if redirectURL != "" { if readCloser != nil {
return readCloser, nil
}
if redirectURL == "" {
return nil, fmt.Errorf("no release asset found for %d", assetID)
}
// Prevent open redirect
if !hasBaseURL(redirectURL, g.baseURL) &&
!hasBaseURL(redirectURL, "https://objects.githubusercontent.com/") {
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.GetID(), g, redirectURL)
return io.NopCloser(strings.NewReader(redirectURL)), nil
}
g.waitAndPickClient() g.waitAndPickClient()
req, err := http.NewRequestWithContext(g.ctx, "GET", redirectURL, nil) req, err := http.NewRequestWithContext(g.ctx, "GET", redirectURL, nil)
if err != nil { if err != nil {
@ -339,16 +370,12 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease)
resp, err := httpClient.Do(req) resp, err := httpClient.Do(req)
err1 := g.RefreshRate() err1 := g.RefreshRate()
if err1 != nil { if err1 != nil {
log.Error("g.getClient().RateLimits: %s", err1) log.Error("g.RefreshRate(): %s", err1)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.Body, nil return resp.Body, nil
}
return nil, fmt.Errorf("No release asset found for %d", assetID)
}
return asset, nil
}, },
}) })
} }
@ -697,7 +724,7 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
SHA: pr.GetHead().GetSHA(), SHA: pr.GetHead().GetSHA(),
OwnerName: pr.GetHead().GetUser().GetLogin(), OwnerName: pr.GetHead().GetUser().GetLogin(),
RepoName: pr.GetHead().GetRepo().GetName(), RepoName: pr.GetHead().GetRepo().GetName(),
CloneURL: pr.GetHead().GetRepo().GetCloneURL(), CloneURL: pr.GetHead().GetRepo().GetCloneURL(), // see below for SECURITY related issues here
}, },
Base: base.PullRequestBranch{ Base: base.PullRequestBranch{
Ref: pr.GetBase().GetRef(), Ref: pr.GetBase().GetRef(),
@ -705,10 +732,13 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
RepoName: pr.GetBase().GetRepo().GetName(), RepoName: pr.GetBase().GetRepo().GetName(),
OwnerName: pr.GetBase().GetUser().GetLogin(), OwnerName: pr.GetBase().GetUser().GetLogin(),
}, },
PatchURL: pr.GetPatchURL(), PatchURL: pr.GetPatchURL(), // see below for SECURITY related issues here
Reactions: reactions, Reactions: reactions,
ForeignIndex: int64(*pr.Number), ForeignIndex: int64(*pr.Number),
}) })
// SECURITY: Ensure that the PR is safe
_ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g)
} }
return allPRs, len(prs) < perPage, nil return allPRs, len(prs) < perPage, nil

View file

@ -63,6 +63,7 @@ type GitlabDownloader struct {
base.NullDownloader base.NullDownloader
ctx context.Context ctx context.Context
client *gitlab.Client client *gitlab.Client
baseURL string
repoID int repoID int
repoName string repoName string
issueCount int64 issueCount int64
@ -124,12 +125,27 @@ func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, username, passw
return &GitlabDownloader{ return &GitlabDownloader{
ctx: ctx, ctx: ctx,
client: gitlabClient, client: gitlabClient,
baseURL: baseURL,
repoID: gr.ID, repoID: gr.ID,
repoName: gr.Name, repoName: gr.Name,
maxPerPage: 100, maxPerPage: 100,
}, nil }, nil
} }
// String implements Stringer
func (g *GitlabDownloader) String() string {
return fmt.Sprintf("migration from gitlab server %s [%d]/%s", g.baseURL, g.repoID, g.repoName)
}
// ColorFormat provides a basic color format for a GitlabDownloader
func (g *GitlabDownloader) ColorFormat(s fmt.State) {
if g == nil {
log.ColorFprintf(s, "<nil: GitlabDownloader>")
return
}
log.ColorFprintf(s, "migration from gitlab server %s [%d]/%s", g.baseURL, g.repoID, g.repoName)
}
// SetContext set context // SetContext set context
func (g *GitlabDownloader) SetContext(ctx context.Context) { func (g *GitlabDownloader) SetContext(ctx context.Context) {
g.ctx = ctx g.ctx = ctx
@ -307,6 +323,11 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
return nil, err return nil, err
} }
if !hasBaseURL(link.URL, g.baseURL) {
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, link.URL)
return io.NopCloser(strings.NewReader(link.URL)), nil
}
req, err := http.NewRequest("GET", link.URL, nil) req, err := http.NewRequest("GET", link.URL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -610,6 +631,9 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
ForeignIndex: int64(pr.IID), ForeignIndex: int64(pr.IID),
Context: gitlabIssueContext{IsMergeRequest: true}, Context: gitlabIssueContext{IsMergeRequest: true},
}) })
// SECURITY: Ensure that the PR is safe
_ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g)
} }
return allPRs, len(prs) < perPage, nil return allPRs, len(prs) < perPage, nil

View file

@ -73,6 +73,20 @@ type GogsDownloader struct {
transport http.RoundTripper transport http.RoundTripper
} }
// String implements Stringer
func (g *GogsDownloader) String() string {
return fmt.Sprintf("migration from gogs server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
}
// ColorFormat provides a basic color format for a GogsDownloader
func (g *GogsDownloader) ColorFormat(s fmt.State) {
if g == nil {
log.ColorFprintf(s, "<nil: GogsDownloader>")
return
}
log.ColorFprintf(s, "migration from gogs server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
}
// SetContext set context // SetContext set context
func (g *GogsDownloader) SetContext(ctx context.Context) { func (g *GogsDownloader) SetContext(ctx context.Context) {
g.ctx = ctx g.ctx = ctx

View file

@ -125,7 +125,7 @@ func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName str
uploader := NewGiteaLocalUploader(ctx, doer, ownerName, opts.RepoName) uploader := NewGiteaLocalUploader(ctx, doer, ownerName, opts.RepoName)
uploader.gitServiceType = opts.GitServiceType uploader.gitServiceType = opts.GitServiceType
if err := migrateRepository(downloader, uploader, opts, messenger); err != nil { if err := migrateRepository(doer, downloader, uploader, opts, messenger); err != nil {
if err1 := uploader.Rollback(); err1 != nil { if err1 := uploader.Rollback(); err1 != nil {
log.Error("rollback failed: %v", err1) log.Error("rollback failed: %v", err1)
} }
@ -174,7 +174,7 @@ func newDownloader(ctx context.Context, ownerName string, opts base.MigrateOptio
// migrateRepository will download information and then upload it to Uploader, this is a simple // migrateRepository will download information and then upload it to Uploader, this is a simple
// process for small repository. For a big repository, save all the data to disk // process for small repository. For a big repository, save all the data to disk
// before upload is better // before upload is better
func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions, messenger base.Messenger) error { func migrateRepository(doer *user_model.User, downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions, messenger base.Messenger) error {
if messenger == nil { if messenger == nil {
messenger = base.NilMessenger messenger = base.NilMessenger
} }
@ -195,6 +195,27 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
return err return err
} }
// SECURITY: If the downloader is not a RepositoryRestorer then we need to recheck the CloneURL
if _, ok := downloader.(*RepositoryRestorer); !ok {
// Now the clone URL can be rewritten by the downloader so we must recheck
if err := IsMigrateURLAllowed(repo.CloneURL, doer); err != nil {
return err
}
// SECURITY: Ensure that we haven't been redirected from an external to a local filesystem
// Now we know all of these must parse
cloneAddrURL, _ := url.Parse(opts.CloneAddr)
cloneURL, _ := url.Parse(repo.CloneURL)
if cloneURL.Scheme == "file" || cloneURL.Scheme == "" {
if cloneAddrURL.Scheme != "file" && cloneAddrURL.Scheme != "" {
return fmt.Errorf("repo info has changed from external to local filesystem")
}
}
// We don't actually need to check the OriginalURL as it isn't used anywhere
}
log.Trace("migrating git data from %s", repo.CloneURL) log.Trace("migrating git data from %s", repo.CloneURL)
messenger("repo.migrate.migrating_git") messenger("repo.migrate.migrating_git")
if err = uploader.CreateRepo(repo, opts); err != nil { if err = uploader.CreateRepo(repo, opts); err != nil {

View file

@ -110,6 +110,20 @@ func NewOneDevDownloader(ctx context.Context, baseURL *url.URL, username, passwo
return downloader return downloader
} }
// String implements Stringer
func (d *OneDevDownloader) String() string {
return fmt.Sprintf("migration from oneDev server %s [%d]/%s", d.baseURL, d.repoID, d.repoName)
}
// ColorFormat provides a basic color format for a OneDevDownloader
func (d *OneDevDownloader) ColorFormat(s fmt.State) {
if d == nil {
log.ColorFprintf(s, "<nil: OneDevDownloader>")
return
}
log.ColorFprintf(s, "migration from oneDev server %s [%d]/%s", d.baseURL, d.repoID, d.repoName)
}
func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string, result interface{}) error { func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string, result interface{}) error {
u, err := d.baseURL.Parse(endpoint) u, err := d.baseURL.Parse(endpoint)
if err != nil { if err != nil {
@ -542,6 +556,9 @@ func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
ForeignIndex: pr.ID, ForeignIndex: pr.ID,
Context: onedevIssueContext{IsPullRequest: true}, Context: onedevIssueContext{IsPullRequest: true},
}) })
// SECURITY: Ensure that the PR is safe
_ = CheckAndEnsureSafePR(pullRequests[len(pullRequests)-1], d.baseURL.String(), d)
} }
return pullRequests, len(pullRequests) == 0, nil return pullRequests, len(pullRequests) == 0, nil

View file

@ -243,6 +243,7 @@ func (r *RepositoryRestorer) GetPullRequests(page, perPage int) ([]*base.PullReq
} }
for _, pr := range pulls { for _, pr := range pulls {
pr.PatchURL = "file://" + filepath.Join(r.baseDir, pr.PatchURL) pr.PatchURL = "file://" + filepath.Join(r.baseDir, pr.PatchURL)
CheckAndEnsureSafePR(pr, "", r)
} }
return pulls, true, nil return pulls, true, nil
} }

View file

@ -37,6 +37,9 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool,
if err != nil { if err != nil {
return false, fmt.Errorf("GetProtectedTags: %v", err) return false, fmt.Errorf("GetProtectedTags: %v", err)
} }
// Trim '--' prefix to prevent command line argument vulnerability.
rel.TagName = strings.TrimPrefix(rel.TagName, "--")
isAllowed, err := git_model.IsUserAllowedToControlTag(protectedTags, rel.TagName, rel.PublisherID) isAllowed, err := git_model.IsUserAllowedToControlTag(protectedTags, rel.TagName, rel.PublisherID)
if err != nil { if err != nil {
return false, err return false, err
@ -52,8 +55,6 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool,
return false, fmt.Errorf("createTag::GetCommit[%v]: %v", rel.Target, err) return false, fmt.Errorf("createTag::GetCommit[%v]: %v", rel.Target, err)
} }
// Trim '--' prefix to prevent command line argument vulnerability.
rel.TagName = strings.TrimPrefix(rel.TagName, "--")
if len(msg) > 0 { if len(msg) > 0 {
if err = gitRepo.CreateAnnotatedTag(rel.TagName, msg, commit.ID.String()); err != nil { if err = gitRepo.CreateAnnotatedTag(rel.TagName, msg, commit.ID.String()); err != nil {
if strings.Contains(err.Error(), "is not a valid tag name") { if strings.Contains(err.Error(), "is not a valid tag name") {
@ -308,7 +309,7 @@ func DeleteReleaseByID(ctx context.Context, id int64, doer *user_model.User, del
} }
} }
if stdout, _, err := git.NewCommand(ctx, "tag", "-d", rel.TagName). if stdout, _, err := git.NewCommand(ctx, "tag", "-d", "--", rel.TagName).
SetDescription(fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID)). SetDescription(fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID)).
RunStdString(&git.RunOpts{Dir: repo.RepoPath()}); err != nil && !strings.Contains(err.Error(), "not found") { RunStdString(&git.RunOpts{Dir: repo.RepoPath()}); err != nil && !strings.Contains(err.Error(), "not found") {
log.Error("DeleteReleaseByID (git tag -d): %d in %v Failed:\nStdout: %s\nError: %v", rel.ID, repo, stdout, err) log.Error("DeleteReleaseByID (git tag -d): %d in %v Failed:\nStdout: %s\nError: %v", rel.ID, repo, stdout, err)

View file

@ -57,6 +57,21 @@ func (ErrUnknownArchiveFormat) Is(err error) bool {
return ok return ok
} }
// RepoRefNotFoundError is returned when a requested reference (commit, tag) was not found.
type RepoRefNotFoundError struct {
RefName string
}
// Error implements error.
func (e RepoRefNotFoundError) Error() string {
return fmt.Sprintf("unrecognized repository reference: %s", e.RefName)
}
func (e RepoRefNotFoundError) Is(err error) bool {
_, ok := err.(RepoRefNotFoundError)
return ok
}
// NewRequest creates an archival request, based on the URI. The // NewRequest creates an archival request, based on the URI. The
// resulting ArchiveRequest is suitable for being passed to ArchiveRepository() // resulting ArchiveRequest is suitable for being passed to ArchiveRepository()
// if it's determined that the request still needs to be satisfied. // if it's determined that the request still needs to be satisfied.
@ -103,7 +118,7 @@ func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest
} }
} }
} else { } else {
return nil, fmt.Errorf("Unknow ref %s type", r.refName) return nil, RepoRefNotFoundError{RefName: r.refName}
} }
return r, nil return r, nil
@ -115,6 +130,49 @@ func (aReq *ArchiveRequest) GetArchiveName() string {
return strings.ReplaceAll(aReq.refName, "/", "-") + "." + aReq.Type.String() return strings.ReplaceAll(aReq.refName, "/", "-") + "." + aReq.Type.String()
} }
// Await awaits the completion of an ArchiveRequest. If the archive has
// already been prepared the method returns immediately. Otherwise an archiver
// process will be started and its completion awaited. On success the returned
// RepoArchiver may be used to download the archive. Note that even if the
// context is cancelled/times out a started archiver will still continue to run
// in the background.
func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver, error) {
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
if err != nil {
return nil, fmt.Errorf("models.GetRepoArchiver: %v", err)
}
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
// Archive already generated, we're done.
return archiver, nil
}
if err := StartArchive(aReq); err != nil {
return nil, fmt.Errorf("archiver.StartArchive: %v", err)
}
poll := time.NewTicker(time.Second * 1)
defer poll.Stop()
for {
select {
case <-graceful.GetManager().HammerContext().Done():
// System stopped.
return nil, graceful.GetManager().HammerContext().Err()
case <-ctx.Done():
return nil, ctx.Err()
case <-poll.C:
archiver, err = repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
if err != nil {
return nil, fmt.Errorf("repo_model.GetRepoArchiver: %v", err)
}
if archiver != nil && archiver.Status == repo_model.ArchiverReady {
return archiver, nil
}
}
}
}
func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) { func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
txCtx, committer, err := db.TxContext() txCtx, committer, err := db.TxContext()
if err != nil { if err != nil {
@ -147,11 +205,7 @@ func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
} }
} }
rPath, err := archiver.RelativePath() rPath := archiver.RelativePath()
if err != nil {
return nil, err
}
_, err = storage.RepoArchives.Stat(rPath) _, err = storage.RepoArchives.Stat(rPath)
if err == nil { if err == nil {
if archiver.Status == repo_model.ArchiverGenerating { if archiver.Status == repo_model.ArchiverGenerating {
@ -284,13 +338,10 @@ func StartArchive(request *ArchiveRequest) error {
} }
func deleteOldRepoArchiver(ctx context.Context, archiver *repo_model.RepoArchiver) error { func deleteOldRepoArchiver(ctx context.Context, archiver *repo_model.RepoArchiver) error {
p, err := archiver.RelativePath()
if err != nil {
return err
}
if err := repo_model.DeleteRepoArchiver(ctx, archiver); err != nil { if err := repo_model.DeleteRepoArchiver(ctx, archiver); err != nil {
return err return err
} }
p := archiver.RelativePath()
if err := storage.RepoArchives.Delete(p); err != nil { if err := storage.RepoArchives.Delete(p); err != nil {
log.Error("delete repo archive file failed: %v", err) log.Error("delete repo archive file failed: %v", err)
} }

View file

@ -341,12 +341,12 @@
</div> </div>
<div class="field"> <div class="field">
<label for="oauth2_required_claim_name">{{.i18n.Tr "admin.auths.oauth2_required_claim_name"}}</label> <label for="oauth2_required_claim_name">{{.i18n.Tr "admin.auths.oauth2_required_claim_name"}}</label>
<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" values="{{$cfg.RequiredClaimName}}"> <input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{$cfg.RequiredClaimName}}">
<p class="help">{{.i18n.Tr "admin.auths.oauth2_required_claim_name_helper"}}</p> <p class="help">{{.i18n.Tr "admin.auths.oauth2_required_claim_name_helper"}}</p>
</div> </div>
<div class="field"> <div class="field">
<label for="oauth2_required_claim_value">{{.i18n.Tr "admin.auths.oauth2_required_claim_value"}}</label> <label for="oauth2_required_claim_value">{{.i18n.Tr "admin.auths.oauth2_required_claim_value"}}</label>
<input id="oauth2_required_claim_value" name="oauth2_required_claim_value" values="{{$cfg.RequiredClaimValue}}"> <input id="oauth2_required_claim_value" name="oauth2_required_claim_value" value="{{$cfg.RequiredClaimValue}}">
<p class="help">{{.i18n.Tr "admin.auths.oauth2_required_claim_value_helper"}}</p> <p class="help">{{.i18n.Tr "admin.auths.oauth2_required_claim_value_helper"}}</p>
</div> </div>
<div class="field"> <div class="field">

View file

@ -22,7 +22,7 @@
<script src='https://hcaptcha.com/1/api.js' async></script> <script src='https://hcaptcha.com/1/api.js' async></script>
{{end}} {{end}}
{{end}} {{end}}
<script src="{{AssetUrlPrefix}}/js/index.js?v={{MD5 AppVer}}" onerror="alert('Failed to load asset files from ' + this.src + ', please make sure the asset files can be accessed and the ROOT_URL setting in app.ini is correct.')"></script> <script src="{{AssetUrlPrefix}}/js/index.js?v={{AssetVersion}}" onerror="alert('Failed to load asset files from ' + this.src + ', please make sure the asset files can be accessed and the ROOT_URL setting in app.ini is correct.')"></script>
{{template "custom/footer" .}} {{template "custom/footer" .}}
</body> </body>
</html> </html>

View file

@ -21,7 +21,7 @@
{{end}} {{end}}
<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml"> <link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
<link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png" type="image/png"> <link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png" type="image/png">
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/index.css?v={{MD5 AppVer}}"> <link rel="stylesheet" href="{{AssetUrlPrefix}}/css/index.css?v={{AssetVersion}}">
{{template "base/head_script" .}} {{template "base/head_script" .}}
<noscript> <noscript>
<style> <style>
@ -67,10 +67,10 @@
<meta property="og:site_name" content="{{AppName}}"> <meta property="og:site_name" content="{{AppName}}">
{{if .IsSigned }} {{if .IsSigned }}
{{ if ne .SignedUser.Theme "gitea" }} {{ if ne .SignedUser.Theme "gitea" }}
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/theme-{{.SignedUser.Theme | PathEscape}}.css?v={{MD5 AppVer}}"> <link rel="stylesheet" href="{{AssetUrlPrefix}}/css/theme-{{.SignedUser.Theme | PathEscape}}.css?v={{AssetVersion}}">
{{end}} {{end}}
{{else if ne DefaultTheme "gitea"}} {{else if ne DefaultTheme "gitea"}}
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/theme-{{DefaultTheme | PathEscape}}.css?v={{MD5 AppVer}}"> <link rel="stylesheet" href="{{AssetUrlPrefix}}/css/theme-{{DefaultTheme | PathEscape}}.css?v={{AssetVersion}}">
{{end}} {{end}}
{{template "custom/header" .}} {{template "custom/header" .}}
</head> </head>

View file

@ -10,6 +10,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
appVer: '{{AppVer}}', appVer: '{{AppVer}}',
appUrl: '{{AppUrl}}', appUrl: '{{AppUrl}}',
appSubUrl: '{{AppSubUrl}}', appSubUrl: '{{AppSubUrl}}',
assetVersionEncoded: encodeURIComponent('{{AssetVersion}}'), // will be used in URL construction directly
assetUrlPrefix: '{{AssetUrlPrefix}}', assetUrlPrefix: '{{AssetUrlPrefix}}',
runModeIsProd: {{.RunModeIsProd}}, runModeIsProd: {{.RunModeIsProd}},
customEmojis: {{CustomEmojis}}, customEmojis: {{CustomEmojis}},

View file

@ -32,9 +32,7 @@
</div> </div>
<div class="ui attached segment members"> <div class="ui attached segment members">
{{range .Members}} {{range .Members}}
<a href="{{.HomeLink}}" title="{{.Name}}"> {{template "shared/user/avatarlink" .}}
{{avatar .}}
</a>
{{end}} {{end}}
</div> </div>
<div class="ui bottom attached header"> <div class="ui bottom attached header">

View file

@ -1,5 +1,6 @@
{{$release := .release}} {{$release := .release}}
{{$defaultBranch := $.root.BranchName}}{{if and .root.IsViewTag (not .noTag)}}{{$defaultBranch = .root.TagName}}{{end}}{{if eq $defaultBranch ""}}{{$defaultBranch = $.root.Repository.DefaultBranch}}{{end}} {{$defaultBranch := $.root.BranchName}}{{if and .root.IsViewTag (not .noTag)}}{{$defaultBranch = .root.TagName}}{{end}}{{if eq $defaultBranch ""}}{{$defaultBranch = $.root.Repository.DefaultBranch}}{{end}}
{{$type := ""}}{{if and .root.IsViewTag (not .noTag)}}{{$type = "tag"}}{{else if .root.IsViewBranch}}{{$type = "branch"}}{{else}}{{$type = "tree"}}{{end}}
{{$showBranchesInDropdown := not .root.HideBranchesInDropdown}} {{$showBranchesInDropdown := not .root.HideBranchesInDropdown}}
<div class="fitted item choose reference{{if not $release}} mr-1{{end}}"> <div class="fitted item choose reference{{if not $release}} mr-1{{end}}">
<div class="ui floating filter dropdown custom" <div class="ui floating filter dropdown custom"
@ -7,20 +8,20 @@
data-can-create-branch="{{if .canCreateBranch}}{{.canCreateBranch}}{{else}}{{.root.CanCreateBranch}}{{end}}" data-can-create-branch="{{if .canCreateBranch}}{{.canCreateBranch}}{{else}}{{.root.CanCreateBranch}}{{end}}"
data-no-results="{{.root.i18n.Tr "repo.pulls.no_results"}}" data-no-results="{{.root.i18n.Tr "repo.pulls.no_results"}}"
data-set-action="{{.setAction}}" data-submit-form="{{.submitForm}}" data-set-action="{{.setAction}}" data-submit-form="{{.submitForm}}"
data-view-type="{{if and .root.IsViewTag (not .noTag)}}tag{{else if .root.IsViewBranch}}branch{{else}}tree{{end}}" data-view-type="{{$type}}"
data-ref-name="{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}" data-ref-name="{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}"
data-branch-url-prefix="{{if .branchURLPrefix}}{{.branchURLPrefix}}{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/branch/{{end}}" data-branch-url-prefix="{{if .branchURLPrefix}}{{.branchURLPrefix}}{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/branch/{{end}}"
data-branch-url-suffix="{{if .branchURLSuffix}}{{.branchURLSuffix}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}" data-branch-url-suffix="{{if .branchURLSuffix}}{{.branchURLSuffix}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}"
data-tag-url-prefix="{{if .tagURLPrefix}}{{.tagURLPrefix}}{{else if $release}}{{$.root.RepoLink}}/compare/{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/tag/{{end}}" data-tag-url-prefix="{{if .tagURLPrefix}}{{.tagURLPrefix}}{{else if $release}}{{$.root.RepoLink}}/compare/{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/tag/{{end}}"
data-tag-url-suffix="{{if .tagURLSuffix}}{{.tagURLSuffix}}{{else if $release}}...{{if $release.IsDraft}}{{PathEscapeSegments $release.Target}}{{else}}{{if $release.TagName}}{{PathEscapeSegments $release.TagName}}{{else}}{{PathEscapeSegments $release.Sha1}}{{end}}{{end}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}"> data-tag-url-suffix="{{if .tagURLSuffix}}{{.tagURLSuffix}}{{else if $release}}...{{if $release.IsDraft}}{{PathEscapeSegments $release.Target}}{{else}}{{if $release.TagName}}{{PathEscapeSegments $release.TagName}}{{else}}{{PathEscapeSegments $release.Sha1}}{{end}}{{end}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}">
<div class="ui basic small compact button" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible"> <div class="branch-dropdown-button ellipsis ui basic small compact button" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
<span class="text"> <span class="text">
{{if $release}} {{if $release}}
{{.root.i18n.Tr "repo.release.compare"}} {{.root.i18n.Tr "repo.release.compare"}}
{{else}} {{else}}
<span :class="{visible: isViewTag}" v-if="isViewTag" v-cloak>{{svg "octicon-tag"}} {{.root.i18n.Tr "repo.tag"}}:</span> <span :class="{visible: isViewTag}" v-if="isViewTag" {{if not (eq $type "tag")}}v-cloak{{end}}>{{svg "octicon-tag"}} {{.root.i18n.Tr "repo.tag"}}:</span>
<span :class="{visible: isViewBranch}" v-if="isViewBranch" v-cloak>{{svg "octicon-git-branch"}} {{.root.i18n.Tr "repo.branch"}}:</span> <span :class="{visible: isViewBranch}" v-if="isViewBranch" {{if not (eq $type "branch")}}v-cloak{{end}}>{{svg "octicon-git-branch"}} {{.root.i18n.Tr "repo.branch"}}:</span>
<span :class="{visible: isViewTree}" v-if="isViewTree" v-cloak>{{svg "octicon-git-branch"}} {{.root.i18n.Tr "repo.tree"}}:</span> <span :class="{visible: isViewTree}" v-if="isViewTree" {{if not (eq $type "tree")}}v-cloak{{end}}>{{svg "octicon-git-branch"}} {{.root.i18n.Tr "repo.tree"}}:</span>
<strong ref="dropdownRefName">{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong> <strong ref="dropdownRefName">{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong>
{{end}} {{end}}
</span> </span>

View file

@ -1,15 +1,15 @@
<!-- there is always at least one button (by context/repo.go) --> <!-- there is always at least one button (by context/repo.go) -->
{{if $.CloneButtonShowHTTPS}} {{if $.CloneButtonShowHTTPS}}
<button class="ui basic clone button no-transition" id="repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}"> <button class="ui basic small compact clone button no-transition" id="repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}">
{{if UseHTTPS}}HTTPS{{else}}HTTP{{end}} {{if UseHTTPS}}HTTPS{{else}}HTTP{{end}}
</button> </button>
{{end}} {{end}}
{{if $.CloneButtonShowSSH}} {{if $.CloneButtonShowSSH}}
<button class="ui basic clone button no-transition" id="repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}"> <button class="ui basic small compact clone button no-transition" id="repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">
SSH SSH
</button> </button>
{{end}} {{end}}
<input id="repo-clone-url" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" size="1" readonly> <input id="repo-clone-url" size="20" class="js-clone-url br-0" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly>
<button class="ui basic icon button tooltip" id="clipboard-btn" data-content="{{.i18n.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{.i18n.Tr "copy_url"}}"> <button class="ui basic small compact icon button tooltip" id="clipboard-btn" data-content="{{.i18n.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{.i18n.Tr "copy_url"}}">
{{svg "octicon-paste"}} {{svg "octicon-copy" 14}}
</button> </button>

View file

@ -222,7 +222,7 @@
{{.Verification.SigningSSHKey.Fingerprint}} {{.Verification.SigningSSHKey.Fingerprint}}
{{else}} {{else}}
<span class="ui text mr-3">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <span class="ui text mr-3">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span>
{{.Verification.SigningKey.KeyID}} {{.Verification.SigningKey.PaddedKeyID}}
{{end}} {{end}}
{{else}} {{else}}
{{svg "octicon-shield-lock" 16 "mr-3"}} {{svg "octicon-shield-lock" 16 "mr-3"}}
@ -231,7 +231,7 @@
{{.Verification.SigningSSHKey.Fingerprint}} {{.Verification.SigningSSHKey.Fingerprint}}
{{else}} {{else}}
<span class="ui text mr-3 tooltip" data-content="{{.i18n.Tr "gpg.default_key"}}">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <span class="ui text mr-3 tooltip" data-content="{{.i18n.Tr "gpg.default_key"}}">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span>
{{.Verification.SigningKey.KeyID}} {{.Verification.SigningKey.PaddedKeyID}}
{{end}} {{end}}
{{end}} {{end}}
{{else if .Verification.Warning}} {{else if .Verification.Warning}}
@ -241,14 +241,14 @@
{{.Verification.SigningSSHKey.Fingerprint}} {{.Verification.SigningSSHKey.Fingerprint}}
{{else}} {{else}}
<span class="ui text mr-3">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <span class="ui text mr-3">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span>
{{.Verification.SigningKey.KeyID}} {{.Verification.SigningKey.PaddedKeyID}}
{{end}} {{end}}
{{else}} {{else}}
{{if .Verification.SigningKey}} {{if .Verification.SigningKey}}
{{if ne .Verification.SigningKey.KeyID ""}} {{if ne .Verification.SigningKey.KeyID ""}}
{{svg "octicon-shield" 16 "mr-3"}} {{svg "octicon-shield" 16 "mr-3"}}
<span class="ui text mr-3">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <span class="ui text mr-3">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span>
{{.Verification.SigningKey.KeyID}} {{.Verification.SigningKey.PaddedKeyID}}
{{end}} {{end}}
{{end}} {{end}}
{{if .Verification.SigningSSHKey}} {{if .Verification.SigningSSHKey}}

View file

@ -3,9 +3,9 @@
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
{{template "repo/sub_menu" .}} {{template "repo/sub_menu" .}}
<div class="ui secondary stackable menu mobile--margin-between-items"> <div class="repo-button-row df ac sb fw mb-4 mt-3">
<div class="df ac">
{{template "repo/branch_dropdown" dict "root" .}} {{template "repo/branch_dropdown" dict "root" .}}
<div class="fitted item">
<a href="{{.RepoLink}}/graph" class="ui basic small compact button"> <a href="{{.RepoLink}}/graph" class="ui basic small compact button">
<span class="text"> <span class="text">
{{svg "octicon-git-branch"}} {{svg "octicon-git-branch"}}

View file

@ -13,14 +13,14 @@
<h4>{{.i18n.Tr "repo.diff.data_not_available"}}</h4> <h4>{{.i18n.Tr "repo.diff.data_not_available"}}</h4>
{{else}} {{else}}
<div> <div>
<div class="diff-detail-box diff-box sticky df sb ac"> <div class="diff-detail-box diff-box sticky df sb ac fw">
<div class="diff-detail-stats df ac"> <div class="diff-detail-stats df ac">
{{svg "octicon-diff" 16 "mr-2"}}{{.i18n.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}} {{svg "octicon-diff" 16 "mr-2"}}{{.i18n.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}}
</div> </div>
<div class="diff-detail-actions df ac"> <div class="diff-detail-actions df ac">
{{if and .PageIsPullFiles $.SignedUserID (not .IsArchived)}} {{if and .PageIsPullFiles $.SignedUserID (not .IsArchived)}}
<progress id="viewed-files-summary" class="mr-2" value="{{.Diff.NumViewedFiles}}" max="{{.Diff.NumFiles}}"></progress> <progress id="viewed-files-summary" class="mr-2" value="{{.Diff.NumViewedFiles}}" max="{{.Diff.NumFiles}}"></progress>
<label for="viewed-files-summary" id="viewed-files-summary-label" class="mr-2" data-text-changed-template="{{.i18n.Tr "repo.pulls.viewed_files_label"}}"> <label for="viewed-files-summary" id="viewed-files-summary-label" class="mr-3" data-text-changed-template="{{.i18n.Tr "repo.pulls.viewed_files_label"}}">
{{.i18n.Tr "repo.pulls.viewed_files_label" .Diff.NumViewedFiles .Diff.NumFiles}} {{.i18n.Tr "repo.pulls.viewed_files_label" .Diff.NumViewedFiles .Diff.NumFiles}}
</label> </label>
{{end}} {{end}}

View file

@ -5,9 +5,7 @@
{{if .OriginalAuthor }} {{if .OriginalAuthor }}
<span class="avatar"><img src="{{AppSubUrl}}/assets/img/avatar_default.png"></span> <span class="avatar"><img src="{{AppSubUrl}}/assets/img/avatar_default.png"></span>
{{else}} {{else}}
<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}> {{template "shared/user/avatarlink" .Poster}}
{{avatar .Poster}}
</a>
{{end}} {{end}}
<div class="content comment-container"> <div class="content comment-container">
<div class="ui top attached header comment-header df ac sb"> <div class="ui top attached header comment-header df ac sb">
@ -27,9 +25,7 @@
</span> </span>
{{else}} {{else}}
<span class="text grey"> <span class="text grey">
<a {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}> {{template "shared/user/namelink" .Poster}}
{{.Poster.GetDisplayName}}
</a>
{{$.root.i18n.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{$.root.i18n.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}}
</span> </span>
{{end}} {{end}}

View file

@ -1,5 +1,5 @@
<div class="ui top right pointing dropdown custom" id="review-box"> <div class="ui top right pointing dropdown custom" id="review-box">
<div class="ui tiny green button btn-review"> <div class="ui tiny green button btn-review ml-2 mr-0">
{{.i18n.Tr "repo.diff.review"}} {{.i18n.Tr "repo.diff.review"}}
<span class="ui small label review-comments-counter" data-pending-comment-number="{{.PendingCodeCommentNumber}}">{{.PendingCodeCommentNumber}}</span> <span class="ui small label review-comments-counter" data-pending-comment-number="{{.PendingCodeCommentNumber}}">{{.PendingCodeCommentNumber}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}

Some files were not shown because too many files have changed in this diff Show more