` with classes `markup` and `XXXXX`. The `markup` class provides out of the box styling (as does `markdown` if `XXXXX` is `markdown`). Otherwise you can use these classes to specifically target the contents of your rendered HTML.
diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go
index 6572b0ee1e..8a4df89511 100644
--- a/modules/markup/csv/csv.go
+++ b/modules/markup/csv/csv.go
@@ -10,6 +10,7 @@ import (
"html"
"io"
"io/ioutil"
+ "regexp"
"strconv"
"code.gitea.io/gitea/modules/csv"
@@ -38,6 +39,15 @@ func (Renderer) Extensions() []string {
return []string{".csv", ".tsv"}
}
+// SanitizerRules implements markup.Renderer
+func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
+ return []setting.MarkupSanitizerRule{
+ {Element: "table", AllowAttr: "class", Regexp: regexp.MustCompile(`data-table`)},
+ {Element: "th", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)},
+ {Element: "td", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)},
+ }
+}
+
func writeField(w io.Writer, element, class, field string) error {
if _, err := io.WriteString(w, "<"); err != nil {
return err
diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go
index 62814c9914..c849f505e7 100644
--- a/modules/markup/external/external.go
+++ b/modules/markup/external/external.go
@@ -30,7 +30,7 @@ func RegisterRenderers() {
// Renderer implements markup.Renderer for external tools
type Renderer struct {
- setting.MarkupRenderer
+ *setting.MarkupRenderer
}
// Name returns the external tool name
@@ -48,6 +48,11 @@ func (p *Renderer) Extensions() []string {
return p.FileExtensions
}
+// SanitizerRules implements markup.Renderer
+func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
+ return p.MarkupSanitizerRules
+}
+
func envMark(envName string) string {
if runtime.GOOS == "windows" {
return "%" + envName + "%"
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 4cdd5798c8..8c3d2b5395 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -112,7 +112,7 @@ func TestRender_links(t *testing.T) {
defaultCustom := setting.Markdown.CustomURLSchemes
setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"}
- ReplaceSanitizer()
+ InitializeSanitizer()
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
test(
@@ -192,7 +192,7 @@ func TestRender_links(t *testing.T) {
// Restore previous settings
setting.Markdown.CustomURLSchemes = defaultCustom
- ReplaceSanitizer()
+ InitializeSanitizer()
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
}
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index 87fae2a23b..cac2a180fa 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -199,7 +199,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
}
_ = lw.Close()
}()
- buf := markup.SanitizeReader(rd)
+ buf := markup.SanitizeReader(rd, "")
_, err := io.Copy(output, buf)
return err
}
@@ -215,7 +215,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
if log.IsDebug() {
log.Debug("Panic in markdown: %v\n%s", err, string(log.Stack(2)))
}
- ret := markup.SanitizeReader(input)
+ ret := markup.SanitizeReader(input, "")
_, err = io.Copy(output, ret)
if err != nil {
log.Error("SanitizeReader failed: %v", err)
@@ -249,6 +249,11 @@ func (Renderer) Extensions() []string {
return setting.Markdown.FileExtensions
}
+// SanitizerRules implements markup.Renderer
+func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
+ return []setting.MarkupSanitizerRule{}
+}
+
// Render implements markup.Renderer
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
return render(ctx, input, output)
diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index 851fc97f9a..7e9f1f45c5 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/alecthomas/chroma"
@@ -41,6 +42,11 @@ func (Renderer) Extensions() []string {
return []string{".org"}
}
+// SanitizerRules implements markup.Renderer
+func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
+ return []setting.MarkupSanitizerRule{}
+}
+
// Render renders orgmode rawbytes to HTML
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
htmlWriter := org.NewHTMLWriter()
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index d60c8ad710..04619caee3 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -81,6 +81,7 @@ type Renderer interface {
Name() string // markup format name
Extensions() []string
NeedPostProcess() bool
+ SanitizerRules() []setting.MarkupSanitizerRule
Render(ctx *RenderContext, input io.Reader, output io.Writer) error
}
@@ -136,37 +137,32 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
_ = pw.Close()
}()
- if renderer.NeedPostProcess() {
- pr2, pw2 := io.Pipe()
- defer func() {
- _ = pr2.Close()
- _ = pw2.Close()
- }()
+ pr2, pw2 := io.Pipe()
+ defer func() {
+ _ = pr2.Close()
+ _ = pw2.Close()
+ }()
- wg.Add(1)
- go func() {
- buf := SanitizeReader(pr2)
- _, err = io.Copy(output, buf)
- _ = pr2.Close()
- wg.Done()
- }()
+ wg.Add(1)
+ go func() {
+ buf := SanitizeReader(pr2, renderer.Name())
+ _, err = io.Copy(output, buf)
+ _ = pr2.Close()
+ wg.Done()
+ }()
- wg.Add(1)
- go func() {
+ wg.Add(1)
+ go func() {
+ if renderer.NeedPostProcess() {
err = PostProcess(ctx, pr, pw2)
- _ = pr.Close()
- _ = pw2.Close()
- wg.Done()
- }()
- } else {
- wg.Add(1)
- go func() {
- buf := SanitizeReader(pr)
- _, err = io.Copy(output, buf)
- _ = pr.Close()
- wg.Done()
- }()
- }
+ } else {
+ _, err = io.Copy(pw2, pr)
+ }
+ _ = pr.Close()
+ _ = pw2.Close()
+ wg.Done()
+ }()
+
if err1 := renderer.Render(ctx, input, pw); err1 != nil {
return err1
}
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index 5611bd06ad..9342d65de5 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -19,8 +19,9 @@ import (
// Sanitizer is a protection wrapper of *bluemonday.Policy which does not allow
// any modification to the underlying policies once it's been created.
type Sanitizer struct {
- policy *bluemonday.Policy
- init sync.Once
+ defaultPolicy *bluemonday.Policy
+ rendererPolicies map[string]*bluemonday.Policy
+ init sync.Once
}
var sanitizer = &Sanitizer{}
@@ -30,47 +31,57 @@ var sanitizer = &Sanitizer{}
// entire application lifecycle.
func NewSanitizer() {
sanitizer.init.Do(func() {
- ReplaceSanitizer()
+ InitializeSanitizer()
})
}
-// ReplaceSanitizer replaces the current sanitizer to account for changes in settings
-func ReplaceSanitizer() {
- sanitizer.policy = bluemonday.UGCPolicy()
+// InitializeSanitizer (re)initializes the current sanitizer to account for changes in settings
+func InitializeSanitizer() {
+ sanitizer.rendererPolicies = map[string]*bluemonday.Policy{}
+ sanitizer.defaultPolicy = createDefaultPolicy()
+
+ for name, renderer := range renderers {
+ sanitizerRules := renderer.SanitizerRules()
+ if len(sanitizerRules) > 0 {
+ policy := createDefaultPolicy()
+ addSanitizerRules(policy, sanitizerRules)
+ sanitizer.rendererPolicies[name] = policy
+ }
+ }
+}
+
+func createDefaultPolicy() *bluemonday.Policy {
+ policy := bluemonday.UGCPolicy()
// For Chroma markdown plugin
- sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^is-loading$`)).OnElements("pre")
- sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^is-loading$`)).OnElements("pre")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code")
// Checkboxes
- sanitizer.policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
- sanitizer.policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
+ policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
+ policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
// Custom URL-Schemes
if len(setting.Markdown.CustomURLSchemes) > 0 {
- sanitizer.policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
+ policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
}
// Allow classes for anchors
- sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue`)).OnElements("a")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue`)).OnElements("a")
// Allow classes for task lists
- sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
// Allow icons
- sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
// Allow unlabelled labels
- sanitizer.policy.AllowNoAttrs().OnElements("label")
+ policy.AllowNoAttrs().OnElements("label")
// Allow classes for emojis
- sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
// Allow icons, emojis, chroma syntax and keyword markup on span
- sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
-
- // Allow data tables
- sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`data-table`)).OnElements("table")
- sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`line-num`)).OnElements("th", "td")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
// Allow generally safe attributes
generalSafeAttrs := []string{"abbr", "accept", "accept-charset",
@@ -101,18 +112,29 @@ func ReplaceSanitizer() {
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "wbr",
}
- sanitizer.policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
+ policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
- sanitizer.policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
+ policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
// FIXME: Need to handle longdesc in img but there is no easy way to do it
// Custom keyword markup
- for _, rule := range setting.ExternalSanitizerRules {
- if rule.Regexp != nil {
- sanitizer.policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
- } else {
- sanitizer.policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
+ addSanitizerRules(policy, setting.ExternalSanitizerRules)
+
+ return policy
+}
+
+func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
+ for _, rule := range rules {
+ if rule.AllowDataURIImages {
+ policy.AllowDataURIImages()
+ }
+ if rule.Element != "" {
+ if rule.Regexp != nil {
+ policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
+ } else {
+ policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
+ }
}
}
}
@@ -120,11 +142,15 @@ func ReplaceSanitizer() {
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
func Sanitize(s string) string {
NewSanitizer()
- return sanitizer.policy.Sanitize(s)
+ return sanitizer.defaultPolicy.Sanitize(s)
}
// SanitizeReader sanitizes a Reader
-func SanitizeReader(r io.Reader) *bytes.Buffer {
+func SanitizeReader(r io.Reader, renderer string) *bytes.Buffer {
NewSanitizer()
- return sanitizer.policy.SanitizeReader(r)
+ policy, exist := sanitizer.rendererPolicies[renderer]
+ if !exist {
+ policy = sanitizer.defaultPolicy
+ }
+ return policy.SanitizeReader(r)
}
diff --git a/modules/setting/markup.go b/modules/setting/markup.go
index 43df4ce442..31ec1dd2eb 100644
--- a/modules/setting/markup.go
+++ b/modules/setting/markup.go
@@ -15,31 +15,34 @@ import (
// ExternalMarkupRenderers represents the external markup renderers
var (
- ExternalMarkupRenderers []MarkupRenderer
+ ExternalMarkupRenderers []*MarkupRenderer
ExternalSanitizerRules []MarkupSanitizerRule
)
// MarkupRenderer defines the external parser configured in ini
type MarkupRenderer struct {
- Enabled bool
- MarkupName string
- Command string
- FileExtensions []string
- IsInputFile bool
- NeedPostProcess bool
+ Enabled bool
+ MarkupName string
+ Command string
+ FileExtensions []string
+ IsInputFile bool
+ NeedPostProcess bool
+ MarkupSanitizerRules []MarkupSanitizerRule
}
// MarkupSanitizerRule defines the policy for whitelisting attributes on
// certain elements.
type MarkupSanitizerRule struct {
- Element string
- AllowAttr string
- Regexp *regexp.Regexp
+ Element string
+ AllowAttr string
+ Regexp *regexp.Regexp
+ AllowDataURIImages bool
}
func newMarkup() {
- ExternalMarkupRenderers = make([]MarkupRenderer, 0, 10)
+ ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10)
ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10)
+
for _, sec := range Cfg.Section("markup").ChildSections() {
name := strings.TrimPrefix(sec.Name(), "markup.")
if name == "" {
@@ -56,50 +59,62 @@ func newMarkup() {
}
func newMarkupSanitizer(name string, sec *ini.Section) {
- haveElement := sec.HasKey("ELEMENT")
- haveAttr := sec.HasKey("ALLOW_ATTR")
- haveRegexp := sec.HasKey("REGEXP")
+ rule, ok := createMarkupSanitizerRule(name, sec)
+ if ok {
+ if strings.HasPrefix(name, "sanitizer.") {
+ names := strings.SplitN(strings.TrimPrefix(name, "sanitizer."), ".", 2)
+ name = names[0]
+ }
+ for _, renderer := range ExternalMarkupRenderers {
+ if name == renderer.MarkupName {
+ renderer.MarkupSanitizerRules = append(renderer.MarkupSanitizerRules, rule)
+ return
+ }
+ }
+ ExternalSanitizerRules = append(ExternalSanitizerRules, rule)
+ }
+}
- if !haveElement && !haveAttr && !haveRegexp {
- log.Warn("Skipping empty section: markup.%s.", name)
- return
+func createMarkupSanitizerRule(name string, sec *ini.Section) (MarkupSanitizerRule, bool) {
+ var rule MarkupSanitizerRule
+
+ ok := false
+ if sec.HasKey("ALLOW_DATA_URI_IMAGES") {
+ rule.AllowDataURIImages = sec.Key("ALLOW_DATA_URI_IMAGES").MustBool(false)
+ ok = true
}
- if !haveElement || !haveAttr || !haveRegexp {
- log.Error("Missing required keys from markup.%s. Must have all three of ELEMENT, ALLOW_ATTR, and REGEXP defined!", name)
- return
- }
+ if sec.HasKey("ELEMENT") || sec.HasKey("ALLOW_ATTR") {
+ rule.Element = sec.Key("ELEMENT").Value()
+ rule.AllowAttr = sec.Key("ALLOW_ATTR").Value()
- elements := sec.Key("ELEMENT").Value()
- allowAttrs := sec.Key("ALLOW_ATTR").Value()
- regexpStr := sec.Key("REGEXP").Value()
-
- if regexpStr == "" {
- rule := MarkupSanitizerRule{
- Element: elements,
- AllowAttr: allowAttrs,
- Regexp: nil,
+ if rule.Element == "" || rule.AllowAttr == "" {
+ log.Error("Missing required values from markup.%s. Must have ELEMENT and ALLOW_ATTR defined!", name)
+ return rule, false
}
- ExternalSanitizerRules = append(ExternalSanitizerRules, rule)
- return
+ regexpStr := sec.Key("REGEXP").Value()
+ if regexpStr != "" {
+ // Validate when parsing the config that this is a valid regular
+ // expression. Then we can use regexp.MustCompile(...) later.
+ compiled, err := regexp.Compile(regexpStr)
+ if err != nil {
+ log.Error("In markup.%s: REGEXP (%s) failed to compile: %v", name, regexpStr, err)
+ return rule, false
+ }
+
+ rule.Regexp = compiled
+ }
+
+ ok = true
}
- // Validate when parsing the config that this is a valid regular
- // expression. Then we can use regexp.MustCompile(...) later.
- compiled, err := regexp.Compile(regexpStr)
- if err != nil {
- log.Error("In module.%s: REGEXP (%s) at definition %d failed to compile: %v", regexpStr, name, err)
- return
+ if !ok {
+ log.Error("Missing required keys from markup.%s. Must have ELEMENT and ALLOW_ATTR or ALLOW_DATA_URI_IMAGES defined!", name)
+ return rule, false
}
- rule := MarkupSanitizerRule{
- Element: elements,
- AllowAttr: allowAttrs,
- Regexp: compiled,
- }
-
- ExternalSanitizerRules = append(ExternalSanitizerRules, rule)
+ return rule, true
}
func newMarkupRenderer(name string, sec *ini.Section) {
@@ -126,7 +141,7 @@ func newMarkupRenderer(name string, sec *ini.Section) {
return
}
- ExternalMarkupRenderers = append(ExternalMarkupRenderers, MarkupRenderer{
+ ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
Enabled: sec.Key("ENABLED").MustBool(false),
MarkupName: name,
FileExtensions: exts,
From b223d361955f8b722f7dd0b358b2e57e6f359edf Mon Sep 17 00:00:00 2001
From: Lunny Xiao
Date: Thu, 24 Jun 2021 05:12:38 +0800
Subject: [PATCH 09/33] Rework repository archive (#14723)
* Use storage to store archive files
* Fix backend lint
* Add archiver table on database
* Finish archive download
* Fix test
* Add database migrations
* Add status for archiver
* Fix lint
* Add queue
* Add doctor to check and delete old archives
* Improve archive queue
* Fix tests
* improve archive storage
* Delete repo archives
* Add missing fixture
* fix fixture
* Fix fixture
* Fix test
* Fix archiver cleaning
* Fix bug
* Add docs for repository archive storage
* remove repo-archive configuration
* Fix test
* Fix test
* Fix lint
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: techknowlogick
---
custom/conf/app.example.ini | 10 +
.../doc/advanced/config-cheat-sheet.en-us.md | 17 +
.../doc/advanced/config-cheat-sheet.zh-cn.md | 15 +
.../user27/repo49.git/refs/heads/test/archive | 1 +
models/fixtures/repo_archiver.yml | 1 +
models/migrations/migrations.go | 2 +
models/migrations/v181.go | 1 +
models/migrations/v185.go | 22 +
models/models.go | 1 +
models/repo.go | 97 ++--
models/repo_archiver.go | 86 ++++
models/unit_tests.go | 2 +
modules/context/context.go | 15 +
modules/doctor/checkOldArchives.go | 59 +++
.../{commit_archive.go => repo_archive.go} | 31 +-
modules/setting/repository.go | 6 +
modules/setting/storage.go | 4 +
modules/storage/storage.go | 15 +-
routers/api/v1/repo/file.go | 3 +-
routers/common/repo.go | 26 --
routers/init.go | 4 +
routers/web/repo/repo.go | 122 ++++-
routers/web/web.go | 3 +-
services/archiver/archiver.go | 428 ++++++++----------
services/archiver/archiver_test.go | 157 ++-----
25 files changed, 648 insertions(+), 480 deletions(-)
create mode 100644 integrations/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive
create mode 100644 models/fixtures/repo_archiver.yml
create mode 100644 models/migrations/v185.go
create mode 100644 models/repo_archiver.go
create mode 100644 modules/doctor/checkOldArchives.go
rename modules/git/{commit_archive.go => repo_archive.go} (60%)
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 38a27509f7..5adfb0546f 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -2048,6 +2048,16 @@ PATH =
;; storage type
;STORAGE_TYPE = local
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; settings for repository archives, will override storage setting
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;[storage.repo-archive]
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; storage type
+;STORAGE_TYPE = local
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; lfs storage will override storage
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 8f1f9ce42d..5e976174fb 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -995,6 +995,23 @@ MINIO_USE_SSL = false
And used by `[attachment]`, `[lfs]` and etc. as `STORAGE_TYPE`.
+## Repository Archive Storage (`storage.repo-archive`)
+
+Configuration for repository archive storage. It will inherit from default `[storage]` or
+`[storage.xxx]` when set `STORAGE_TYPE` to `xxx`. The default of `PATH`
+is `data/repo-archive` and the default of `MINIO_BASE_PATH` is `repo-archive/`.
+
+- `STORAGE_TYPE`: **local**: Storage type for repo archive, `local` for local disk or `minio` for s3 compatible object storage service or other name defined with `[storage.xxx]`
+- `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
+- `PATH`: **./data/repo-archive**: Where to store archive files, only available when `STORAGE_TYPE` is `local`.
+- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio`
+- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio`
+- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `STORAGE_TYPE is` `minio`
+- `MINIO_BUCKET`: **gitea**: Minio bucket to store the lfs only available when `STORAGE_TYPE` is `minio`
+- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio`
+- `MINIO_BASE_PATH`: **repo-archive/**: Minio base path on the bucket only available when `STORAGE_TYPE` is `minio`
+- `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio`
+
## Other (`other`)
- `SHOW_FOOTER_BRANDING`: **false**: Show Gitea branding in the footer.
diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
index 79cfd94cc7..2303a631d5 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
@@ -382,6 +382,21 @@ MINIO_USE_SSL = false
然后你在 `[attachment]`, `[lfs]` 等中可以把这个名字用作 `STORAGE_TYPE` 的值。
+## Repository Archive Storage (`storage.repo-archive`)
+
+Repository archive 的存储配置。 如果 `STORAGE_TYPE` 为空,则此配置将从 `[storage]` 继承。如果不为 `local` 或者 `minio` 而为 `xxx`, 则从 `[storage.xxx]` 继承。当继承时, `PATH` 默认为 `data/repo-archive`,`MINIO_BASE_PATH` 默认为 `repo-archive/`。
+
+- `STORAGE_TYPE`: **local**: Repository archive 的存储类型,`local` 将存储到磁盘,`minio` 将存储到 s3 兼容的对象服务。
+- `SERVE_DIRECT`: **false**: 允许直接重定向到存储系统。当前,仅 Minio/S3 是支持的。
+- `PATH`: 存放 Repository archive 上传的文件的地方,默认是 `data/repo-archive`。
+- `MINIO_ENDPOINT`: **localhost:9000**: Minio 地址,仅当 `STORAGE_TYPE` 为 `minio` 时有效。
+- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID,仅当 `STORAGE_TYPE` 为 `minio` 时有效。
+- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `STORAGE_TYPE` 为 `minio` 时有效。
+- `MINIO_BUCKET`: **gitea**: Minio bucket,仅当 `STORAGE_TYPE` 为 `minio` 时有效。
+- `MINIO_LOCATION`: **us-east-1**: Minio location ,仅当 `STORAGE_TYPE` 为 `minio` 时有效。
+- `MINIO_BASE_PATH`: **repo-archive/**: Minio base path ,仅当 `STORAGE_TYPE` 为 `minio` 时有效。
+- `MINIO_USE_SSL`: **false**: Minio 是否启用 ssl ,仅当 `STORAGE_TYPE` 为 `minio` 时有效。
+
## Other (`other`)
- `SHOW_FOOTER_BRANDING`: 为真则在页面底部显示Gitea的字样。
diff --git a/integrations/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive b/integrations/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive
new file mode 100644
index 0000000000..0f13243bfd
--- /dev/null
+++ b/integrations/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive
@@ -0,0 +1 @@
+aacbdfe9e1c4b47f60abe81849045fa4e96f1d75
diff --git a/models/fixtures/repo_archiver.yml b/models/fixtures/repo_archiver.yml
new file mode 100644
index 0000000000..ca780a73aa
--- /dev/null
+++ b/models/fixtures/repo_archiver.yml
@@ -0,0 +1 @@
+[] # empty
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 880f55092d..4e17a6a2c8 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -319,6 +319,8 @@ var migrations = []Migration{
NewMigration("Create PushMirror table", createPushMirrorTable),
// v184 -> v185
NewMigration("Rename Task errors to message", renameTaskErrorsToMessage),
+ // v185 -> v186
+ NewMigration("Add new table repo_archiver", addRepoArchiver),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v181.go b/models/migrations/v181.go
index 6ba4edc155..65045593ad 100644
--- a/models/migrations/v181.go
+++ b/models/migrations/v181.go
@@ -1,3 +1,4 @@
+// Copyright 2021 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.
diff --git a/models/migrations/v185.go b/models/migrations/v185.go
new file mode 100644
index 0000000000..0969948897
--- /dev/null
+++ b/models/migrations/v185.go
@@ -0,0 +1,22 @@
+// Copyright 2021 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 (
+ "xorm.io/xorm"
+)
+
+func addRepoArchiver(x *xorm.Engine) error {
+ // RepoArchiver represents all archivers
+ type RepoArchiver struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"index unique(s)"`
+ Type int `xorm:"unique(s)"`
+ Status int
+ CommitID string `xorm:"VARCHAR(40) unique(s)"`
+ CreatedUnix int64 `xorm:"INDEX NOT NULL created"`
+ }
+ return x.Sync2(new(RepoArchiver))
+}
diff --git a/models/models.go b/models/models.go
index c325fd3811..3266be0f4a 100644
--- a/models/models.go
+++ b/models/models.go
@@ -136,6 +136,7 @@ func init() {
new(RepoTransfer),
new(IssueIndex),
new(PushMirror),
+ new(RepoArchiver),
)
gonicNames := []string{"SSL", "UID"}
diff --git a/models/repo.go b/models/repo.go
index dc4e03a28a..2baf6e9bdd 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -1587,6 +1587,22 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
return err
}
+ // Remove archives
+ var archives []*RepoArchiver
+ if err = sess.Where("repo_id=?", repoID).Find(&archives); err != nil {
+ return err
+ }
+
+ for _, v := range archives {
+ v.Repo = repo
+ p, _ := v.RelativePath()
+ removeStorageWithNotice(sess, storage.RepoArchives, "Delete repo archive file", p)
+ }
+
+ if _, err := sess.Delete(&RepoArchiver{RepoID: repoID}); err != nil {
+ return err
+ }
+
if repo.NumForks > 0 {
if _, err = sess.Exec("UPDATE `repository` SET fork_id=0,is_fork=? WHERE fork_id=?", false, repo.ID); err != nil {
log.Error("reset 'fork_id' and 'is_fork': %v", err)
@@ -1768,64 +1784,45 @@ func DeleteRepositoryArchives(ctx context.Context) error {
func DeleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration) error {
log.Trace("Doing: ArchiveCleanup")
- if err := x.Where("id > 0").Iterate(new(Repository), func(idx int, bean interface{}) error {
- return deleteOldRepositoryArchives(ctx, olderThan, idx, bean)
- }); err != nil {
- log.Trace("Error: ArchiveClean: %v", err)
- return err
+ for {
+ var archivers []RepoArchiver
+ err := x.Where("created_unix < ?", time.Now().Add(-olderThan).Unix()).
+ Asc("created_unix").
+ Limit(100).
+ Find(&archivers)
+ if err != nil {
+ log.Trace("Error: ArchiveClean: %v", err)
+ return err
+ }
+
+ for _, archiver := range archivers {
+ if err := deleteOldRepoArchiver(ctx, &archiver); err != nil {
+ return err
+ }
+ }
+ if len(archivers) < 100 {
+ break
+ }
}
log.Trace("Finished: ArchiveCleanup")
return nil
}
-func deleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration, idx int, bean interface{}) error {
- repo := bean.(*Repository)
- basePath := filepath.Join(repo.RepoPath(), "archives")
+var delRepoArchiver = new(RepoArchiver)
- for _, ty := range []string{"zip", "targz"} {
- select {
- case <-ctx.Done():
- return ErrCancelledf("before deleting old repository archives with filetype %s for %s", ty, repo.FullName())
- default:
- }
-
- path := filepath.Join(basePath, ty)
- file, err := os.Open(path)
- if err != nil {
- if !os.IsNotExist(err) {
- log.Warn("Unable to open directory %s: %v", path, err)
- return err
- }
-
- // If the directory doesn't exist, that's okay.
- continue
- }
-
- files, err := file.Readdir(0)
- file.Close()
- if err != nil {
- log.Warn("Unable to read directory %s: %v", path, err)
- return err
- }
-
- minimumOldestTime := time.Now().Add(-olderThan)
- for _, info := range files {
- if info.ModTime().Before(minimumOldestTime) && !info.IsDir() {
- select {
- case <-ctx.Done():
- return ErrCancelledf("before deleting old repository archive file %s with filetype %s for %s", info.Name(), ty, repo.FullName())
- default:
- }
- toDelete := filepath.Join(path, info.Name())
- // This is a best-effort purge, so we do not check error codes to confirm removal.
- if err = util.Remove(toDelete); err != nil {
- log.Trace("Unable to delete %s, but proceeding: %v", toDelete, err)
- }
- }
- }
+func deleteOldRepoArchiver(ctx context.Context, archiver *RepoArchiver) error {
+ p, err := archiver.RelativePath()
+ if err != nil {
+ return err
+ }
+ _, err = x.ID(archiver.ID).Delete(delRepoArchiver)
+ if err != nil {
+ return err
+ }
+ if err := storage.RepoArchives.Delete(p); err != nil {
+ log.Error("delete repo archive file failed: %v", err)
}
-
return nil
}
diff --git a/models/repo_archiver.go b/models/repo_archiver.go
new file mode 100644
index 0000000000..833a22ee13
--- /dev/null
+++ b/models/repo_archiver.go
@@ -0,0 +1,86 @@
+// Copyright 2021 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 models
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/timeutil"
+)
+
+// RepoArchiverStatus represents repo archive status
+type RepoArchiverStatus int
+
+// enumerate all repo archive statuses
+const (
+ RepoArchiverGenerating = iota // the archiver is generating
+ RepoArchiverReady // it's ready
+)
+
+// RepoArchiver represents all archivers
+type RepoArchiver struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"index unique(s)"`
+ Repo *Repository `xorm:"-"`
+ Type git.ArchiveType `xorm:"unique(s)"`
+ Status RepoArchiverStatus
+ CommitID string `xorm:"VARCHAR(40) unique(s)"`
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"`
+}
+
+// LoadRepo loads repository
+func (archiver *RepoArchiver) LoadRepo() (*Repository, error) {
+ if archiver.Repo != nil {
+ return archiver.Repo, nil
+ }
+
+ var repo Repository
+ has, err := x.ID(archiver.RepoID).Get(&repo)
+ if err != nil {
+ return nil, err
+ }
+ if !has {
+ return nil, ErrRepoNotExist{
+ ID: archiver.RepoID,
+ }
+ }
+ return &repo, nil
+}
+
+// RelativePath returns relative path
+func (archiver *RepoArchiver) RelativePath() (string, error) {
+ repo, err := archiver.LoadRepo()
+ if err != nil {
+ return "", err
+ }
+
+ return fmt.Sprintf("%s/%s/%s.%s", repo.FullName(), archiver.CommitID[:2], archiver.CommitID, archiver.Type.String()), nil
+}
+
+// GetRepoArchiver get an archiver
+func GetRepoArchiver(ctx DBContext, repoID int64, tp git.ArchiveType, commitID string) (*RepoArchiver, error) {
+ var archiver RepoArchiver
+ has, err := ctx.e.Where("repo_id=?", repoID).And("`type`=?", tp).And("commit_id=?", commitID).Get(&archiver)
+ if err != nil {
+ return nil, err
+ }
+ if has {
+ return &archiver, nil
+ }
+ return nil, nil
+}
+
+// AddRepoArchiver adds an archiver
+func AddRepoArchiver(ctx DBContext, archiver *RepoArchiver) error {
+ _, err := ctx.e.Insert(archiver)
+ return err
+}
+
+// UpdateRepoArchiverStatus updates archiver's status
+func UpdateRepoArchiverStatus(ctx DBContext, archiver *RepoArchiver) error {
+ _, err := ctx.e.ID(archiver.ID).Cols("status").Update(archiver)
+ return err
+}
diff --git a/models/unit_tests.go b/models/unit_tests.go
index 5a145fa2c0..f8d6819333 100644
--- a/models/unit_tests.go
+++ b/models/unit_tests.go
@@ -74,6 +74,8 @@ func MainTest(m *testing.M, pathToGiteaRoot string) {
setting.RepoAvatar.Storage.Path = filepath.Join(setting.AppDataPath, "repo-avatars")
+ setting.RepoArchive.Storage.Path = filepath.Join(setting.AppDataPath, "repo-archive")
+
if err = storage.Init(); err != nil {
fatalTestError("storage.Init: %v\n", err)
}
diff --git a/modules/context/context.go b/modules/context/context.go
index 7b3fd2899a..64f8b12084 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -380,6 +380,21 @@ func (ctx *Context) ServeFile(file string, names ...string) {
http.ServeFile(ctx.Resp, ctx.Req, file)
}
+// ServeStream serves file via io stream
+func (ctx *Context) ServeStream(rd io.Reader, name string) {
+ ctx.Resp.Header().Set("Content-Description", "File Transfer")
+ ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
+ ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+name)
+ ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary")
+ ctx.Resp.Header().Set("Expires", "0")
+ ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
+ ctx.Resp.Header().Set("Pragma", "public")
+ _, err := io.Copy(ctx.Resp, rd)
+ if err != nil {
+ ctx.ServerError("Download file failed", err)
+ }
+}
+
// Error returned an error to web browser
func (ctx *Context) Error(status int, contents ...string) {
var v = http.StatusText(status)
diff --git a/modules/doctor/checkOldArchives.go b/modules/doctor/checkOldArchives.go
new file mode 100644
index 0000000000..a4e2ffbd1f
--- /dev/null
+++ b/modules/doctor/checkOldArchives.go
@@ -0,0 +1,59 @@
+// Copyright 2021 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 doctor
+
+import (
+ "os"
+ "path/filepath"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
+)
+
+func checkOldArchives(logger log.Logger, autofix bool) error {
+ numRepos := 0
+ numReposUpdated := 0
+ err := iterateRepositories(func(repo *models.Repository) error {
+ if repo.IsEmpty {
+ return nil
+ }
+
+ p := filepath.Join(repo.RepoPath(), "archives")
+ isDir, err := util.IsDir(p)
+ if err != nil {
+ log.Warn("check if %s is directory failed: %v", p, err)
+ }
+ if isDir {
+ numRepos++
+ if autofix {
+ if err := os.RemoveAll(p); err == nil {
+ numReposUpdated++
+ } else {
+ log.Warn("remove %s failed: %v", p, err)
+ }
+ }
+ }
+ return nil
+ })
+
+ if autofix {
+ logger.Info("%d / %d old archives in repository deleted", numReposUpdated, numRepos)
+ } else {
+ logger.Info("%d old archives in repository need to be deleted", numRepos)
+ }
+
+ return err
+}
+
+func init() {
+ Register(&Check{
+ Title: "Check old archives",
+ Name: "check-old-archives",
+ IsDefault: false,
+ Run: checkOldArchives,
+ Priority: 7,
+ })
+}
diff --git a/modules/git/commit_archive.go b/modules/git/repo_archive.go
similarity index 60%
rename from modules/git/commit_archive.go
rename to modules/git/repo_archive.go
index d075ba0911..07003aa6b2 100644
--- a/modules/git/commit_archive.go
+++ b/modules/git/repo_archive.go
@@ -8,6 +8,7 @@ package git
import (
"context"
"fmt"
+ "io"
"path/filepath"
"strings"
)
@@ -33,32 +34,28 @@ func (a ArchiveType) String() string {
return "unknown"
}
-// CreateArchiveOpts represents options for creating an archive
-type CreateArchiveOpts struct {
- Format ArchiveType
- Prefix bool
-}
-
// CreateArchive create archive content to the target path
-func (c *Commit) CreateArchive(ctx context.Context, target string, opts CreateArchiveOpts) error {
- if opts.Format.String() == "unknown" {
- return fmt.Errorf("unknown format: %v", opts.Format)
+func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, target io.Writer, usePrefix bool, commitID string) error {
+ if format.String() == "unknown" {
+ return fmt.Errorf("unknown format: %v", format)
}
args := []string{
"archive",
}
- if opts.Prefix {
- args = append(args, "--prefix="+filepath.Base(strings.TrimSuffix(c.repo.Path, ".git"))+"/")
+ if usePrefix {
+ args = append(args, "--prefix="+filepath.Base(strings.TrimSuffix(repo.Path, ".git"))+"/")
}
args = append(args,
- "--format="+opts.Format.String(),
- "-o",
- target,
- c.ID.String(),
+ "--format="+format.String(),
+ commitID,
)
- _, err := NewCommandContext(ctx, args...).RunInDir(c.repo.Path)
- return err
+ var stderr strings.Builder
+ err := NewCommandContext(ctx, args...).RunInDirPipeline(repo.Path, target, &stderr)
+ if err != nil {
+ return ConcatenateError(err, stderr.String())
+ }
+ return nil
}
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index a7666895e1..c2a6357d94 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -251,6 +251,10 @@ var (
}
RepoRootPath string
ScriptType = "bash"
+
+ RepoArchive = struct {
+ Storage
+ }{}
)
func newRepository() {
@@ -328,4 +332,6 @@ func newRepository() {
if !filepath.IsAbs(Repository.Upload.TempPath) {
Repository.Upload.TempPath = path.Join(AppWorkPath, Repository.Upload.TempPath)
}
+
+ RepoArchive.Storage = getStorage("repo-archive", "", nil)
}
diff --git a/modules/setting/storage.go b/modules/setting/storage.go
index 3ab08d8d2a..075152db59 100644
--- a/modules/setting/storage.go
+++ b/modules/setting/storage.go
@@ -43,6 +43,10 @@ func getStorage(name, typ string, targetSec *ini.Section) Storage {
sec.Key("MINIO_LOCATION").MustString("us-east-1")
sec.Key("MINIO_USE_SSL").MustBool(false)
+ if targetSec == nil {
+ targetSec, _ = Cfg.NewSection(name)
+ }
+
var storage Storage
storage.Section = targetSec
storage.Type = typ
diff --git a/modules/storage/storage.go b/modules/storage/storage.go
index 984f154db4..b3708908f8 100644
--- a/modules/storage/storage.go
+++ b/modules/storage/storage.go
@@ -114,6 +114,9 @@ var (
Avatars ObjectStorage
// RepoAvatars represents repository avatars storage
RepoAvatars ObjectStorage
+
+ // RepoArchives represents repository archives storage
+ RepoArchives ObjectStorage
)
// Init init the stoarge
@@ -130,7 +133,11 @@ func Init() error {
return err
}
- return initLFS()
+ if err := initLFS(); err != nil {
+ return err
+ }
+
+ return initRepoArchives()
}
// NewStorage takes a storage type and some config and returns an ObjectStorage or an error
@@ -169,3 +176,9 @@ func initRepoAvatars() (err error) {
RepoAvatars, err = NewStorage(setting.RepoAvatar.Storage.Type, &setting.RepoAvatar.Storage)
return
}
+
+func initRepoArchives() (err error) {
+ log.Info("Initialising Repository Archive storage with type: %s", setting.RepoArchive.Storage.Type)
+ RepoArchives, err = NewStorage(setting.RepoArchive.Storage.Type, &setting.RepoArchive.Storage)
+ return
+}
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 39a60df33f..e6427ea4f4 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -18,6 +18,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/routers/web/repo"
)
// GetRawFile get a file by path on a repository
@@ -126,7 +127,7 @@ func GetArchive(ctx *context.APIContext) {
ctx.Repo.GitRepo = gitRepo
defer gitRepo.Close()
- common.Download(ctx.Context)
+ repo.Download(ctx.Context)
}
// GetEditorconfig get editor config of a repository
diff --git a/routers/common/repo.go b/routers/common/repo.go
index c61b5ec57f..22403da097 100644
--- a/routers/common/repo.go
+++ b/routers/common/repo.go
@@ -7,7 +7,6 @@ package common
import (
"fmt"
"io"
- "net/http"
"path"
"path/filepath"
"strings"
@@ -19,7 +18,6 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/services/archiver"
)
// ServeBlob download a git.Blob
@@ -41,30 +39,6 @@ func ServeBlob(ctx *context.Context, blob *git.Blob) error {
return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc)
}
-// Download an archive of a repository
-func Download(ctx *context.Context) {
- uri := ctx.Params("*")
- aReq := archiver.DeriveRequestFrom(ctx, uri)
-
- if aReq == nil {
- ctx.Error(http.StatusNotFound)
- return
- }
-
- downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName()
- complete := aReq.IsComplete()
- if !complete {
- aReq = archiver.ArchiveRepository(aReq)
- complete = aReq.WaitForCompletion(ctx)
- }
-
- if complete {
- ctx.ServeFile(aReq.GetArchivePath(), downloadName)
- } else {
- ctx.Error(http.StatusNotFound)
- }
-}
-
// ServeData download file from io.Reader
func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error {
buf := make([]byte, 1024)
diff --git a/routers/init.go b/routers/init.go
index 4c28a95395..bbf39a3f50 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -33,6 +33,7 @@ import (
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/private"
web_routers "code.gitea.io/gitea/routers/web"
+ "code.gitea.io/gitea/services/archiver"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/mailer"
mirror_service "code.gitea.io/gitea/services/mirror"
@@ -63,6 +64,9 @@ func NewServices() {
mailer.NewContext()
_ = cache.NewContext()
notification.NewContext()
+ if err := archiver.Init(); err != nil {
+ log.Fatal("archiver init failed: %v", err)
+ }
}
// GlobalInit is for global configuration reload-able.
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index f149e92a8b..919fd4620d 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -15,8 +15,10 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/web"
archiver_service "code.gitea.io/gitea/services/archiver"
"code.gitea.io/gitea/services/forms"
@@ -364,25 +366,123 @@ func RedirectDownload(ctx *context.Context) {
ctx.Error(http.StatusNotFound)
}
-// InitiateDownload will enqueue an archival request, as needed. It may submit
-// a request that's already in-progress, but the archiver service will just
-// kind of drop it on the floor if this is the case.
-func InitiateDownload(ctx *context.Context) {
+// Download an archive of a repository
+func Download(ctx *context.Context) {
uri := ctx.Params("*")
- aReq := archiver_service.DeriveRequestFrom(ctx, uri)
-
+ aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
+ if err != nil {
+ ctx.ServerError("archiver_service.NewRequest", err)
+ return
+ }
if aReq == nil {
ctx.Error(http.StatusNotFound)
return
}
- complete := aReq.IsComplete()
- if !complete {
- aReq = archiver_service.ArchiveRepository(aReq)
- complete, _ = aReq.TimedWaitForCompletion(ctx, 2*time.Second)
+ archiver, err := models.GetRepoArchiver(models.DefaultDBContext(), aReq.RepoID, aReq.Type, aReq.CommitID)
+ if err != nil {
+ ctx.ServerError("models.GetRepoArchiver", err)
+ return
+ }
+ if archiver != nil && archiver.Status == models.RepoArchiverReady {
+ download(ctx, aReq.GetArchiveName(), archiver)
+ return
+ }
+
+ if err := archiver_service.StartArchive(aReq); err != nil {
+ ctx.ServerError("archiver_service.StartArchive", err)
+ return
+ }
+
+ var times int
+ var 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 = models.GetRepoArchiver(models.DefaultDBContext(), aReq.RepoID, aReq.Type, aReq.CommitID)
+ if err != nil {
+ ctx.ServerError("archiver_service.StartArchive", err)
+ return
+ }
+ if archiver != nil && archiver.Status == models.RepoArchiverReady {
+ download(ctx, aReq.GetArchiveName(), archiver)
+ return
+ }
+ }
+ }
+}
+
+func download(ctx *context.Context, archiveName string, archiver *models.RepoArchiver) {
+ downloadName := ctx.Repo.Repository.Name + "-" + archiveName
+
+ rPath, err := archiver.RelativePath()
+ if err != nil {
+ ctx.ServerError("archiver.RelativePath", err)
+ return
+ }
+
+ 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.ServeStream(fr, downloadName)
+}
+
+// InitiateDownload will enqueue an archival request, as needed. It may submit
+// a request that's already in-progress, but the archiver service will just
+// kind of drop it on the floor if this is the case.
+func InitiateDownload(ctx *context.Context) {
+ uri := ctx.Params("*")
+ aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
+ if err != nil {
+ ctx.ServerError("archiver_service.NewRequest", err)
+ return
+ }
+ if aReq == nil {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+
+ archiver, err := models.GetRepoArchiver(models.DefaultDBContext(), aReq.RepoID, aReq.Type, aReq.CommitID)
+ if err != nil {
+ ctx.ServerError("archiver_service.StartArchive", err)
+ return
+ }
+ if archiver == nil || archiver.Status != models.RepoArchiverReady {
+ if err := archiver_service.StartArchive(aReq); err != nil {
+ ctx.ServerError("archiver_service.StartArchive", err)
+ return
+ }
+ }
+
+ var completed bool
+ if archiver != nil && archiver.Status == models.RepoArchiverReady {
+ completed = true
}
ctx.JSON(http.StatusOK, map[string]interface{}{
- "complete": complete,
+ "complete": completed,
})
}
diff --git a/routers/web/web.go b/routers/web/web.go
index 2c8a6411a1..883213479c 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -22,7 +22,6 @@ import (
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/misc"
- "code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/web/admin"
"code.gitea.io/gitea/routers/web/dev"
"code.gitea.io/gitea/routers/web/events"
@@ -888,7 +887,7 @@ func RegisterRoutes(m *web.Route) {
}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypeCode))
m.Group("/archive", func() {
- m.Get("/*", common.Download)
+ m.Get("/*", repo.Download)
m.Post("/*", repo.InitiateDownload)
}, repo.MustBeNotEmpty, reqRepoCodeReader)
diff --git a/services/archiver/archiver.go b/services/archiver/archiver.go
index dfa6334d95..00c0281306 100644
--- a/services/archiver/archiver.go
+++ b/services/archiver/archiver.go
@@ -6,22 +6,20 @@
package archiver
import (
+ "errors"
+ "fmt"
"io"
- "io/ioutil"
"os"
- "path"
"regexp"
"strings"
- "sync"
- "time"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/storage"
)
// ArchiveRequest defines the parameters of an archive request, which notably
@@ -30,223 +28,174 @@ import (
// This is entirely opaque to external entities, though, and mostly used as a
// handle elsewhere.
type ArchiveRequest struct {
- uri string
- repo *git.Repository
- refName string
- ext string
- archivePath string
- archiveType git.ArchiveType
- archiveComplete bool
- commit *git.Commit
- cchan chan struct{}
+ RepoID int64
+ refName string
+ Type git.ArchiveType
+ CommitID string
}
-var archiveInProgress []*ArchiveRequest
-var archiveMutex sync.Mutex
-
// SHA1 hashes will only go up to 40 characters, but SHA256 hashes will go all
// the way to 64.
var shaRegex = regexp.MustCompile(`^[0-9a-f]{4,64}$`)
-// These facilitate testing, by allowing the unit tests to control (to some extent)
-// the goroutine used for processing the queue.
-var archiveQueueMutex *sync.Mutex
-var archiveQueueStartCond *sync.Cond
-var archiveQueueReleaseCond *sync.Cond
+// NewRequest creates an archival request, based on the URI. The
+// resulting ArchiveRequest is suitable for being passed to ArchiveRepository()
+// if it's determined that the request still needs to be satisfied.
+func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest, error) {
+ r := &ArchiveRequest{
+ RepoID: repoID,
+ }
-// GetArchivePath returns the path from which we can serve this archive.
-func (aReq *ArchiveRequest) GetArchivePath() string {
- return aReq.archivePath
+ var ext string
+ switch {
+ case strings.HasSuffix(uri, ".zip"):
+ ext = ".zip"
+ r.Type = git.ZIP
+ case strings.HasSuffix(uri, ".tar.gz"):
+ ext = ".tar.gz"
+ r.Type = git.TARGZ
+ default:
+ return nil, fmt.Errorf("Unknown format: %s", uri)
+ }
+
+ r.refName = strings.TrimSuffix(uri, ext)
+
+ var err error
+ // Get corresponding commit.
+ if repo.IsBranchExist(r.refName) {
+ r.CommitID, err = repo.GetBranchCommitID(r.refName)
+ if err != nil {
+ return nil, err
+ }
+ } else if repo.IsTagExist(r.refName) {
+ r.CommitID, err = repo.GetTagCommitID(r.refName)
+ if err != nil {
+ return nil, err
+ }
+ } else if shaRegex.MatchString(r.refName) {
+ if repo.IsCommitExist(r.refName) {
+ r.CommitID = r.refName
+ } else {
+ return nil, git.ErrNotExist{
+ ID: r.refName,
+ }
+ }
+ } else {
+ return nil, fmt.Errorf("Unknow ref %s type", r.refName)
+ }
+
+ return r, nil
}
// GetArchiveName returns the name of the caller, based on the ref used by the
// caller to create this request.
func (aReq *ArchiveRequest) GetArchiveName() string {
- return aReq.refName + aReq.ext
+ return strings.ReplaceAll(aReq.refName, "/", "-") + "." + aReq.Type.String()
}
-// IsComplete returns the completion status of this request.
-func (aReq *ArchiveRequest) IsComplete() bool {
- return aReq.archiveComplete
-}
-
-// WaitForCompletion will wait for this request to complete, with no timeout.
-// It returns whether the archive was actually completed, as the channel could
-// have also been closed due to an error.
-func (aReq *ArchiveRequest) WaitForCompletion(ctx *context.Context) bool {
- select {
- case <-aReq.cchan:
- case <-ctx.Done():
- }
-
- return aReq.IsComplete()
-}
-
-// TimedWaitForCompletion will wait for this request to complete, with timeout
-// happening after the specified Duration. It returns whether the archive is
-// now complete and whether we hit the timeout or not. The latter may not be
-// useful if the request is complete or we started to shutdown.
-func (aReq *ArchiveRequest) TimedWaitForCompletion(ctx *context.Context, dur time.Duration) (bool, bool) {
- timeout := false
- select {
- case <-time.After(dur):
- timeout = true
- case <-aReq.cchan:
- case <-ctx.Done():
- }
-
- return aReq.IsComplete(), timeout
-}
-
-// The caller must hold the archiveMutex across calls to getArchiveRequest.
-func getArchiveRequest(repo *git.Repository, commit *git.Commit, archiveType git.ArchiveType) *ArchiveRequest {
- for _, r := range archiveInProgress {
- // Need to be referring to the same repository.
- if r.repo.Path == repo.Path && r.commit.ID == commit.ID && r.archiveType == archiveType {
- return r
- }
- }
- return nil
-}
-
-// DeriveRequestFrom creates an archival request, based on the URI. The
-// resulting ArchiveRequest is suitable for being passed to ArchiveRepository()
-// if it's determined that the request still needs to be satisfied.
-func DeriveRequestFrom(ctx *context.Context, uri string) *ArchiveRequest {
- if ctx.Repo == nil || ctx.Repo.GitRepo == nil {
- log.Trace("Repo not initialized")
- return nil
- }
- r := &ArchiveRequest{
- uri: uri,
- repo: ctx.Repo.GitRepo,
- }
-
- switch {
- case strings.HasSuffix(uri, ".zip"):
- r.ext = ".zip"
- r.archivePath = path.Join(r.repo.Path, "archives/zip")
- r.archiveType = git.ZIP
- case strings.HasSuffix(uri, ".tar.gz"):
- r.ext = ".tar.gz"
- r.archivePath = path.Join(r.repo.Path, "archives/targz")
- r.archiveType = git.TARGZ
- default:
- log.Trace("Unknown format: %s", uri)
- return nil
- }
-
- r.refName = strings.TrimSuffix(r.uri, r.ext)
- isDir, err := util.IsDir(r.archivePath)
+func doArchive(r *ArchiveRequest) (*models.RepoArchiver, error) {
+ ctx, commiter, err := models.TxDBContext()
if err != nil {
- ctx.ServerError("Download -> util.IsDir(archivePath)", err)
- return nil
+ return nil, err
}
- if !isDir {
- if err := os.MkdirAll(r.archivePath, os.ModePerm); err != nil {
- ctx.ServerError("Download -> os.MkdirAll(archivePath)", err)
- return nil
- }
+ defer commiter.Close()
+
+ archiver, err := models.GetRepoArchiver(ctx, r.RepoID, r.Type, r.CommitID)
+ if err != nil {
+ return nil, err
}
- // Get corresponding commit.
- if r.repo.IsBranchExist(r.refName) {
- r.commit, err = r.repo.GetBranchCommit(r.refName)
- if err != nil {
- ctx.ServerError("GetBranchCommit", err)
- return nil
- }
- } else if r.repo.IsTagExist(r.refName) {
- r.commit, err = r.repo.GetTagCommit(r.refName)
- if err != nil {
- ctx.ServerError("GetTagCommit", err)
- return nil
- }
- } else if shaRegex.MatchString(r.refName) {
- r.commit, err = r.repo.GetCommit(r.refName)
- if err != nil {
- ctx.NotFound("GetCommit", nil)
- return nil
+ if archiver != nil {
+ // FIXME: If another process are generating it, we think it's not ready and just return
+ // Or we should wait until the archive generated.
+ if archiver.Status == models.RepoArchiverGenerating {
+ return nil, nil
}
} else {
- ctx.NotFound("DeriveRequestFrom", nil)
- return nil
+ archiver = &models.RepoArchiver{
+ RepoID: r.RepoID,
+ Type: r.Type,
+ CommitID: r.CommitID,
+ Status: models.RepoArchiverGenerating,
+ }
+ if err := models.AddRepoArchiver(ctx, archiver); err != nil {
+ return nil, err
+ }
}
- archiveMutex.Lock()
- defer archiveMutex.Unlock()
- if rExisting := getArchiveRequest(r.repo, r.commit, r.archiveType); rExisting != nil {
- return rExisting
- }
-
- r.archivePath = path.Join(r.archivePath, base.ShortSha(r.commit.ID.String())+r.ext)
- r.archiveComplete, err = util.IsFile(r.archivePath)
+ rPath, err := archiver.RelativePath()
if err != nil {
- ctx.ServerError("util.IsFile", err)
- return nil
- }
- return r
-}
-
-func doArchive(r *ArchiveRequest) {
- var (
- err error
- tmpArchive *os.File
- destArchive *os.File
- )
-
- // Close the channel to indicate to potential waiters that this request
- // has finished.
- defer close(r.cchan)
-
- // It could have happened that we enqueued two archival requests, due to
- // race conditions and difficulties in locking. Do one last check that
- // the archive we're referring to doesn't already exist. If it does exist,
- // then just mark the request as complete and move on.
- isFile, err := util.IsFile(r.archivePath)
- if err != nil {
- log.Error("Unable to check if %s util.IsFile: %v. Will ignore and recreate.", r.archivePath, err)
- }
- if isFile {
- r.archiveComplete = true
- return
+ return nil, err
}
- // Create a temporary file to use while the archive is being built. We
- // will then copy it into place (r.archivePath) once it's fully
- // constructed.
- tmpArchive, err = ioutil.TempFile("", "archive")
- if err != nil {
- log.Error("Unable to create a temporary archive file! Error: %v", err)
- return
+ _, err = storage.RepoArchives.Stat(rPath)
+ if err == nil {
+ if archiver.Status == models.RepoArchiverGenerating {
+ archiver.Status = models.RepoArchiverReady
+ return archiver, models.UpdateRepoArchiverStatus(ctx, archiver)
+ }
+ return archiver, nil
}
+
+ if !errors.Is(err, os.ErrNotExist) {
+ return nil, fmt.Errorf("unable to stat archive: %v", err)
+ }
+
+ rd, w := io.Pipe()
defer func() {
- tmpArchive.Close()
- os.Remove(tmpArchive.Name())
+ w.Close()
+ rd.Close()
}()
-
- if err = r.commit.CreateArchive(graceful.GetManager().ShutdownContext(), tmpArchive.Name(), git.CreateArchiveOpts{
- Format: r.archiveType,
- Prefix: setting.Repository.PrefixArchiveFiles,
- }); err != nil {
- log.Error("Download -> CreateArchive "+tmpArchive.Name(), err)
- return
- }
-
- // Now we copy it into place
- if destArchive, err = os.Create(r.archivePath); err != nil {
- log.Error("Unable to open archive " + r.archivePath)
- return
- }
- _, err = io.Copy(destArchive, tmpArchive)
- destArchive.Close()
+ var done = make(chan error)
+ repo, err := archiver.LoadRepo()
if err != nil {
- log.Error("Unable to write archive " + r.archivePath)
- return
+ return nil, fmt.Errorf("archiver.LoadRepo failed: %v", err)
}
- // Block any attempt to finalize creating a new request if we're marking
- r.archiveComplete = true
+ gitRepo, err := git.OpenRepository(repo.RepoPath())
+ if err != nil {
+ return nil, err
+ }
+ defer gitRepo.Close()
+
+ go func(done chan error, w *io.PipeWriter, archiver *models.RepoArchiver, gitRepo *git.Repository) {
+ defer func() {
+ if r := recover(); r != nil {
+ done <- fmt.Errorf("%v", r)
+ }
+ }()
+
+ err = gitRepo.CreateArchive(
+ graceful.GetManager().ShutdownContext(),
+ archiver.Type,
+ w,
+ setting.Repository.PrefixArchiveFiles,
+ archiver.CommitID,
+ )
+ _ = w.CloseWithError(err)
+ done <- err
+ }(done, w, archiver, gitRepo)
+
+ // TODO: add lfs data to zip
+ // TODO: add submodule data to zip
+
+ if _, err := storage.RepoArchives.Save(rPath, rd, -1); err != nil {
+ return nil, fmt.Errorf("unable to write archive: %v", err)
+ }
+
+ err = <-done
+ if err != nil {
+ return nil, err
+ }
+
+ if archiver.Status == models.RepoArchiverGenerating {
+ archiver.Status = models.RepoArchiverReady
+ if err = models.UpdateRepoArchiverStatus(ctx, archiver); err != nil {
+ return nil, err
+ }
+ }
+
+ return archiver, commiter.Commit()
}
// ArchiveRepository satisfies the ArchiveRequest being passed in. Processing
@@ -255,65 +204,46 @@ func doArchive(r *ArchiveRequest) {
// anything. In all cases, the caller should be examining the *ArchiveRequest
// being returned for completion, as it may be different than the one they passed
// in.
-func ArchiveRepository(request *ArchiveRequest) *ArchiveRequest {
- // We'll return the request that's already been enqueued if it has been
- // enqueued, or we'll immediately enqueue it if it has not been enqueued
- // and it is not marked complete.
- archiveMutex.Lock()
- defer archiveMutex.Unlock()
- if rExisting := getArchiveRequest(request.repo, request.commit, request.archiveType); rExisting != nil {
- return rExisting
- }
- if request.archiveComplete {
- return request
- }
+func ArchiveRepository(request *ArchiveRequest) (*models.RepoArchiver, error) {
+ return doArchive(request)
+}
- request.cchan = make(chan struct{})
- archiveInProgress = append(archiveInProgress, request)
- go func() {
- // Wait to start, if we have the Cond for it. This is currently only
- // useful for testing, so that the start and release of queued entries
- // can be controlled to examine the queue.
- if archiveQueueStartCond != nil {
- archiveQueueMutex.Lock()
- archiveQueueStartCond.Wait()
- archiveQueueMutex.Unlock()
- }
+var archiverQueue queue.UniqueQueue
- // Drop the mutex while we process the request. This may take a long
- // time, and it's not necessary now that we've added the reequest to
- // archiveInProgress.
- doArchive(request)
-
- if archiveQueueReleaseCond != nil {
- archiveQueueMutex.Lock()
- archiveQueueReleaseCond.Wait()
- archiveQueueMutex.Unlock()
- }
-
- // Purge this request from the list. To do so, we'll just take the
- // index at which we ended up at and swap the final element into that
- // position, then chop off the now-redundant final element. The slice
- // may have change in between these two segments and we may have moved,
- // so we search for it here. We could perhaps avoid this search
- // entirely if len(archiveInProgress) == 1, but we should verify
- // correctness.
- archiveMutex.Lock()
- defer archiveMutex.Unlock()
-
- idx := -1
- for _idx, req := range archiveInProgress {
- if req == request {
- idx = _idx
- break
+// Init initlize archive
+func Init() error {
+ handler := func(data ...queue.Data) {
+ for _, datum := range data {
+ archiveReq, ok := datum.(*ArchiveRequest)
+ if !ok {
+ log.Error("Unable to process provided datum: %v - not possible to cast to IndexerData", datum)
+ continue
+ }
+ log.Trace("ArchiverData Process: %#v", archiveReq)
+ if _, err := doArchive(archiveReq); err != nil {
+ log.Error("Archive %v faild: %v", datum, err)
}
}
- if idx == -1 {
- log.Error("ArchiveRepository: Failed to find request for removal.")
- return
- }
- archiveInProgress = append(archiveInProgress[:idx], archiveInProgress[idx+1:]...)
- }()
+ }
- return request
+ archiverQueue = queue.CreateUniqueQueue("repo-archive", handler, new(ArchiveRequest))
+ if archiverQueue == nil {
+ return errors.New("unable to create codes indexer queue")
+ }
+
+ go graceful.GetManager().RunWithShutdownFns(archiverQueue.Run)
+
+ return nil
+}
+
+// StartArchive push the archive request to the queue
+func StartArchive(request *ArchiveRequest) error {
+ has, err := archiverQueue.Has(request)
+ if err != nil {
+ return err
+ }
+ if has {
+ return nil
+ }
+ return archiverQueue.Push(request)
}
diff --git a/services/archiver/archiver_test.go b/services/archiver/archiver_test.go
index 6dcd942bf5..3f3f369987 100644
--- a/services/archiver/archiver_test.go
+++ b/services/archiver/archiver_test.go
@@ -6,108 +6,75 @@ package archiver
import (
"path/filepath"
- "sync"
"testing"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/test"
- "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
-var queueMutex sync.Mutex
-
func TestMain(m *testing.M) {
models.MainTest(m, filepath.Join("..", ".."))
}
func waitForCount(t *testing.T, num int) {
- var numQueued int
- // Wait for up to 10 seconds for the queue to be impacted.
- timeout := time.Now().Add(10 * time.Second)
- for {
- numQueued = len(archiveInProgress)
- if numQueued == num || time.Now().After(timeout) {
- break
- }
- }
-
- assert.Len(t, archiveInProgress, num)
-}
-
-func releaseOneEntry(t *testing.T, inFlight []*ArchiveRequest) {
- var nowQueued, numQueued int
-
- numQueued = len(archiveInProgress)
-
- // Release one, then wait up to 10 seconds for it to complete.
- queueMutex.Lock()
- archiveQueueReleaseCond.Signal()
- queueMutex.Unlock()
- timeout := time.Now().Add(10 * time.Second)
- for {
- nowQueued = len(archiveInProgress)
- if nowQueued != numQueued || time.Now().After(timeout) {
- break
- }
- }
-
- // Make sure we didn't just timeout.
- assert.NotEqual(t, numQueued, nowQueued)
-
- // Also make sure that we released only one.
- assert.Equal(t, numQueued-1, nowQueued)
}
func TestArchive_Basic(t *testing.T) {
assert.NoError(t, models.PrepareTestDatabase())
- archiveQueueMutex = &queueMutex
- archiveQueueStartCond = sync.NewCond(&queueMutex)
- archiveQueueReleaseCond = sync.NewCond(&queueMutex)
- defer func() {
- archiveQueueMutex = nil
- archiveQueueStartCond = nil
- archiveQueueReleaseCond = nil
- }()
-
ctx := test.MockContext(t, "user27/repo49")
firstCommit, secondCommit := "51f84af23134", "aacbdfe9e1c4"
- bogusReq := DeriveRequestFrom(ctx, firstCommit+".zip")
- assert.Nil(t, bogusReq)
-
test.LoadRepo(t, ctx, 49)
- bogusReq = DeriveRequestFrom(ctx, firstCommit+".zip")
- assert.Nil(t, bogusReq)
-
test.LoadGitRepo(t, ctx)
defer ctx.Repo.GitRepo.Close()
+ bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
+ assert.NoError(t, err)
+ assert.NotNil(t, bogusReq)
+ assert.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName())
+
// Check a series of bogus requests.
// Step 1, valid commit with a bad extension.
- bogusReq = DeriveRequestFrom(ctx, firstCommit+".dilbert")
+ bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".dilbert")
+ assert.Error(t, err)
assert.Nil(t, bogusReq)
// Step 2, missing commit.
- bogusReq = DeriveRequestFrom(ctx, "dbffff.zip")
+ bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff.zip")
+ assert.Error(t, err)
assert.Nil(t, bogusReq)
// Step 3, doesn't look like branch/tag/commit.
- bogusReq = DeriveRequestFrom(ctx, "db.zip")
+ bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db.zip")
+ assert.Error(t, err)
assert.Nil(t, bogusReq)
+ bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master.zip")
+ assert.NoError(t, err)
+ assert.NotNil(t, bogusReq)
+ assert.EqualValues(t, "master.zip", bogusReq.GetArchiveName())
+
+ bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive.zip")
+ assert.NoError(t, err)
+ assert.NotNil(t, bogusReq)
+ assert.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName())
+
// Now two valid requests, firstCommit with valid extensions.
- zipReq := DeriveRequestFrom(ctx, firstCommit+".zip")
+ zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
+ assert.NoError(t, err)
assert.NotNil(t, zipReq)
- tgzReq := DeriveRequestFrom(ctx, firstCommit+".tar.gz")
+ tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".tar.gz")
+ assert.NoError(t, err)
assert.NotNil(t, tgzReq)
- secondReq := DeriveRequestFrom(ctx, secondCommit+".zip")
+ secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".zip")
+ assert.NoError(t, err)
assert.NotNil(t, secondReq)
inFlight := make([]*ArchiveRequest, 3)
@@ -128,41 +95,9 @@ func TestArchive_Basic(t *testing.T) {
// Sleep two seconds to make sure the queue doesn't change.
time.Sleep(2 * time.Second)
- assert.Len(t, archiveInProgress, 3)
- // Release them all, they'll then stall at the archiveQueueReleaseCond while
- // we examine the queue state.
- queueMutex.Lock()
- archiveQueueStartCond.Broadcast()
- queueMutex.Unlock()
-
- // Iterate through all of the in-flight requests and wait for their
- // completion.
- for _, req := range inFlight {
- req.WaitForCompletion(ctx)
- }
-
- for _, req := range inFlight {
- assert.True(t, req.IsComplete())
- exist, err := util.IsExist(req.GetArchivePath())
- assert.NoError(t, err)
- assert.True(t, exist)
- }
-
- arbitraryReq := inFlight[0]
- // Reopen the channel so we don't double-close, mark it incomplete. We're
- // going to run it back through the archiver, and it should get marked
- // complete again.
- arbitraryReq.cchan = make(chan struct{})
- arbitraryReq.archiveComplete = false
- doArchive(arbitraryReq)
- assert.True(t, arbitraryReq.IsComplete())
-
- // Queues should not have drained yet, because we haven't released them.
- // Do so now.
- assert.Len(t, archiveInProgress, 3)
-
- zipReq2 := DeriveRequestFrom(ctx, firstCommit+".zip")
+ zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
+ assert.NoError(t, err)
// This zipReq should match what's sitting in the queue, as we haven't
// let it release yet. From the consumer's point of view, this looks like
// a long-running archive task.
@@ -173,46 +108,22 @@ func TestArchive_Basic(t *testing.T) {
// predecessor has cleared out of the queue.
ArchiveRepository(zipReq2)
- // Make sure the queue hasn't grown any.
- assert.Len(t, archiveInProgress, 3)
-
- // Make sure the queue drains properly
- releaseOneEntry(t, inFlight)
- assert.Len(t, archiveInProgress, 2)
- releaseOneEntry(t, inFlight)
- assert.Len(t, archiveInProgress, 1)
- releaseOneEntry(t, inFlight)
- assert.Empty(t, archiveInProgress)
-
// Now we'll submit a request and TimedWaitForCompletion twice, before and
// after we release it. We should trigger both the timeout and non-timeout
// cases.
- var completed, timedout bool
- timedReq := DeriveRequestFrom(ctx, secondCommit+".tar.gz")
+ timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz")
+ assert.NoError(t, err)
assert.NotNil(t, timedReq)
ArchiveRepository(timedReq)
- // Guaranteed to timeout; we haven't signalled the request to start..
- completed, timedout = timedReq.TimedWaitForCompletion(ctx, 2*time.Second)
- assert.False(t, completed)
- assert.True(t, timedout)
-
- queueMutex.Lock()
- archiveQueueStartCond.Broadcast()
- queueMutex.Unlock()
-
- // Shouldn't timeout, we've now signalled it and it's a small request.
- completed, timedout = timedReq.TimedWaitForCompletion(ctx, 15*time.Second)
- assert.True(t, completed)
- assert.False(t, timedout)
-
- zipReq2 = DeriveRequestFrom(ctx, firstCommit+".zip")
+ zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip")
+ assert.NoError(t, err)
// Now, we're guaranteed to have released the original zipReq from the queue.
// Ensure that we don't get handed back the released entry somehow, but they
// should remain functionally equivalent in all fields. The exception here
// is zipReq.cchan, which will be non-nil because it's a completed request.
// It's fine to go ahead and set it to nil now.
- zipReq.cchan = nil
+
assert.Equal(t, zipReq, zipReq2)
assert.False(t, zipReq == zipReq2)
From 5f2ef17fdb7523398f1b84d8a1cad0f24b72667e Mon Sep 17 00:00:00 2001
From: zeripath
Date: Wed, 23 Jun 2021 22:41:39 +0100
Subject: [PATCH 10/33] Don't WARN log UserNotExist errors on ExternalUserLogin
failure (#16238)
Instead log these at debug - with warn logging for other errors.
Fix #16235
Signed-off-by: Andrew Thornton
---
models/login_source.go | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/models/login_source.go b/models/login_source.go
index 098b48a8cd..359b562b65 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -856,7 +856,11 @@ func UserSignIn(username, password string) (*User, error) {
return authUser, nil
}
- log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
+ if IsErrUserNotExist(err) {
+ log.Debug("Failed to login '%s' via '%s': %v", username, source.Name, err)
+ } else {
+ log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
+ }
}
return nil, ErrUserNotExist{user.ID, user.Name, 0}
From d13a0e621b9a87a50392450c96e7f293cb1e550c Mon Sep 17 00:00:00 2001
From: zeripath
Date: Thu, 24 Jun 2021 00:02:23 +0100
Subject: [PATCH 11/33] Do not show No match found for tribute (#16231)
Tribute.js will show an untranslated no match found if no emoji or mentions.
Further the mentions should really require a preceding space.
This PR fixes both of these.
Signed-off-by: Andrew Thornton
---
web_src/js/features/tribute.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/web_src/js/features/tribute.js b/web_src/js/features/tribute.js
index 851ff74e57..6fdb2f5df0 100644
--- a/web_src/js/features/tribute.js
+++ b/web_src/js/features/tribute.js
@@ -32,7 +32,7 @@ function makeCollections({mentions, emoji}) {
if (emoji) {
collections.push({
values: window.config.tributeValues,
- noMatchTemplate: () => null,
+ requireLeadingSpace: true,
menuItemTemplate: (item) => {
return `
@@ -69,7 +69,7 @@ export default async function attachTribute(elementOrNodeList, {mentions, emoji}
emoji: emoji || emojiNodes.length > 0,
});
- const tribute = new Tribute({collection: collections});
+ const tribute = new Tribute({collection: collections, noMatchTemplate: ''});
for (const node of uniqueNodes) {
tribute.attach(node);
}
From 71c5a8f7f8b127c559115a6bbbfe6372ecf6dd10 Mon Sep 17 00:00:00 2001
From: GiteaBot
Date: Thu, 24 Jun 2021 00:11:37 +0000
Subject: [PATCH 12/33] [skip ci] Updated translations via Crowdin
---
options/locale/locale_bg-BG.ini | 11 +++++++++--
options/locale/locale_cs-CZ.ini | 11 +++++++++--
options/locale/locale_de-DE.ini | 15 +++++++++++++--
options/locale/locale_es-ES.ini | 11 +++++++++--
options/locale/locale_fa-IR.ini | 11 +++++++++--
options/locale/locale_fi-FI.ini | 11 +++++++++--
options/locale/locale_fr-FR.ini | 11 +++++++++--
options/locale/locale_hu-HU.ini | 11 +++++++++--
options/locale/locale_id-ID.ini | 11 +++++++++--
options/locale/locale_it-IT.ini | 11 +++++++++--
options/locale/locale_ja-JP.ini | 33 +++++++++++++++++++++++++++++++--
options/locale/locale_ko-KR.ini | 11 +++++++++--
options/locale/locale_lv-LV.ini | 11 +++++++++--
options/locale/locale_ml-IN.ini | 11 +++++++++--
options/locale/locale_nl-NL.ini | 11 +++++++++--
options/locale/locale_pl-PL.ini | 11 +++++++++--
options/locale/locale_pt-BR.ini | 11 +++++++++--
options/locale/locale_pt-PT.ini | 11 +++++++++--
options/locale/locale_ru-RU.ini | 11 +++++++++--
options/locale/locale_sr-SP.ini | 7 +++++++
options/locale/locale_sv-SE.ini | 11 +++++++++--
options/locale/locale_tr-TR.ini | 11 +++++++++--
options/locale/locale_uk-UA.ini | 11 +++++++++--
options/locale/locale_zh-CN.ini | 11 +++++++++--
options/locale/locale_zh-HK.ini | 9 ++++++++-
options/locale/locale_zh-TW.ini | 11 +++++++++--
26 files changed, 257 insertions(+), 49 deletions(-)
diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini
index ebfc59d166..834c88e177 100644
--- a/options/locale/locale_bg-BG.ini
+++ b/options/locale/locale_bg-BG.ini
@@ -259,12 +259,19 @@ authorization_failed=Оторизацията беше неуспешна
sspi_auth_failed=SSPI удостоверяването беше неуспешно
[mail]
+
activate_account=Моля активирайте Вашия профил
+
activate_email=Провери адрес на ел. поща
-reset_password=Възстановете акаунта си
-register_success=Успешна регистрация
+
register_notify=Добре дошли в Gitea
+reset_password=Възстановете акаунта си
+
+register_success=Успешна регистрация
+
+
+
diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 576cf0615e..8fbdfd4a55 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -314,12 +314,19 @@ password_pwned=Heslo, které jste zvolili, je na %s auf folgenden Link, um dein Konto zu aktivieren:
+
register_notify=Willkommen bei Gitea
+register_notify.text_1=dies ist deine Bestätigungs-E-Mail für %s!
+register_notify.text_3=Wenn dieser Account von dir erstellt wurde, musst du zuerst dein Passwort setzen .
+
+reset_password=Stelle dein Konto wieder her
+reset_password.title=%s, du hast um Wiederherstellung deines Kontos gebeten
+
+register_success=Registrierung erfolgreich
+
+
release.new.subject=Release %s in %s erschienen
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index 2ece1992e6..6a62ff7726 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -316,12 +316,19 @@ password_pwned=La contraseña que eligió está en una 存在しないか、閲覧が許可されていません 。
+never=無し
[error]
occurred=エラーが発生しました
@@ -314,12 +316,19 @@ password_pwned=あなたが選択したパスワードは、過去の情報漏
password_pwned_err=HaveIBeenPwnedへのリクエストを完了できませんでした
[mail]
+
activate_account=あなたのアカウントをアクティベートしてください。
+
activate_email=メール アドレスを確認します
-reset_password=アカウントを回復
-register_success=登録が完了しました
+
register_notify=Giteaへようこそ
+reset_password=アカウントを回復
+
+register_success=登録が完了しました
+
+
+
release.new.subject=%[2]s の %[1]s がリリースされました
repo.transfer.subject_to=%s が "%s" を %s に移転しようとしています
@@ -724,6 +733,7 @@ mirror_prune_desc=不要になった古いリモートトラッキング参照
mirror_interval=ミラー間隔 (有効な時間の単位は'h'、'm'、's')。 自動的な同期を無効にする場合は0。
mirror_interval_invalid=ミラー間隔が不正です。
mirror_address=クローンするURL
+mirror_address_desc=必要な資格情報は「認証」セクションに設定してください。
mirror_address_url_invalid=入力したURLは無効です。 URLの構成要素はすべて正しくエスケープする必要があります。
mirror_address_protocol_invalid=入力したURLは無効です。 ミラーできるのは、http(s):// または git:// の場所からだけです。
mirror_lfs=Large File Storage (LFS)
@@ -786,6 +796,7 @@ form.reach_limit_of_creation_n=すでにあなたが作成できるリポジト
form.name_reserved=リポジトリ名 '%s' は予約されています。
form.name_pattern_not_allowed='%s' の形式はリポジトリ名に使用できません。
+need_auth=認証
migrate_options=移行オプション
migrate_service=移行するサービス
migrate_options_mirror_helper=このリポジトリをミラー にする
@@ -819,11 +830,19 @@ migrated_from_fake=%[1]sから移行
migrate.migrate=%s からの移行
migrate.migrating=%s から移行しています ...
migrate.migrating_failed=%s からの移行が失敗しました。
+migrate.migrating_failed.error=エラー: %s
migrate.github.description=Github.com または Github Enterprise からデータを移行します。
migrate.git.description=Gitサービスからgitデータを移行またはミラーを作成します
migrate.gitlab.description=GitLab.com またはセルフホストのgitlabサーバーからデータを移行します。
migrate.gitea.description=Gitea.comまたはセルフホストのGiteaサーバーからデータを移行します。
migrate.gogs.description=notabug.org や、他のセルフホストのGogsサーバーからデータを移行します。
+migrate.migrating_git=Gitデータ移行中
+migrate.migrating_topics=トピック移行中
+migrate.migrating_milestones=マイルストーン移行中
+migrate.migrating_labels=ラベル移行中
+migrate.migrating_releases=リリース移行中
+migrate.migrating_issues=課題移行中
+migrate.migrating_pulls=プルリクエスト移行中
mirror_from=ミラー元
forked_from=フォーク元
@@ -1546,6 +1565,15 @@ settings.hooks=Webhook
settings.githooks=Gitフック
settings.basic_settings=基本設定
settings.mirror_settings=ミラー設定
+settings.mirror_settings.docs=他のリポジトリへの自動的なプッシュ/プルを行うよう、プロジェクトを設定します。 ブランチ、タグ、コミットが自動的に同期されます。 リポジトリをミラーするには?
+settings.mirror_settings.mirrored_repository=同期するリポジトリ
+settings.mirror_settings.direction=方向
+settings.mirror_settings.direction.pull=プル
+settings.mirror_settings.direction.push=プッシュ
+settings.mirror_settings.last_update=最終更新
+settings.mirror_settings.push_mirror.none=プッシュミラーは設定されていません
+settings.mirror_settings.push_mirror.remote_url=リモートGitリポジトリのURL
+settings.mirror_settings.push_mirror.add=プッシュミラーを追加
settings.sync_mirror=今すぐ同期
settings.mirror_sync_in_progress=ミラー同期を実行しています。 しばらくあとでまた確認してください。
settings.email_notifications.enable=メール通知有効
@@ -1611,6 +1639,7 @@ settings.transfer_form_title=確認のためリポジトリ名を入力:
settings.transfer_in_progress=現在進行中の転送があります。このリポジトリを別のユーザーに転送したい場合はキャンセルしてください。
settings.transfer_notices_1=- 個人ユーザーに移転すると、あなたはリポジトリへのアクセス権を失います。
settings.transfer_notices_2=- あなたが所有(または共同で所有)している組織に移転すると、リポジトリへのアクセス権は維持されます。
+settings.transfer_notices_3=- プライベートリポジトリを個人ユーザーに移転した場合は、最低限そのユーザーが読み取り権限を持つよう設定されます (必要に応じて権限が変更されます)。
settings.transfer_owner=新しいオーナー
settings.transfer_perform=転送を実行
settings.transfer_started=このリポジトリは転送のためにマークされており、「%s」からの確認を待っています
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index acf7efb478..f4a05a7e49 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -268,12 +268,19 @@ authorization_failed=인증 실패
sspi_auth_failed=SSPI 인증 실패
[mail]
+
activate_account=계정을 활성화하세요
+
activate_email=이메일 주소 확인
-reset_password=계정 복구
-register_success=등록 완료
+
register_notify=Gitea에 오신것을 환영합니다!
+reset_password=계정 복구
+
+register_success=등록 완료
+
+
+
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index f4db35671a..2fad43b543 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -314,12 +314,19 @@ password_pwned=Ievadītā parole ir
Date: Fri, 25 Jun 2021 02:37:07 +0800
Subject: [PATCH 14/33] Replace ARCCache with TwoQueueCache to avoid patent
issue (#16240)
Co-authored-by: Mura Li
Co-authored-by: techknowlogick
---
modules/highlight/highlight.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go
index e22e9d5b32..568035fbb7 100644
--- a/modules/highlight/highlight.go
+++ b/modules/highlight/highlight.go
@@ -33,7 +33,7 @@ var (
once sync.Once
- cache *lru.ARCCache
+ cache *lru.TwoQueueCache
)
// NewContext loads custom highlight map from local config
@@ -45,7 +45,7 @@ func NewContext() {
}
// The size 512 is simply a conservative rule of thumb
- c, err := lru.NewARC(512)
+ c, err := lru.New2Q(512)
if err != nil {
panic(fmt.Sprintf("failed to initialize LRU cache for highlighter: %s", err))
}
From 6c3433151fdb84a9dc1214442573da2d7cc76e3e Mon Sep 17 00:00:00 2001
From: sebastian-sauer
Date: Fri, 25 Jun 2021 00:05:51 +0200
Subject: [PATCH 15/33] API: Allow COMMENT reviews to not specify a body
(#16229)
* Allow COMMENT reviews to not specify a body
when using web ui there is no need to specify a body.
so we don't need to specify a body if adding a COMMENT-review
via our api.
* Ensure comments or Body is provided
and add some integration tests for reviewtype COMMENT.
Signed-off-by: Sebastian Sauer
---
integrations/api_pull_review_test.go | 54 ++++++++++++++++++++++++++++
routers/api/v1/repo/pull_review.go | 22 ++++++++----
2 files changed, 70 insertions(+), 6 deletions(-)
diff --git a/integrations/api_pull_review_test.go b/integrations/api_pull_review_test.go
index ebe8539a82..bcc0cbffcb 100644
--- a/integrations/api_pull_review_test.go
+++ b/integrations/api_pull_review_test.go
@@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models"
api "code.gitea.io/gitea/modules/structs"
+ jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert"
)
@@ -139,6 +140,59 @@ func TestAPIPullReview(t *testing.T) {
req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token)
resp = session.MakeRequest(t, req, http.StatusNoContent)
+ // test CreatePullReview Comment without body but with comments
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
+ // Body: "",
+ Event: "COMMENT",
+ Comments: []api.CreatePullReviewComment{{
+ Path: "README.md",
+ Body: "first new line",
+ OldLineNum: 0,
+ NewLineNum: 1,
+ }, {
+ Path: "README.md",
+ Body: "first old line",
+ OldLineNum: 1,
+ NewLineNum: 0,
+ },
+ },
+ })
+ var commentReview api.PullReview
+
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &commentReview)
+ assert.EqualValues(t, "COMMENT", commentReview.State)
+ assert.EqualValues(t, 2, commentReview.CodeCommentsCount)
+ assert.EqualValues(t, "", commentReview.Body)
+ assert.EqualValues(t, false, commentReview.Dismissed)
+
+ // test CreatePullReview Comment with body but without comments
+ commentBody := "This is a body of the comment."
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
+ Body: commentBody,
+ Event: "COMMENT",
+ Comments: []api.CreatePullReviewComment{},
+ })
+
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &commentReview)
+ assert.EqualValues(t, "COMMENT", commentReview.State)
+ assert.EqualValues(t, 0, commentReview.CodeCommentsCount)
+ assert.EqualValues(t, commentBody, commentReview.Body)
+ assert.EqualValues(t, false, commentReview.Dismissed)
+
+ // test CreatePullReview Comment without body and no comments
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
+ Body: "",
+ Event: "COMMENT",
+ Comments: []api.CreatePullReviewComment{},
+ })
+ resp = session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ errMap := make(map[string]interface{})
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ json.Unmarshal(resp.Body.Bytes(), &errMap)
+ assert.EqualValues(t, "review event COMMENT requires a body or a comment", errMap["message"].(string))
+
// test get review requests
// to make it simple, use same api with get review
pullIssue12 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 12}).(*models.Issue)
diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go
index 35414e0a80..323904f45c 100644
--- a/routers/api/v1/repo/pull_review.go
+++ b/routers/api/v1/repo/pull_review.go
@@ -307,7 +307,7 @@ func CreatePullReview(ctx *context.APIContext) {
}
// determine review type
- reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body)
+ reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(opts.Comments) > 0)
if isWrong {
return
}
@@ -429,7 +429,7 @@ func SubmitPullReview(ctx *context.APIContext) {
}
// determine review type
- reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body)
+ reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(review.Comments) > 0)
if isWrong {
return
}
@@ -463,12 +463,15 @@ func SubmitPullReview(ctx *context.APIContext) {
}
// preparePullReviewType return ReviewType and false or nil and true if an error happen
-func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, event api.ReviewStateType, body string) (models.ReviewType, bool) {
+func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, event api.ReviewStateType, body string, hasComments bool) (models.ReviewType, bool) {
if err := pr.LoadIssue(); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return -1, true
}
+ needsBody := true
+ hasBody := len(strings.TrimSpace(body)) > 0
+
var reviewType models.ReviewType
switch event {
case api.ReviewStateApproved:
@@ -478,6 +481,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
return -1, true
}
reviewType = models.ReviewTypeApprove
+ needsBody = false
case api.ReviewStateRequestChanges:
// can not reject your own PR
@@ -489,13 +493,19 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
case api.ReviewStateComment:
reviewType = models.ReviewTypeComment
+ needsBody = false
+ // if there is no body we need to ensure that there are comments
+ if !hasBody && !hasComments {
+ ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body or a comment", event))
+ return -1, true
+ }
default:
reviewType = models.ReviewTypePending
}
- // reject reviews with empty body if not approve type
- if reviewType != models.ReviewTypeApprove && len(strings.TrimSpace(body)) == 0 {
- ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s need body", event))
+ // reject reviews with empty body if a body is required for this call
+ if needsBody && !hasBody {
+ ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body", event))
return -1, true
}
From 837e8b30a76293fd96d2aae872e30637668ddb3f Mon Sep 17 00:00:00 2001
From: GiteaBot
Date: Fri, 25 Jun 2021 00:11:36 +0000
Subject: [PATCH 16/33] [skip ci] Updated translations via Crowdin
---
options/locale/locale_de-DE.ini | 37 ++++++++++++++++++++++
options/locale/locale_es-ES.ini | 56 +++++++++++++++++++++++++++++++++
options/locale/locale_fr-FR.ini | 44 ++++++++++++++++++++++++++
options/locale/locale_hu-HU.ini | 49 +++++++++++++++++++++++++++++
options/locale/locale_id-ID.ini | 2 ++
options/locale/locale_it-IT.ini | 2 ++
options/locale/locale_ko-KR.ini | 4 +++
options/locale/locale_nl-NL.ini | 13 ++++++++
options/locale/locale_zh-TW.ini | 7 +++--
9 files changed, 211 insertions(+), 3 deletions(-)
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index 4572aefb0d..e8f2d1d587 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -316,30 +316,64 @@ password_pwned=Das von dir gewählte Passwort ist auf einer %s,
activate_account=Bitte aktiviere dein Konto
+activate_account.title=%s, bitte aktiviere dein Konto
+activate_account.test_1=Hallo %[1]s , danke für deine Registrierung bei %[2]!
+activate_account.test_2=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto zu aktivieren:
activate_email=Bestätige deine E-Mail-Adresse
+activate_email.title=%s, bitte verifiziere deine E-Mail-Adresse
activate_email.text=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto zu aktivieren:
register_notify=Willkommen bei Gitea
+register_notify.title=%[1]s, willkommen bei %[2]s
register_notify.text_1=dies ist deine Bestätigungs-E-Mail für %s!
+register_notify.text_2=Du kannst dich jetzt mit dem Benutzernamen "%s" anmelden.
register_notify.text_3=Wenn dieser Account von dir erstellt wurde, musst du zuerst dein Passwort setzen .
reset_password=Stelle dein Konto wieder her
reset_password.title=%s, du hast um Wiederherstellung deines Kontos gebeten
+reset_password.text=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto wiederherzustellen:
register_success=Registrierung erfolgreich
+issue_assigned.pull=@%[1]s hat dich im Repository %[3]s dem Pull Request %[2]s zugewiesen.
+issue_assigned.issue=@%[1]s hat dich im Repository %[3]s dem Issue %[2]s zugewiesen.
+issue.x_mentioned_you=@%s hat dich erwähnt:
+issue.action.force_push=%[1]s hat %[3]s mit %[4]s auf %[2]s überschrieben.
+issue.action.push_1=@%[1]s hat einen Commit auf %[2]s gepusht
+issue.action.push_n=@%[1]s hat %[3]d Commits auf %[2]s gepusht
+issue.action.close=@%[1]s hat #%[2]d geschlossen.
+issue.action.reopen=@%[1]s hat #%[2]d wieder geöffnet.
+issue.action.merge=@%[1]s hat #%[2]d in %[3]s gemergt.
+issue.action.approve=@%[1]s hat diesen Pull-Request approved.
+issue.action.reject=@%[1]s hat Änderungen auf diesem Pull-Request angefordert.
+issue.action.review=@%[1]s hat diesen Pull-Request kommentiert.
+issue.action.review_dismissed=@%[1]s hat das letzte Review von %[2]s für diesen Pull Request verworfen.
+issue.action.ready_for_review=@%[1]s hat diesen Pull Request zum Review freigegeben.
+issue.action.new=@%[1]s hat #%[2]d geöffnet.
+issue.in_tree_path=In %s:
release.new.subject=Release %s in %s erschienen
+release.new.text=@%[1]s hat %[2]s in %[3]s released
+release.title=Titel: %s
+release.note=Anmerkung:
+release.downloads=Downloads:
+release.download.zip=Quellcode (ZIP Datei)
+release.download.targz=Quellcode (TAR.GZ Datei)
repo.transfer.subject_to=%s möchte "%s" an %s übertragen
repo.transfer.subject_to_you=%s möchte dir "%s" übertragen
repo.transfer.to_you=dir
+repo.transfer.body=Um es anzunehmen oder abzulehnen, öffne %s, oder ignoriere es einfach.
repo.collaborator.added.subject=%s hat dich zu %s hinzugefügt
+repo.collaborator.added.text=Du wurdest als Mitarbeiter für folgendes Repository hinzugefügt:
[modal]
yes=Ja
@@ -841,6 +875,7 @@ migrate.gitlab.description=Migriere Daten von GitLab.com oder einem selbst gehos
migrate.gitea.description=Migriere Daten von Gitea.com oder einem selbst gehostetem Gitea Server.
migrate.gogs.description=Migriere Daten von notabug.org oder einem anderen, selbst gehosteten Gogs Server.
migrate.migrating_git=Git Daten werden migriert
+migrate.migrating_topics=Themen werden migriert
migrate.migrating_milestones=Meilensteine werden migriert
migrate.migrating_labels=Labels werden migriert
migrate.migrating_releases=Releases werden migriert
@@ -1568,6 +1603,8 @@ settings.hooks=Webhooks
settings.githooks=Git-Hooks
settings.basic_settings=Grundeinstellungen
settings.mirror_settings=Mirror-Einstellungen
+settings.mirror_settings.docs=Richte dein Projekt so ein, dass Änderungen automatisch in ein anderes Repository gepusht, oder aus einem anderen Repository gepullt werden. Branches, tags und commits werden dann automatisch synchronisiert. Wie kann ich ein Repository spiegeln? (Englisch)
+settings.mirror_settings.mirrored_repository=Gespiegeltes Repository
settings.mirror_settings.direction=Richtung
settings.mirror_settings.direction.pull=Pull
settings.mirror_settings.direction.push=Push
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index 6a62ff7726..017b83a9b3 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -316,26 +316,64 @@ password_pwned=La contraseña que eligió está en una establezca su contraseña primero.
reset_password=Recupere su cuenta
+reset_password.title=%s, has solicitado recuperar tu cuenta
+reset_password.text=Haga clic en el siguiente enlace para recuperar su cuenta dentro de %s :
register_success=Registro completado
+issue_assigned.pull=@%[1]s le asignó al pull request %[2]s en el repositorio %[3]s.
+issue_assigned.issue=@%[1]s le asignó a la incidencia %[2]s en el repositorio %[3]s.
+issue.x_mentioned_you=@%s te mencionó:
+issue.action.force_push=%[1]s empujó a la fuerza el %[2]s de %[3]s a %[4]s.
+issue.action.push_1=@%[1]s hizo 1 commit al %[2]s
+issue.action.push_n=@%[1]s push %[3]d commits a %[2]s
+issue.action.close=@%[1]s cerró #%[2]d.
+issue.action.reopen=@%[1]s reabrió #%[2]d.
+issue.action.merge=@%[1]s fusionó #%[2]d en %[3]s.
+issue.action.approve=@%[1]s aprobó este pull request.
+issue.action.reject=@%[1]s solicitó cambios en este pull request.
+issue.action.review=@%[1]s comentó en este pull request.
+issue.action.review_dismissed=@%[1]s descartó la última revisión de %[2]s para este pull request.
+issue.action.ready_for_review=@%[1]s marcó este pull request listo para ser revisado.
+issue.action.new=@%[1]s creó #%[2]d.
+issue.in_tree_path=En %s:
release.new.subject=%s en %s publicado
+release.new.text=@%[1]s lanzó %[2]s en %[3]s
+release.title=Título: %s
+release.note=Nota:
+release.downloads=Descargas:
+release.download.zip=Código fuente (ZIP)
+release.download.targz=Código fuente (TAR.GZ)
repo.transfer.subject_to=%s desea transferir "%s" a %s
repo.transfer.subject_to_you=%s desea transferir "%s" a usted
repo.transfer.to_you=usted
+repo.transfer.body=Para aceptarlo o rechazarlo, visita %s o simplemente ignórelo.
repo.collaborator.added.subject=%s le añadió en %s
+repo.collaborator.added.text=Has sido añadido como colaborador del repositorio:
[modal]
yes=Sí
@@ -733,6 +771,7 @@ mirror_prune_desc=Eliminar referencias de seguimiento de remotes obsoletas
mirror_interval=Intervalo de réplica (Las unidades de tiempo válidas son 'h', 'm', 's'). Pone 0 para deshabilitar la sincronización automática.
mirror_interval_invalid=El intervalo de réplica no es válido.
mirror_address=Clonar desde URL
+mirror_address_desc=Ponga cualquier credencial requerida en la sección de Autorización.
mirror_address_url_invalid=La url proporcionada no es válida. Debe escapar correctamente de todos los componentes de la url.
mirror_address_protocol_invalid=La url proporcionada no es válida. Sólo las ubicaciones http(s):// o git:// pueden ser replicadas desde.
mirror_lfs=Almacenamiento de archivos grande (LFS)
@@ -741,6 +780,8 @@ mirror_lfs_endpoint=Punto final de LFS
mirror_lfs_endpoint_desc=Sync intentará usar la url del clon para determinar el servidor LFS . También puede especificar un punto final personalizado si los datos LFS del repositorio se almacenan en otro lugar.
mirror_last_synced=Sincronizado por última vez
mirror_password_placeholder=(Sin cambios)
+mirror_password_blank_placeholder=(Indefinido)
+mirror_password_help=Cambie el nombre de usario para eliminar una contraseña almacenada.
watchers=Seguidores
stargazers=Fans
forks=Forks
@@ -827,11 +868,19 @@ migrated_from_fake=Migrado desde %[1]s
migrate.migrate=Migrar desde %s
migrate.migrating=Migrando desde %s ...
migrate.migrating_failed=La migración desde %s ha fallado.
+migrate.migrating_failed.error=Error: %s
migrate.github.description=Migrar datos de Github.com o Github Enterprise.
migrate.git.description=Migrar o replicar de datos de git desde los servicios de Git
migrate.gitlab.description=Migrar datos de GitLab.com o servidor gitlab autoalojado.
migrate.gitea.description=Migrando datos de Gitea.com o servidor Gitea autoalojado.
migrate.gogs.description=Migrando datos de notabug.org u otro servidor de Gogs autoalojado.
+migrate.migrating_git=Migrando datos de Git
+migrate.migrating_topics=Migrando Temas
+migrate.migrating_milestones=Migrando Hitos
+migrate.migrating_labels=Migrando etiquetas
+migrate.migrating_releases=Migrando Lanzamientos
+migrate.migrating_issues=Migrando Incidencías
+migrate.migrating_pulls=Migrando Pull Requests
mirror_from=réplica de
forked_from=forkeado de
@@ -1325,7 +1374,10 @@ pulls.manually_merged_as=El Pull Request se ha fusionado manualmente como Comience el título con %s para prevenir que el pull request se fusione accidentalmente.`
+pulls.cannot_merge_work_in_progress=Este pull request está marcado como un trabajo en curso.
pulls.still_in_progress=¿Aún en curso?
+pulls.add_prefix=Añadir prefijo %s
+pulls.remove_prefix=Eliminar prefijo %s
pulls.data_broken=Este pull request está rota debido a que falta información del fork.
pulls.files_conflicted=Este pull request tiene cambios en conflicto con la rama de destino.
pulls.is_checking=La comprobación de conflicto de fusión está en progreso. Inténtalo de nuevo en unos momentos.
@@ -1552,10 +1604,14 @@ settings.githooks=Git Hooks
settings.basic_settings=Configuración Básica
settings.mirror_settings=Configuración de réplica
settings.mirror_settings.docs=Configure su proyecto para insertar y/o extraer automáticamente los cambios hacia/desde otro repositorio. Las ramas, etiquetas y commits se sincronizarán automáticamente. ¿Cómo replico los repositorios?
+settings.mirror_settings.mirrored_repository=Repositorio Replicado
settings.mirror_settings.direction=Dirección
settings.mirror_settings.direction.pull=Pull
+settings.mirror_settings.direction.push=Push
settings.mirror_settings.last_update=Última actualización
+settings.mirror_settings.push_mirror.none=No hay Réplicas de Push configurados
settings.mirror_settings.push_mirror.remote_url=URL del repositorio remoto de Git
+settings.mirror_settings.push_mirror.add=Añadir Réplica de Push
settings.sync_mirror=Sincronizar ahora
settings.mirror_sync_in_progress=La sincronización del repositorio replicado está en curso. Vuelva a intentarlo más tarde.
settings.email_notifications.enable=Habilitar las notificaciones por correo electrónico
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 42d7ada018..493c950581 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -91,8 +91,10 @@ loading=Chargement…
step1=Étape 1:
step2=Étape 2:
+error=Erreur
error404=La page que vous essayez d'atteindre n'existe pas ou vous n'êtes pas autorisé à la voir.
+never=Jamais
[error]
occurred=Une erreur est survenue
@@ -316,6 +318,7 @@ password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned
[mail]
activate_account=Veuillez activer votre compte
+activate_account.title=%s, veuillez activer votre compte
activate_email=Veuillez vérifier votre adresse e-mail
@@ -326,14 +329,24 @@ reset_password=Récupérer votre compte
register_success=Inscription réussie
+issue.x_mentioned_you=@%s vous a mentionné:
+issue.action.approve=@%[1]s a approuvé cette demande d'ajout.
+issue.action.reject=@%[1]s a demandé des modifications sur cette demande d'ajout.
+issue.action.review=@%[1]s a commenté sur cette demande d'ajout.
+issue.action.review_dismissed=@%[1]s a rejeté la dernière révision de %[2]s pour cette demande d'ajout.
+issue.action.ready_for_review=@%[1]s a marqué cette demande d'ajout prête à être revue.
+issue.in_tree_path=Dans %s:
release.new.subject=%s publiée dans %s
+release.title=Titre: %s
+release.downloads=Téléchargements :
repo.transfer.subject_to=%s aimerait transférer "%s" à %s
repo.transfer.subject_to_you=%s aimerait vous transférer "%s"
repo.transfer.to_you=vous
repo.collaborator.added.subject=%s vous a ajouté à %s
+repo.collaborator.added.text=Vous avez été ajouté en tant que collaborateur du dépôt :
[modal]
yes=Oui
@@ -729,6 +742,7 @@ mirror_prune_desc=Supprimer les références externes obsolètes
mirror_interval=Intervalle de synchronisation ('h', 'm', et 's' sont des unités valides), 0 pour désactiver.
mirror_interval_invalid=L'intervalle de synchronisation est invalide.
mirror_address=Cloner depuis une URL
+mirror_address_desc=Insérez tous les identifiants requis dans la section Autorisation.
mirror_address_url_invalid=L'url fournie est invalide. Vous devez échapper tous les composants de l'url correctement.
mirror_address_protocol_invalid=L'url fournie est invalide. Seuls les protocoles http(s):// ou git:// peuvent être la source du miroir.
mirror_lfs=Stockage de fichiers volumineux (LFS)
@@ -736,6 +750,9 @@ mirror_lfs_desc=Activer la mise en miroir des données LFS.
mirror_lfs_endpoint=Point d'accès LFS
mirror_lfs_endpoint_desc=La synchronisation tentera d'utiliser l'url de clonage pour déterminer le serveur LFS . Vous pouvez également spécifier un point d'accès personnalisé si les données LFS du dépôt sont stockées ailleurs.
mirror_last_synced=Dernière synchronisation
+mirror_password_placeholder=(Aucune modification)
+mirror_password_blank_placeholder=(Non défini)
+mirror_password_help=Modifiez le nom d'utilisateur pour effacer un mot de passe enregistré.
watchers=Observateurs
stargazers=Fans
forks=Bifurcations
@@ -788,6 +805,7 @@ form.reach_limit_of_creation_n=Vous avez déjà atteint la limite de %d dépôts
form.name_reserved=Le dépôt "%s" a un nom réservé.
form.name_pattern_not_allowed="%s" n'est pas autorisé dans un nom de dépôt.
+need_auth=Autorisation
migrate_options=Options de migration
migrate_service=Service de migration
migrate_options_mirror_helper=Ce dépôt sera un miroir
@@ -821,11 +839,19 @@ migrated_from_fake=Migré de %[1]s
migrate.migrate=Migrer depuis %s
migrate.migrating=Migration de %s ...
migrate.migrating_failed=La migration de %s a échoué.
+migrate.migrating_failed.error=Erreur: %s
migrate.github.description=Migration de données depuis Github.com ou Github Enterprise.
migrate.git.description=Migration ou Miroir des données git depuis des services Git
migrate.gitlab.description=Migration des données depuis GitLab.com ou d'un serveur gitlab hébergé.
migrate.gitea.description=Migration des données depuis Gitea.com ou un serveur Gitea hébergé soi-même.
migrate.gogs.description=Migration de données depuis notabug.org ou un autre serveur Gogs auto-hébergé.
+migrate.migrating_git=Migration des données Git
+migrate.migrating_topics=Migration des sujets
+migrate.migrating_milestones=Migration des jalons
+migrate.migrating_labels=Migration des étiquettes
+migrate.migrating_releases=Migration des versions
+migrate.migrating_issues=Migration des tickets
+migrate.migrating_pulls=Migration des demandes d'ajout
mirror_from=miroir de
forked_from=bifurqué depuis
@@ -858,6 +884,7 @@ branch=Branche
tree=Aborescence
clear_ref=`Effacer la référence actuelle`
filter_branch_and_tag=Filtrer une branche ou un tag
+find_tag=Rechercher un tag
branches=Branches
tags=Tags
issues=Tickets
@@ -1288,6 +1315,8 @@ issues.review.resolved_by=marquer cette conversation comme résolue
issues.assignee.error=Tous les assignés n'ont pas été ajoutés en raison d'une erreur inattendue.
issues.reference_issue.body=Corps
+compare.compare_base=base
+compare.compare_head=comparer
pulls.desc=Activer les demandes de fusion et la revue de code.
pulls.new=Nouvelle demande d'ajout
@@ -1316,6 +1345,7 @@ pulls.manually_merged_as=La demande d'ajout a été fusionnée manuellement en t
pulls.is_closed=La demande de fusion a été fermée.
pulls.has_merged=La pull request a été fusionnée.
pulls.title_wip_desc=`Préfixer le titre par %s pour empêcher cette demande d'ajout d'être fusionnée par erreur.`
+pulls.remove_prefix=Enlever le préfixe %s
pulls.data_broken=Cette demande de fusion est impossible par manque d'informations de bifurcation.
pulls.files_conflicted=Cette demande d'ajout contient des modifications en conflit avec la branche ciblée.
pulls.is_checking=Vérification des conflits de fusion en cours. Réessayez dans quelques instants.
@@ -1541,6 +1571,9 @@ settings.hooks=Déclencheurs Web
settings.githooks=Déclencheurs Git
settings.basic_settings=Paramètres de base
settings.mirror_settings=Réglages Miroir
+settings.mirror_settings.direction=Direction
+settings.mirror_settings.last_update=Dernière mise à jour
+settings.mirror_settings.push_mirror.remote_url=URL du dépôt distant Git
settings.sync_mirror=Synchroniser maintenant
settings.mirror_sync_in_progress=La synchronisation est en cours. Revenez dans une minute.
settings.email_notifications.enable=Activer les notifications par e-mail
@@ -1549,6 +1582,7 @@ settings.email_notifications.disable=Désactiver les notifications par e-mail
settings.email_notifications.submit=Définir la préférence e-mail
settings.site=Site Web
settings.update_settings=Valider
+settings.branches.update_default_branch=Changer la Branche par Défaut
settings.advanced_settings=Paramètres avancés
settings.wiki_desc=Activer le wiki du dépôt
settings.use_internal_wiki=Utiliser le wiki interne
@@ -1911,6 +1945,7 @@ diff.image.overlay=Superposition
releases.desc=Suivi des versions et des téléchargements.
release.releases=Versions
release.detail=Détails de la version
+release.tags=Tags
release.new_release=Nouvelle version
release.draft=Brouillon
release.prerelease=Pré-publication
@@ -2103,8 +2138,12 @@ dashboard.operation_switch=Basculer
dashboard.operation_run=Exécuter
dashboard.clean_unbind_oauth=Effacer les connexions OAuth associées
dashboard.clean_unbind_oauth_success=Toutes les connexions OAuth associées ont été supprimées.
+dashboard.cron.finished=Tâche planifiée : %[1]s a terminé
+dashboard.delete_repo_archives=Supprimer toutes les archives des dépôts (ZIP, TAR.GZ, etc..)
+dashboard.delete_repo_archives.started=Tâche de suppression de toutes les archives de dépôts démarrée.
dashboard.delete_missing_repos=Supprimer tous les dépôts dont les fichiers Git sont manquants
dashboard.delete_generated_repository_avatars=Supprimer les avatars de dépôt générés
+dashboard.repo_health_check=Vérifier l'état de santé de tous les dépôts
dashboard.check_repo_stats=Voir les statistiques de tous les dépôts
dashboard.archive_cleanup=Supprimer les archives des vieux dépôts
dashboard.git_gc_repos=Collecter les déchets des dépôts
@@ -2141,6 +2180,7 @@ dashboard.total_gc_time=Pause GC
dashboard.total_gc_pause=Pause GC
dashboard.last_gc_pause=Dernière Pause GC
dashboard.gc_times=Nombres de GC
+dashboard.delete_old_actions=Supprimer toutes les anciennes actions de la base de données
users.user_manage_panel=Gestion du compte utilisateur
users.new_account=Créer un compte
@@ -2177,6 +2217,7 @@ users.delete_account=Supprimer cet utilisateur
users.still_own_repo=Cet utilisateur possède un ou plusieurs dépôts. Veuillez les supprimer ou les transférer à un autre utilisateur.
users.still_has_org=Cet utilisateur est membre d'une organisation. Veuillez le retirer de toutes les organisations dont il est membre au préalable.
users.deletion_success=Le compte a été supprimé.
+users.reset_2fa=Réinitialiser l'authentification à deux facteurs
emails.email_manage_panel=Gestion des courriels des utilisateurs
emails.primary=Principale
@@ -2198,6 +2239,7 @@ orgs.members=Membres
orgs.new_orga=Nouvelle organisation
repos.repo_manage_panel=Gestion des dépôts
+repos.unadopted=Dépôts non adoptés
repos.owner=Propriétaire
repos.name=Nom
repos.private=Privé
@@ -2383,6 +2425,7 @@ config.mailer_use_sendmail=Utiliser Sendmail
config.mailer_sendmail_path=Chemin d’accès à Sendmail
config.mailer_sendmail_args=Arguments supplémentaires pour Sendmail
config.mailer_sendmail_timeout=Délai d’attente de Sendmail
+config.test_email_placeholder=E-mail (ex: test@example.com)
config.send_test_mail=Envoyer un e-mail de test
config.test_mail_failed=Impossible d'envoyer un e-mail de test à '%s' : %v
config.test_mail_sent=Un e-mail de test à été envoyé à '%s'.
@@ -2510,6 +2553,7 @@ notices.delete_selected=Supprimé les éléments sélectionnés
notices.delete_all=Supprimer toutes les notifications
notices.type=Type
notices.type_1=Dépôt
+notices.type_2=Tâche
notices.desc=Description
notices.op=Opération
notices.delete_success=Les informations systèmes ont été supprimées.
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index 5671a998d1..953d5ab8ac 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -19,6 +19,9 @@ create_new=Létrehozás…
user_profile_and_more=Profil és beállítások...
signed_in_as=Bejelentkezve mint
enable_javascript=Ez az oldal jobban működik JavaScript-tel.
+toc=Tartalomjegyzék
+licenses=Licencek
+return_to_gitea=Vissza a Gitea-hoz
username=Felhasználónév
email=E-mail cím
@@ -50,6 +53,8 @@ new_migrate=Új migráció
new_mirror=Új tükör
new_fork=Új másolat
new_org=Új szervezet
+new_project=Új projekt
+new_project_board=Új projekt tábla
manage_org=Szervezetek kezelése
admin_panel=Rendszergazdai felület
account_settings=Fiók beállítások
@@ -70,6 +75,7 @@ issues=Hibajegyek
milestones=Mérföldkövek
cancel=Mégse
+save=Mentés
add=Hozzáadás
add_all=Összes hozzáadása
remove=Eltávolítás
@@ -79,6 +85,8 @@ write=Írás
preview=Előnézet
loading=Betöltés…
+step1=1. lépés:
+step2=2. lépés:
error404=Az elérni kívánt oldal vagy nem létezik , vagy nincs jogosultsága a megtekintéséhez.
@@ -91,6 +99,7 @@ report_message=Ha biztos benne, hogy ez egy Gitea hiba, keressen a problémára
app_desc=Fájdalommentes, saját gépre telepíthető Git szolgáltatás
install=Könnyen telepíthető
platform=Keresztplatformos
+platform_desc=A Gitea minden platformon fut, ahol a Go fordíthat: Windows, macOS, Linux, ARM, stb. Válassza azt, amelyet szereti!
lightweight=Könnyűsúlyú
license=Nyílt forráskódú
@@ -202,7 +211,12 @@ my_mirrors=Tükreim
view_home=Nézet %s
search_repos=Tároló keresés…
+show_archived=Archivált
+show_private=Privát
+show_both_private_public=Publikus és privát mutatása
+show_only_private=Csak privát mutatása
+show_only_public=Csak publikus mutatása
issues.in_your_repos=A tárolóidban
@@ -225,6 +239,7 @@ register_helper_msg=Van már felhasználói fiókja? Jelentkezzen be!
social_register_helper_msg=Van már felhasználói fiókja? Csatlakoztassa most!
disable_register_prompt=Regisztráció le van tiltva. Kérjük, lépjen kapcsolatba az oldal adminisztrátorával.
disable_register_mail=Ki van kapcsolva a visszaigazoló e-mail küldése a regisztrációnál.
+remember_me=Eszköz megjegyzése
forgot_password_title=Elfelejtett jelszó
forgot_password=Elfelejtette a jelszavát?
sign_up_now=Szeretne bejelentkezni? Regisztráljon most.
@@ -374,6 +389,7 @@ repositories=Tárolók
activity=Nyilvános tevékenységek
followers=Követők
starred=Csillagozott tárolók
+projects=Projektek
following=Követve
follow=Követés
unfollow=Követés törlése
@@ -417,6 +433,7 @@ continue=Folytatás
cancel=Mégsem
language=Nyelv
ui=Téma
+privacy=Adatvédelem
lookup_avatar_by_mail=Avatar mutatása email cím alapján
federated_avatar_lookup=Összevont profilkép keresés
@@ -480,6 +497,7 @@ subkeys=Alkulcsok
key_id=Kulcs ID
key_name=Kulcs neve
key_content=Tartalom
+principal_content=Tartalom
add_key_success=A SSH kulcsod sikeresen hozzáadva: '%s'
add_gpg_key_success=A GPG kulcsod sikeresen hozzáadva: '%s'
delete_key=Eltávolítás
@@ -511,6 +529,7 @@ new_token_desc=A tokent használó alkalmazásoknak teljes hozzáférése van a
token_name=Token neve
generate_token=Token generálása
generate_token_success=Új token létrehozva. Másold le most, mivel többször nem fog megjelenni.
+generate_token_name_duplicate=A %s nevet már használja egy alkalmazás. Válassz kérlek más nevet.
delete_token=Törlés
access_token_deletion=Hozzáférési Token Törlése
access_token_deletion_desc=Egy token törlésével visszavonja a hozzáférést a fiókjához az ezt használó alkalmazásoktól. Folytatja?
@@ -548,6 +567,7 @@ twofa_is_enrolled=A fiókja jelenleg használ kétlépcsős hit
twofa_not_enrolled=A fiókja jelenleg nem használ kétlépcsős hitelesítést.
twofa_disable=Kétlépcsős hitelesítés letiltása
twofa_scratch_token_regenerate=Kaparós kód újragenerálása
+twofa_enroll=Kétlépcsős hitelesítés használata
twofa_disable_note=A kétlépcsős azonosítás szükség esetén letiltható.
twofa_disable_desc=A kétlépcsős hitelesítés letiltása a fiókot kevésbé biztonságossá teszi. Folytatható?
twofa_disabled=Kétlépcsős hitelesítés letiltva.
@@ -623,9 +643,17 @@ reactions_more=és további %d
language_other=Egyéb
+desc.private=Privát
+desc.public=Nyilvános
+desc.private_template=Privát sablon
+desc.public_template=Sablon
+desc.internal=Belső
+desc.archived=Archivált
template.items=Sablon elemek
template.git_content=Git tartalom (alapértelmezett branch)
+template.git_hooks=Git Hook-ok
+template.webhooks=Webhook-ok
template.topics=Témák
template.avatar=Avatar
template.issue_labels=Hibajegy címkék
@@ -686,11 +714,14 @@ tags=Címkék
issues=Hibajegyek
pulls=Egyesítési kérések
labels=Címkék
+org_labels_desc_manage=kezelés
milestones=Mérföldkövek
commits=Commit-ok
commit=Commit
+release=Kiadás
releases=Kiadások
+tag=Címke
file_raw=Nyers
file_history=Előzmények
file_view_raw=Nyers fájl megtekintése
@@ -701,6 +732,7 @@ audio_not_supported_in_browser=A böngésző nem támogatja a HTML5 audio tag-et
stored_lfs=Git LFS-el eltárolva
symbolic_link=Szimbolikus hivatkozás
commit_graph=Commit gráf
+commit_graph.hide_pr_refs=Pull request-ek elrejtése
normal_view=Normál nézet
line=sor
lines=sor
@@ -763,12 +795,17 @@ ext_issues.desc=Külső hibakövető csatlakoztatás.
issues.desc=Hibajelentések, feladatok és mérföldkövek elrendezése.
+issues.filter_milestones=Mérföldkövek szűrése
+issues.filter_labels=Címkék szűrése
issues.new=Új hibajegy
issues.new.title_empty=A cím nem lehet üres
issues.new.labels=Címkék
+issues.new.add_labels_title=Címke alkalmazása
issues.new.no_label=Nincs címke
issues.new.clear_labels=Címkék kiürítése
+issues.new.no_items=Nincsenek elemek
issues.new.milestone=Mérföldkő
+issues.new.add_milestone_title=Mérföldkő beállítása
issues.new.no_milestone=Nincs mérföldkő
issues.new.clear_milestone=Mérföldkő eltávolítása
issues.new.open_milestone=Nyitott mérföldkövek
@@ -776,6 +813,8 @@ issues.new.closed_milestone=Lezárt mérföldkövek
issues.new.assignees=Megbízottak
issues.new.clear_assignees=Megbízottak eltávolítása
issues.new.no_assignees=Nincsenek megbízottak
+issues.new.no_reviewers=Nincs véleményező
+issues.new.add_reviewer_title=Véleményezés kérése
issues.no_ref=Nincsen ág/címke megadva
issues.create=Hibajegy létrehozása
issues.new_label=Új címke
@@ -851,6 +890,7 @@ issues.commit_ref_at=`hivatkozott erre a hibajegyre egy commit-ból Jelentkezz be hogy csatlakozz a beszélgetéshez.
issues.edit=Szerkesztés
issues.cancel=Mégsem
@@ -1160,6 +1200,8 @@ settings.add_team_duplicate=A csapat már rendelkezik a tárolóval
settings.add_team_success=A csapatnak most van hozzáférése a tárolóhoz.
settings.remove_team_success=A csapat hozzáférése a tárolóhoz törölve lett.
settings.add_webhook=Webhook hozzáadása
+settings.webhook_deletion=Webhook eltávolítása
+settings.webhook_deletion_success=A webhook el lett távolítva.
settings.webhook.test_delivery=Küldés Kipróbálása
settings.webhook.request=Kérés
settings.webhook.response=Válasz
@@ -1260,6 +1302,7 @@ diff.show_unified_view=Egyesített nézet
diff.stats_desc=%d fájl változott, egészen pontosan %d új sor hozzáadva és %d régi sor törölve
diff.bin=BINáris
diff.view_file=Fájl megtekintése
+diff.file_byte_size=Méret
diff.file_suppressed=A különbségek nem kerülnek megjelenítésre, mivel a fájl túl nagy
diff.too_many_files=Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott
diff.comment.placeholder=Hozzászólás létrehozása
@@ -1283,6 +1326,7 @@ release.cancel=Mégse
release.publish=Kiadás közzététele
release.save_draft=Piszkozat mentése
release.deletion_success=A kiadás törölve.
+release.deletion_tag_success=A cimke törölve lett.
release.tag_name_invalid=Ez a címkenév érvénytelen.
release.downloads=Letöltések
release.download_count=Letöltések: %s
@@ -1657,6 +1701,7 @@ config.session_life_time=Munkamenet Élettartama
config.https_only=Csak HTTPS
config.cookie_life_time=Süti Élettartam
+config.picture_config=Kép és Avatár Konfiguráció
config.picture_service=Kép Szolgáltatás
config.disable_gravatar=Gravatar Kikapcsolása
config.enable_federated_avatar=Összevont profilkép lekérés engedélyezése
@@ -1677,12 +1722,14 @@ config.log_config=Naplózási Beállítások
config.log_mode=Naplózási Módja
config.disabled_logger=Letiltva
config.access_log_template=Sablon
+config.xorm_log_sql=SQL naplózása
monitor.cron=Ütemezett Feladatok
monitor.name=Név
monitor.schedule=Ütemezés
monitor.next=Legközelebb
monitor.previous=Legutóbb
+monitor.execute_times=Végrehajtások
monitor.process=Futó Folyamatok
monitor.desc=Leírás
monitor.start=Kezdés Időpontja
@@ -1706,6 +1753,7 @@ notices.delete_selected=Kiválasztottak Törlése
notices.delete_all=Minden Értesítés Törlése
notices.type=Típus
notices.type_1=Tároló
+notices.type_2=Feladat
notices.desc=Leírás
notices.op=Op.
notices.delete_success=A rendszer-értesítések törölve lettek.
@@ -1724,6 +1772,7 @@ merge_pull_request=`végrehajtott egy egyesítési kérést: %s
delete_tag=címke %[2]s törölve innen: %[3]s
delete_branch=ág %[2]s törölve innen: %[3]s
+compare_branch=Összehasonlítás
compare_commits=%d commit összehasonlítása
compare_commits_general=Commitok összehasonlítása
diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index 86fb628e5f..e7d2113452 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -19,6 +19,7 @@ create_new=Buat…
user_profile_and_more=Profil dan Pengaturan…
signed_in_as=Masuk sebagai
enable_javascript=Situs web ini bekerja lebih baik dengan JavaScript.
+toc=Daftar Isi
username=Nama Pengguna
email=Alamat Email
@@ -70,6 +71,7 @@ issues=Masalah
milestones=Tonggak
cancel=Batal
+save=Simpan
add=Tambah
add_all=Tambah Semua
remove=Buang
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index 43f70d8305..a59531b2b6 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -836,6 +836,7 @@ commits=Commit
commit=Commit
release=Rilascio
releases=Rilasci
+tag=Etichetta
file_raw=Originale
file_history=Cronologia
file_view_source=Visualizza sorgente
@@ -957,6 +958,7 @@ projects.board.edit_title=Nuovo Nome Della Scheda
projects.board.new_title=Nuovo Nome Della Scheda
projects.board.new_submit=Invia
projects.board.new=Nuova Scheda
+projects.board.set_default=Imposta come predefinito
projects.board.delete=Elimina Scheda
projects.board.deletion_desc=L'eliminazione di una scheda di progetto sposta tutti i problemi correlati a 'Uncategorized'. Continuare?
projects.open=Apri
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index f4a05a7e49..e0b195d5e8 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -50,6 +50,8 @@ new_migrate=새 마이그레이션
new_mirror=새로운 미러
new_fork=새 저장소 포크
new_org=새로운 조직
+new_project=새 프로젝트
+new_project_board=새 프로젝트 보드
manage_org=조직 관리
admin_panel=사이트 관리
account_settings=계정 설정
@@ -70,6 +72,7 @@ issues=이슈들
milestones=마일스톤
cancel=취소
+save=저장
add=추가
add_all=모두 추가
remove=삭제
@@ -83,6 +86,7 @@ loading=불러오는 중...
[error]
+occurred=오류가 발생했습니다
[startpage]
app_desc=편리한 설치형 Git 서비스
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index 91492b0aab..52436a6bc1 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -15,6 +15,7 @@ page=Pagina
template=Sjabloon
language=Taal
notifications=Meldingen
+active_stopwatch=Actieve Tijd Tracker
create_new=Maken…
user_profile_and_more=Profiel en instellingen…
signed_in_as=Aangemeld als
@@ -75,6 +76,7 @@ pull_requests=Pull requests
issues=Kwesties
milestones=Mijlpalen
+ok=OK
cancel=Annuleren
save=Opslaan
add=Toevoegen
@@ -86,6 +88,8 @@ write=Schrijf
preview=Voorbeeld
loading=Laden…
+step1=Stap 1:
+step2=Stap 2:
error404=De pagina die u probeert te bereiken bestaat niet of u bent niet gemachtigd om het te bekijken.
@@ -202,6 +206,7 @@ default_enable_timetracking=Tijdregistratie standaard inschakelen
default_enable_timetracking_popup=Tijdsregistratie voor nieuwe repositories standaard inschakelen.
no_reply_address=Verborgen e-maildomein
no_reply_address_helper=Domeinnaam voor gebruikers met een verborgen e-mailadres. Bijvoorbeeld zal de gebruikersnaam 'joe' in Git worden geregistreerd als 'joe@noreply.example.org' als het verborgen email domein is ingesteld op 'noreply.example.org'.
+password_algorithm=Wachtwoord Hash Algoritme
[home]
uname_holder=Gebruikersnaam of e-mailadres
@@ -215,6 +220,7 @@ my_mirrors=Mijn kopieën
view_home=Bekijk %s
search_repos=Zoek een repository…
filter=Andere filters
+filter_by_team_repositories=Filter op team repositories
show_archived=Gearchiveerd
show_both_archived_unarchived=Toont zowel gearchiveerd als niet-gearchiveerd
@@ -316,8 +322,12 @@ register_success=Registratie succesvol
+release.new.subject=%s in %s vrijgegeven
+repo.transfer.subject_to=%s zou "%s" willen overdragen aan %s
+repo.transfer.subject_to_you=%s wil "%s" aan jou overdragen
+repo.collaborator.added.subject=%s heeft jou toegevoegd aan %s
[modal]
yes=Ja
@@ -364,6 +374,7 @@ password_not_match=De wachtwoorden komen niet overeen.
lang_select_error=Selecteer een taal uit de lijst.
username_been_taken=Deze naam is al in gebruik.
+username_change_not_local_user=Niet-lokale gebruikers mogen hun gebruikersnaam niet wijzigen.
repo_name_been_taken=De repository-naam wordt al gebruikt.
repository_files_already_exist=Er bestaan al bestanden voor deze repository. Neem contact op met de systeembeheerder.
repository_files_already_exist.adopt=Bestanden bestaan al voor deze repository en kunnen alleen worden geadopteerd.
@@ -659,6 +670,7 @@ email_notifications.submit=E-mailvoorkeur instellen
[repo]
owner=Eigenaar
+owner_helper=Sommige organisaties kunnen niet worden weergegeven in de dropdown vanwege een limiet op het maximale aantal repositories.
repo_name=Naam van repository
repo_name_helper=Goede repository-namen zijn kort, makkelijk te onthouden en uniek.
repo_size=Repositorygrootte
@@ -679,6 +691,7 @@ use_template=Gebruik dit sjabloon
generate_repo=Repository genereren
generate_from=Genereer van
repo_desc=Omschrijving
+repo_desc_helper=Voer korte beschrijving in (optioneel)
repo_lang=Taal
repo_gitignore_helper=Selecteer .gitignore templates.
issue_labels=Issuelabels
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index 902b9b2512..da0f2192a7 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -101,6 +101,7 @@ occurred=發生錯誤
report_message=如果你確定這是一個 Gitea 的 bug,請去 GitHub 搜尋相關的問題,如果有需要你也可以開一個新的問題
[startpage]
+app_desc=一套極易架設的 Git 服務
install=安裝容易
install_desc=簡單地執行您平台的二進位檔 ,或是使用 Docker ,你也可以從套件管理員 安裝。
platform=跨平台
@@ -262,10 +263,10 @@ forgot_password_title=忘記密碼
forgot_password=忘記密碼?
sign_up_now=還沒有帳戶?馬上註冊。
sign_up_successful=帳戶已成功建立。
-confirmation_mail_sent_prompt=一封新的確認信已發送至 %s 。請檢查您的收件匣,並在 %s 內完成註冊作業。
+confirmation_mail_sent_prompt=新的確認信已發送至 %s 。請在 %s內檢查您的收件匣並完成註冊作業。
must_change_password=更新您的密碼
allow_password_change=要求使用者更改密碼 (推薦)
-reset_password_mail_sent_prompt=一封確認信已發送至 %s 。請檢查您的收件匣,並在 %s 內完成帳戶救援作業。
+reset_password_mail_sent_prompt=確認信已發送至 %s 。請在 %s內檢查您的收件匣並完成帳戶救援作業。
active_your_account=啟用您的帳戶
account_activated=帳戶已啟用
prohibit_login=禁止登入
@@ -525,7 +526,7 @@ add_new_email=新增電子信箱
add_new_openid=新增 OpenID URI
add_email=新增電子信箱
add_openid=新增 OpenID URI
-add_email_confirmation_sent=一封新的確認郵件已發送至 '%s',請檢查您的收件匣並在 %s 內確認您的電郵地址。
+add_email_confirmation_sent=確認信已發送至「%s」,請在 %s內檢查您的收件匣並確認您的電子信箱。
add_email_success=已加入新的電子信箱。
email_preference_set_success=已套用郵件偏好設定
add_openid_success=該 OpenID 已添加。
From 7a0ed9a0469b24768c9041e137bfcd2d28f05319 Mon Sep 17 00:00:00 2001
From: Martin Strob
Date: Fri, 25 Jun 2021 14:38:41 +0200
Subject: [PATCH 17/33] fix IIS reverse proxy doc (#16246)
---
docs/content/doc/usage/reverse-proxies.en-us.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/docs/content/doc/usage/reverse-proxies.en-us.md b/docs/content/doc/usage/reverse-proxies.en-us.md
index e2fdb1d2b7..b339048d68 100644
--- a/docs/content/doc/usage/reverse-proxies.en-us.md
+++ b/docs/content/doc/usage/reverse-proxies.en-us.md
@@ -221,6 +221,9 @@ If you wish to run Gitea with IIS. You will need to setup IIS with URL Rewrite a
```xml
+
+
+
From 44b8b07631666e3ae691149bdba31ca0f51569f5 Mon Sep 17 00:00:00 2001
From: KN4CK3R
Date: Fri, 25 Jun 2021 16:28:55 +0200
Subject: [PATCH 18/33] Add tag protection (#15629)
* Added tag protection in hook.
* Prevent UI tag creation if protected.
* Added settings page.
* Added tests.
* Added suggestions.
* Moved tests.
* Use individual errors.
* Removed unneeded methods.
* Switched delete selector.
* Changed method names.
* No reason to be unique.
* Allow editing of protected tags.
* Removed unique key from migration.
* Added docs page.
* Changed date.
* Respond with 404 to not found tags.
* Replaced glob with regex pattern.
* Added support for glob and regex pattern.
* Updated documentation.
* Changed white* to allow*.
* Fixed edit button link.
* Added cancel button.
Co-authored-by: zeripath
Co-authored-by: Lunny Xiao
---
cmd/hook.go | 8 +-
.../doc/advanced/protected-tags.en-us.md | 57 +++
integrations/mirror_pull_test.go | 2 +
integrations/repo_tag_test.go | 74 ++++
models/error.go | 15 +
models/migrations/migrations.go | 2 +
models/migrations/v186.go | 26 ++
models/models.go | 1 +
models/protected_tag.go | 131 +++++++
models/protected_tag_test.go | 162 ++++++++
models/repo.go | 1 +
modules/validation/binding.go | 59 ++-
modules/validation/binding_test.go | 7 +-
modules/validation/regex_pattern_test.go | 60 +++
modules/web/middleware/binding.go | 2 +
options/locale/locale_en-US.ini | 18 +-
routers/private/hook.go | 367 ++++++++++--------
routers/web/repo/release.go | 16 +
routers/web/repo/setting.go | 1 +
routers/web/repo/tag.go | 182 +++++++++
routers/web/web.go | 9 +
services/forms/repo_tag_form.go | 27 ++
services/release/release.go | 27 +-
services/release/release_test.go | 26 ++
templates/repo/settings/nav.tmpl | 1 +
templates/repo/settings/navbar.tmpl | 3 +
templates/repo/settings/tags.tmpl | 132 +++++++
27 files changed, 1227 insertions(+), 189 deletions(-)
create mode 100644 docs/content/doc/advanced/protected-tags.en-us.md
create mode 100644 integrations/repo_tag_test.go
create mode 100644 models/migrations/v186.go
create mode 100644 models/protected_tag.go
create mode 100644 models/protected_tag_test.go
create mode 100644 modules/validation/regex_pattern_test.go
create mode 100644 routers/web/repo/tag.go
create mode 100644 services/forms/repo_tag_form.go
create mode 100644 templates/repo/settings/tags.tmpl
diff --git a/cmd/hook.go b/cmd/hook.go
index 312c9a14fc..2fbbfb4d21 100644
--- a/cmd/hook.go
+++ b/cmd/hook.go
@@ -221,8 +221,8 @@ Gitea or set your environment appropriately.`, "")
total++
lastline++
- // If the ref is a branch, check if it's protected
- if strings.HasPrefix(refFullName, git.BranchPrefix) {
+ // If the ref is a branch or tag, check if it's protected
+ if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
oldCommitIDs[count] = oldCommitID
newCommitIDs[count] = newCommitID
refFullNames[count] = refFullName
@@ -230,7 +230,7 @@ Gitea or set your environment appropriately.`, "")
fmt.Fprintf(out, "*")
if count >= hookBatchSize {
- fmt.Fprintf(out, " Checking %d branches\n", count)
+ fmt.Fprintf(out, " Checking %d references\n", count)
hookOptions.OldCommitIDs = oldCommitIDs
hookOptions.NewCommitIDs = newCommitIDs
@@ -261,7 +261,7 @@ Gitea or set your environment appropriately.`, "")
hookOptions.NewCommitIDs = newCommitIDs[:count]
hookOptions.RefFullNames = refFullNames[:count]
- fmt.Fprintf(out, " Checking %d branches\n", count)
+ fmt.Fprintf(out, " Checking %d references\n", count)
statusCode, msg := private.HookPreReceive(username, reponame, hookOptions)
switch statusCode {
diff --git a/docs/content/doc/advanced/protected-tags.en-us.md b/docs/content/doc/advanced/protected-tags.en-us.md
new file mode 100644
index 0000000000..36e6e16975
--- /dev/null
+++ b/docs/content/doc/advanced/protected-tags.en-us.md
@@ -0,0 +1,57 @@
+---
+date: "2021-05-14T00:00:00-00:00"
+title: "Protected tags"
+slug: "protected-tags"
+weight: 45
+toc: false
+draft: false
+menu:
+ sidebar:
+ parent: "advanced"
+ name: "Protected tags"
+ weight: 45
+ identifier: "protected-tags"
+---
+
+# Protected tags
+
+Protected tags allow control over who has permission to create or update git tags. Each rule allows you to match either an individual tag name, or use an appropriate pattern to control multiple tags at once.
+
+**Table of Contents**
+
+{{< toc >}}
+
+## Setting up protected tags
+
+To protect a tag, you need to follow these steps:
+
+1. Go to the repository’s **Settings** > **Tags** page.
+1. Type a pattern to match a name. You can use a single name, a [glob pattern](https://pkg.go.dev/github.com/gobwas/glob#Compile) or a regular expression.
+1. Choose the allowed users and/or teams. If you leave these fields empty noone is allowed to create or modify this tag.
+1. Select **Save** to save the configuration.
+
+## Pattern protected tags
+
+The pattern uses [glob](https://pkg.go.dev/github.com/gobwas/glob#Compile) or regular expressions to match a tag name. For regular expressions you need to enclose the pattern in slashes.
+
+Examples:
+
+| Type | Pattern Protected Tag | Possible Matching Tags |
+| ----- | ------------------------ | --------------------------------------- |
+| Glob | `v*` | `v`, `v-1`, `version2` |
+| Glob | `v[0-9]` | `v0`, `v1` up to `v9` |
+| Glob | `*-release` | `2.1-release`, `final-release` |
+| Glob | `gitea` | only `gitea` |
+| Glob | `*gitea*` | `gitea`, `2.1-gitea`, `1_gitea-release` |
+| Glob | `{v,rel}-*` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` |
+| Glob | `*` | matches all possible tag names |
+| Regex | `/\Av/` | `v`, `v-1`, `version2` |
+| Regex | `/\Av[0-9]\z/` | `v0`, `v1` up to `v9` |
+| Regex | `/\Av\d+\.\d+\.\d+\z/` | `v1.0.17`, `v2.1.0` |
+| Regex | `/\Av\d+(\.\d+){0,2}\z/` | `v1`, `v2.1`, `v1.2.34` |
+| Regex | `/-release\z/` | `2.1-release`, `final-release` |
+| Regex | `/gitea/` | `gitea`, `2.1-gitea`, `1_gitea-release` |
+| Regex | `/\Agitea\z/` | only `gitea` |
+| Regex | `/^gitea$/` | only `gitea` |
+| Regex | `/\A(v\|rel)-/` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` |
+| Regex | `/.+/` | matches all possible tag names |
diff --git a/integrations/mirror_pull_test.go b/integrations/mirror_pull_test.go
index 0e4da74fcf..3908f35557 100644
--- a/integrations/mirror_pull_test.go
+++ b/integrations/mirror_pull_test.go
@@ -59,7 +59,9 @@ func TestMirrorPull(t *testing.T) {
assert.NoError(t, release_service.CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v0.2",
Target: "master",
Title: "v0.2 is released",
diff --git a/integrations/repo_tag_test.go b/integrations/repo_tag_test.go
new file mode 100644
index 0000000000..eb3f2b47fb
--- /dev/null
+++ b/integrations/repo_tag_test.go
@@ -0,0 +1,74 @@
+// Copyright 2021 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 integrations
+
+import (
+ "io/ioutil"
+ "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/release"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCreateNewTagProtected(t *testing.T) {
+ defer prepareTestEnv(t)()
+
+ repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+ owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
+
+ t.Run("API", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ err := release.CreateNewTag(owner, repo, "master", "v-1", "first tag")
+ assert.NoError(t, err)
+
+ err = models.InsertProtectedTag(&models.ProtectedTag{
+ RepoID: repo.ID,
+ NamePattern: "v-*",
+ })
+ assert.NoError(t, err)
+ err = models.InsertProtectedTag(&models.ProtectedTag{
+ RepoID: repo.ID,
+ NamePattern: "v-1.1",
+ AllowlistUserIDs: []int64{repo.OwnerID},
+ })
+ assert.NoError(t, err)
+
+ err = release.CreateNewTag(owner, repo, "master", "v-2", "second tag")
+ assert.Error(t, err)
+ assert.True(t, models.IsErrProtectedTagName(err))
+
+ err = release.CreateNewTag(owner, repo, "master", "v-1.1", "third tag")
+ assert.NoError(t, err)
+ })
+
+ t.Run("Git", func(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ username := "user2"
+ httpContext := NewAPITestContext(t, username, "repo1")
+
+ dstPath, err := ioutil.TempDir("", httpContext.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath)
+
+ u.Path = httpContext.GitPath()
+ u.User = url.UserPassword(username, userPassword)
+
+ doGitClone(dstPath, u)(t)
+
+ _, err = git.NewCommand("tag", "v-2").RunInDir(dstPath)
+ assert.NoError(t, err)
+
+ _, err = git.NewCommand("push", "--tags").RunInDir(dstPath)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "Tag v-2 is protected")
+ })
+ })
+}
diff --git a/models/error.go b/models/error.go
index 501bf86869..513effdb02 100644
--- a/models/error.go
+++ b/models/error.go
@@ -985,6 +985,21 @@ func (err ErrInvalidTagName) Error() string {
return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
}
+// ErrProtectedTagName represents a "ProtectedTagName" kind of error.
+type ErrProtectedTagName struct {
+ TagName string
+}
+
+// IsErrProtectedTagName checks if an error is a ErrProtectedTagName.
+func IsErrProtectedTagName(err error) bool {
+ _, ok := err.(ErrProtectedTagName)
+ return ok
+}
+
+func (err ErrProtectedTagName) Error() string {
+ return fmt.Sprintf("release tag name is protected [tag_name: %s]", err.TagName)
+}
+
// ErrRepoFileAlreadyExists represents a "RepoFileAlreadyExist" kind of error.
type ErrRepoFileAlreadyExists struct {
Path string
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 4e17a6a2c8..978ba6368f 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -321,6 +321,8 @@ var migrations = []Migration{
NewMigration("Rename Task errors to message", renameTaskErrorsToMessage),
// v185 -> v186
NewMigration("Add new table repo_archiver", addRepoArchiver),
+ // v186 -> v187
+ NewMigration("Create protected tag table", createProtectedTagTable),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v186.go b/models/migrations/v186.go
new file mode 100644
index 0000000000..eb6ec7118c
--- /dev/null
+++ b/models/migrations/v186.go
@@ -0,0 +1,26 @@
+// Copyright 2021 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 (
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/xorm"
+)
+
+func createProtectedTagTable(x *xorm.Engine) error {
+ type ProtectedTag struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64
+ NamePattern string
+ AllowlistUserIDs []int64 `xorm:"JSON TEXT"`
+ AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
+
+ CreatedUnix timeutil.TimeStamp `xorm:"created"`
+ UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
+ }
+
+ return x.Sync2(new(ProtectedTag))
+}
diff --git a/models/models.go b/models/models.go
index 3266be0f4a..610933d327 100644
--- a/models/models.go
+++ b/models/models.go
@@ -137,6 +137,7 @@ func init() {
new(IssueIndex),
new(PushMirror),
new(RepoArchiver),
+ new(ProtectedTag),
)
gonicNames := []string{"SSL", "UID"}
diff --git a/models/protected_tag.go b/models/protected_tag.go
new file mode 100644
index 0000000000..88f20dd29a
--- /dev/null
+++ b/models/protected_tag.go
@@ -0,0 +1,131 @@
+// Copyright 2021 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 models
+
+import (
+ "regexp"
+ "strings"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/gobwas/glob"
+)
+
+// ProtectedTag struct
+type ProtectedTag struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64
+ NamePattern string
+ RegexPattern *regexp.Regexp `xorm:"-"`
+ GlobPattern glob.Glob `xorm:"-"`
+ AllowlistUserIDs []int64 `xorm:"JSON TEXT"`
+ AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
+
+ CreatedUnix timeutil.TimeStamp `xorm:"created"`
+ UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
+}
+
+// InsertProtectedTag inserts a protected tag to database
+func InsertProtectedTag(pt *ProtectedTag) error {
+ _, err := x.Insert(pt)
+ return err
+}
+
+// UpdateProtectedTag updates the protected tag
+func UpdateProtectedTag(pt *ProtectedTag) error {
+ _, err := x.ID(pt.ID).AllCols().Update(pt)
+ return err
+}
+
+// DeleteProtectedTag deletes a protected tag by ID
+func DeleteProtectedTag(pt *ProtectedTag) error {
+ _, err := x.ID(pt.ID).Delete(&ProtectedTag{})
+ return err
+}
+
+// EnsureCompiledPattern ensures the glob pattern is compiled
+func (pt *ProtectedTag) EnsureCompiledPattern() error {
+ if pt.RegexPattern != nil || pt.GlobPattern != nil {
+ return nil
+ }
+
+ var err error
+ if len(pt.NamePattern) >= 2 && strings.HasPrefix(pt.NamePattern, "/") && strings.HasSuffix(pt.NamePattern, "/") {
+ pt.RegexPattern, err = regexp.Compile(pt.NamePattern[1 : len(pt.NamePattern)-1])
+ } else {
+ pt.GlobPattern, err = glob.Compile(pt.NamePattern)
+ }
+ return err
+}
+
+// IsUserAllowed returns true if the user is allowed to modify the tag
+func (pt *ProtectedTag) IsUserAllowed(userID int64) (bool, error) {
+ if base.Int64sContains(pt.AllowlistUserIDs, userID) {
+ return true, nil
+ }
+
+ if len(pt.AllowlistTeamIDs) == 0 {
+ return false, nil
+ }
+
+ in, err := IsUserInTeams(userID, pt.AllowlistTeamIDs)
+ if err != nil {
+ return false, err
+ }
+ return in, nil
+}
+
+// GetProtectedTags gets all protected tags of the repository
+func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) {
+ tags := make([]*ProtectedTag, 0)
+ return tags, x.Find(&tags, &ProtectedTag{RepoID: repo.ID})
+}
+
+// GetProtectedTagByID gets the protected tag with the specific id
+func GetProtectedTagByID(id int64) (*ProtectedTag, error) {
+ tag := new(ProtectedTag)
+ has, err := x.ID(id).Get(tag)
+ if err != nil {
+ return nil, err
+ }
+ if !has {
+ return nil, nil
+ }
+ return tag, nil
+}
+
+// IsUserAllowedToControlTag checks if a user can control the specific tag.
+// It returns true if the tag name is not protected or the user is allowed to control it.
+func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int64) (bool, error) {
+ isAllowed := true
+ for _, tag := range tags {
+ err := tag.EnsureCompiledPattern()
+ if err != nil {
+ return false, err
+ }
+
+ if !tag.matchString(tagName) {
+ continue
+ }
+
+ isAllowed, err = tag.IsUserAllowed(userID)
+ if err != nil {
+ return false, err
+ }
+ if isAllowed {
+ break
+ }
+ }
+
+ return isAllowed, nil
+}
+
+func (pt *ProtectedTag) matchString(name string) bool {
+ if pt.RegexPattern != nil {
+ return pt.RegexPattern.MatchString(name)
+ }
+ return pt.GlobPattern.Match(name)
+}
diff --git a/models/protected_tag_test.go b/models/protected_tag_test.go
new file mode 100644
index 0000000000..3dc895c69f
--- /dev/null
+++ b/models/protected_tag_test.go
@@ -0,0 +1,162 @@
+// Copyright 2021 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 models
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIsUserAllowed(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+
+ pt := &ProtectedTag{}
+ allowed, err := pt.IsUserAllowed(1)
+ assert.NoError(t, err)
+ assert.False(t, allowed)
+
+ pt = &ProtectedTag{
+ AllowlistUserIDs: []int64{1},
+ }
+ allowed, err = pt.IsUserAllowed(1)
+ assert.NoError(t, err)
+ assert.True(t, allowed)
+
+ allowed, err = pt.IsUserAllowed(2)
+ assert.NoError(t, err)
+ assert.False(t, allowed)
+
+ pt = &ProtectedTag{
+ AllowlistTeamIDs: []int64{1},
+ }
+ allowed, err = pt.IsUserAllowed(1)
+ assert.NoError(t, err)
+ assert.False(t, allowed)
+
+ allowed, err = pt.IsUserAllowed(2)
+ assert.NoError(t, err)
+ assert.True(t, allowed)
+
+ pt = &ProtectedTag{
+ AllowlistUserIDs: []int64{1},
+ AllowlistTeamIDs: []int64{1},
+ }
+ allowed, err = pt.IsUserAllowed(1)
+ assert.NoError(t, err)
+ assert.True(t, allowed)
+
+ allowed, err = pt.IsUserAllowed(2)
+ assert.NoError(t, err)
+ assert.True(t, allowed)
+}
+
+func TestIsUserAllowedToControlTag(t *testing.T) {
+ cases := []struct {
+ name string
+ userid int64
+ allowed bool
+ }{
+ {
+ name: "test",
+ userid: 1,
+ allowed: true,
+ },
+ {
+ name: "test",
+ userid: 3,
+ allowed: true,
+ },
+ {
+ name: "gitea",
+ userid: 1,
+ allowed: true,
+ },
+ {
+ name: "gitea",
+ userid: 3,
+ allowed: false,
+ },
+ {
+ name: "test-gitea",
+ userid: 1,
+ allowed: true,
+ },
+ {
+ name: "test-gitea",
+ userid: 3,
+ allowed: false,
+ },
+ {
+ name: "gitea-test",
+ userid: 1,
+ allowed: true,
+ },
+ {
+ name: "gitea-test",
+ userid: 3,
+ allowed: true,
+ },
+ {
+ name: "v-1",
+ userid: 1,
+ allowed: false,
+ },
+ {
+ name: "v-1",
+ userid: 2,
+ allowed: true,
+ },
+ {
+ name: "release",
+ userid: 1,
+ allowed: false,
+ },
+ }
+
+ t.Run("Glob", func(t *testing.T) {
+ protectedTags := []*ProtectedTag{
+ {
+ NamePattern: `*gitea`,
+ AllowlistUserIDs: []int64{1},
+ },
+ {
+ NamePattern: `v-*`,
+ AllowlistUserIDs: []int64{2},
+ },
+ {
+ NamePattern: "release",
+ },
+ }
+
+ for n, c := range cases {
+ isAllowed, err := IsUserAllowedToControlTag(protectedTags, c.name, c.userid)
+ assert.NoError(t, err)
+ assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n)
+ }
+ })
+
+ t.Run("Regex", func(t *testing.T) {
+ protectedTags := []*ProtectedTag{
+ {
+ NamePattern: `/gitea\z/`,
+ AllowlistUserIDs: []int64{1},
+ },
+ {
+ NamePattern: `/\Av-/`,
+ AllowlistUserIDs: []int64{2},
+ },
+ {
+ NamePattern: "/release/",
+ },
+ }
+
+ for n, c := range cases {
+ isAllowed, err := IsUserAllowedToControlTag(protectedTags, c.name, c.userid)
+ assert.NoError(t, err)
+ assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n)
+ }
+ })
+}
diff --git a/models/repo.go b/models/repo.go
index 2baf6e9bdd..4ce3d4839b 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -1498,6 +1498,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
&Mirror{RepoID: repoID},
&Notification{RepoID: repoID},
&ProtectedBranch{RepoID: repoID},
+ &ProtectedTag{RepoID: repoID},
&PullRequest{BaseRepoID: repoID},
&PushMirror{RepoID: repoID},
&Release{RepoID: repoID},
diff --git a/modules/validation/binding.go b/modules/validation/binding.go
index 5cfd994d2d..4cef48daf3 100644
--- a/modules/validation/binding.go
+++ b/modules/validation/binding.go
@@ -19,6 +19,9 @@ const (
// ErrGlobPattern is returned when glob pattern is invalid
ErrGlobPattern = "GlobPattern"
+
+ // ErrRegexPattern is returned when a regex pattern is invalid
+ ErrRegexPattern = "RegexPattern"
)
var (
@@ -53,6 +56,8 @@ func AddBindingRules() {
addGitRefNameBindingRule()
addValidURLBindingRule()
addGlobPatternRule()
+ addRegexPatternRule()
+ addGlobOrRegexPatternRule()
}
func addGitRefNameBindingRule() {
@@ -102,17 +107,55 @@ func addGlobPatternRule() {
IsMatch: func(rule string) bool {
return rule == "GlobPattern"
},
+ IsValid: globPatternValidator,
+ })
+}
+
+func globPatternValidator(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
+ str := fmt.Sprintf("%v", val)
+
+ if len(str) != 0 {
+ if _, err := glob.Compile(str); err != nil {
+ errs.Add([]string{name}, ErrGlobPattern, err.Error())
+ return false, errs
+ }
+ }
+
+ return true, errs
+}
+
+func addRegexPatternRule() {
+ binding.AddRule(&binding.Rule{
+ IsMatch: func(rule string) bool {
+ return rule == "RegexPattern"
+ },
+ IsValid: regexPatternValidator,
+ })
+}
+
+func regexPatternValidator(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
+ str := fmt.Sprintf("%v", val)
+
+ if _, err := regexp.Compile(str); err != nil {
+ errs.Add([]string{name}, ErrRegexPattern, err.Error())
+ return false, errs
+ }
+
+ return true, errs
+}
+
+func addGlobOrRegexPatternRule() {
+ binding.AddRule(&binding.Rule{
+ IsMatch: func(rule string) bool {
+ return rule == "GlobOrRegexPattern"
+ },
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
- str := fmt.Sprintf("%v", val)
+ str := strings.TrimSpace(fmt.Sprintf("%v", val))
- if len(str) != 0 {
- if _, err := glob.Compile(str); err != nil {
- errs.Add([]string{name}, ErrGlobPattern, err.Error())
- return false, errs
- }
+ if len(str) >= 2 && strings.HasPrefix(str, "/") && strings.HasSuffix(str, "/") {
+ return regexPatternValidator(errs, name, str[1:len(str)-1])
}
-
- return true, errs
+ return globPatternValidator(errs, name, val)
},
})
}
diff --git a/modules/validation/binding_test.go b/modules/validation/binding_test.go
index e0daba89e5..d3b4e686ae 100644
--- a/modules/validation/binding_test.go
+++ b/modules/validation/binding_test.go
@@ -26,9 +26,10 @@ type (
}
TestForm struct {
- BranchName string `form:"BranchName" binding:"GitRefName"`
- URL string `form:"ValidUrl" binding:"ValidUrl"`
- GlobPattern string `form:"GlobPattern" binding:"GlobPattern"`
+ BranchName string `form:"BranchName" binding:"GitRefName"`
+ URL string `form:"ValidUrl" binding:"ValidUrl"`
+ GlobPattern string `form:"GlobPattern" binding:"GlobPattern"`
+ RegexPattern string `form:"RegexPattern" binding:"RegexPattern"`
}
)
diff --git a/modules/validation/regex_pattern_test.go b/modules/validation/regex_pattern_test.go
new file mode 100644
index 0000000000..afe1bcf425
--- /dev/null
+++ b/modules/validation/regex_pattern_test.go
@@ -0,0 +1,60 @@
+// Copyright 2021 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 validation
+
+import (
+ "regexp"
+ "testing"
+
+ "gitea.com/go-chi/binding"
+)
+
+func getRegexPatternErrorString(pattern string) string {
+ if _, err := regexp.Compile(pattern); err != nil {
+ return err.Error()
+ }
+ return ""
+}
+
+var regexValidationTestCases = []validationTestCase{
+ {
+ description: "Empty regex pattern",
+ data: TestForm{
+ RegexPattern: "",
+ },
+ expectedErrors: binding.Errors{},
+ },
+ {
+ description: "Valid regex",
+ data: TestForm{
+ RegexPattern: `(\d{1,3})+`,
+ },
+ expectedErrors: binding.Errors{},
+ },
+
+ {
+ description: "Invalid regex",
+ data: TestForm{
+ RegexPattern: "[a-",
+ },
+ expectedErrors: binding.Errors{
+ binding.Error{
+ FieldNames: []string{"RegexPattern"},
+ Classification: ErrRegexPattern,
+ Message: getRegexPatternErrorString("[a-"),
+ },
+ },
+ },
+}
+
+func Test_RegexPatternValidation(t *testing.T) {
+ AddBindingRules()
+
+ for _, testCase := range regexValidationTestCases {
+ t.Run(testCase.description, func(t *testing.T) {
+ performValidationTest(t, testCase)
+ })
+ }
+}
diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go
index cd418c9792..cbdb29b812 100644
--- a/modules/web/middleware/binding.go
+++ b/modules/web/middleware/binding.go
@@ -135,6 +135,8 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl
data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field))
case validation.ErrGlobPattern:
data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message)
+ case validation.ErrRegexPattern:
+ data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
default:
data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 59ee6e48ea..a809f49eeb 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -83,6 +83,7 @@ add = Add
add_all = Add All
remove = Remove
remove_all = Remove All
+edit = Edit
write = Write
preview = Preview
@@ -415,6 +416,7 @@ email_error = ` is not a valid email address.`
url_error = ` is not a valid URL.`
include_error = ` must contain substring '%s'.`
glob_pattern_error = ` glob pattern is invalid: %s.`
+regex_pattern_error = ` regex pattern is invalid: %s.`
unknown_error = Unknown error:
captcha_incorrect = The CAPTCHA code is incorrect.
password_not_match = The passwords do not match.
@@ -1802,7 +1804,7 @@ settings.event_pull_request_review_desc = Pull request approved, rejected, or re
settings.event_pull_request_sync = Pull Request Synchronized
settings.event_pull_request_sync_desc = Pull request synchronized.
settings.branch_filter = Branch filter
-settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or *
, events for all branches are reported. See github.com/gobwas/glob documentation for syntax. Examples: master
, {master,release*}
.
+settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or *
, events for all branches are reported. See github.com/gobwas/glob documentation for syntax. Examples: master
, {master,release*}
.
settings.active = Active
settings.active_helper = Information about triggered events will be sent to this webhook URL.
settings.add_hook_success = The webhook has been added.
@@ -1872,7 +1874,7 @@ settings.dismiss_stale_approvals_desc = When new commits that change the content
settings.require_signed_commits = Require Signed Commits
settings.require_signed_commits_desc = Reject pushes to this branch if they are unsigned or unverifiable.
settings.protect_protected_file_patterns = Protected file patterns (separated using semicolon '\;'):
-settings.protect_protected_file_patterns_desc = Protected files that are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon ('\;'). See github.com/gobwas/glob documentation for pattern syntax. Examples: .drone.yml
, /docs/**/*.txt
.
+settings.protect_protected_file_patterns_desc = Protected files that are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon ('\;'). See github.com/gobwas/glob documentation for pattern syntax. Examples: .drone.yml
, /docs/**/*.txt
.
settings.add_protected_branch = Enable protection
settings.delete_protected_branch = Disable protection
settings.update_protect_branch_success = Branch protection for branch '%s' has been updated.
@@ -1891,6 +1893,16 @@ settings.choose_branch = Choose a branch…
settings.no_protected_branch = There are no protected branches.
settings.edit_protected_branch = Edit
settings.protected_branch_required_approvals_min = Required approvals cannot be negative.
+settings.tags = Tags
+settings.tags.protection = Tag Protection
+settings.tags.protection.pattern = Tag Pattern
+settings.tags.protection.allowed = Allowed
+settings.tags.protection.allowed.users = Allowed users
+settings.tags.protection.allowed.teams = Allowed teams
+settings.tags.protection.allowed.noone = No One
+settings.tags.protection.create = Protect Tag
+settings.tags.protection.none = There are no protected tags.
+settings.tags.protection.pattern.description = You can use a single name or a glob pattern or regular expression to match multiple tags. Read more in the protected tags guide .
settings.bot_token = Bot Token
settings.chat_id = Chat ID
settings.matrix.homeserver_url = Homeserver URL
@@ -1904,6 +1916,7 @@ settings.archive.success = The repo was successfully archived.
settings.archive.error = An error occurred while trying to archive the repo. See the log for more details.
settings.archive.error_ismirror = You cannot archive a mirrored repo.
settings.archive.branchsettings_unavailable = Branch settings are not available if the repo is archived.
+settings.archive.tagsettings_unavailable = Tag settings are not available if the repo is archived.
settings.unarchive.button = Un-Archive Repo
settings.unarchive.header = Un-Archive This Repo
settings.unarchive.text = Un-Archiving the repo will restore its ability to receive commits and pushes, as well as new issues and pull-requests.
@@ -2018,6 +2031,7 @@ release.deletion_tag_desc = Will delete this tag from repository. Repository con
release.deletion_tag_success = The tag has been deleted.
release.tag_name_already_exist = A release with this tag name already exists.
release.tag_name_invalid = The tag name is not valid.
+release.tag_name_protected = The tag name is protected.
release.tag_already_exist = This tag name already exists.
release.downloads = Downloads
release.download_count = Downloads: %s
diff --git a/routers/private/hook.go b/routers/private/hook.go
index 17ea4f2437..9f5579b6ae 100644
--- a/routers/private/hook.go
+++ b/routers/private/hook.go
@@ -155,125 +155,202 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
private.GitQuarantinePath+"="+opts.GitQuarantinePath)
}
+ protectedTags, err := repo.GetProtectedTags()
+ if err != nil {
+ log.Error("Unable to get protected tags for %-v Error: %v", repo, err)
+ ctx.JSON(http.StatusInternalServerError, private.Response{
+ Err: err.Error(),
+ })
+ return
+ }
+
// Iterate across the provided old commit IDs
for i := range opts.OldCommitIDs {
oldCommitID := opts.OldCommitIDs[i]
newCommitID := opts.NewCommitIDs[i]
refFullName := opts.RefFullNames[i]
- branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
- if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA {
- log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo)
- ctx.JSON(http.StatusForbidden, private.Response{
- Err: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName),
- })
- return
- }
+ if strings.HasPrefix(refFullName, git.BranchPrefix) {
+ branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
+ if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA {
+ log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo)
+ ctx.JSON(http.StatusForbidden, private.Response{
+ Err: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName),
+ })
+ return
+ }
- protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName)
- if err != nil {
- log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
- ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: err.Error(),
- })
- return
- }
-
- // Allow pushes to non-protected branches
- if protectBranch == nil || !protectBranch.IsProtected() {
- continue
- }
-
- // This ref is a protected branch.
- //
- // First of all we need to enforce absolutely:
- //
- // 1. Detect and prevent deletion of the branch
- if newCommitID == git.EmptySHA {
- log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
- ctx.JSON(http.StatusForbidden, private.Response{
- Err: fmt.Sprintf("branch %s is protected from deletion", branchName),
- })
- return
- }
-
- // 2. Disallow force pushes to protected branches
- if git.EmptySHA != oldCommitID {
- output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env)
+ protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName)
if err != nil {
- log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
+ log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Fail to detect force push: %v", err),
+ Err: err.Error(),
})
return
- } else if len(output) > 0 {
- log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
- ctx.JSON(http.StatusForbidden, private.Response{
- Err: fmt.Sprintf("branch %s is protected from force push", branchName),
- })
- return
-
}
- }
- // 3. Enforce require signed commits
- if protectBranch.RequireSignedCommits {
- err := verifyCommits(oldCommitID, newCommitID, gitRepo, env)
- if err != nil {
- if !isErrUnverifiedCommit(err) {
- log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
+ // Allow pushes to non-protected branches
+ if protectBranch == nil || !protectBranch.IsProtected() {
+ continue
+ }
+
+ // This ref is a protected branch.
+ //
+ // First of all we need to enforce absolutely:
+ //
+ // 1. Detect and prevent deletion of the branch
+ if newCommitID == git.EmptySHA {
+ log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
+ ctx.JSON(http.StatusForbidden, private.Response{
+ Err: fmt.Sprintf("branch %s is protected from deletion", branchName),
+ })
+ return
+ }
+
+ // 2. Disallow force pushes to protected branches
+ if git.EmptySHA != oldCommitID {
+ output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env)
+ if err != nil {
+ log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err),
+ Err: fmt.Sprintf("Fail to detect force push: %v", err),
+ })
+ return
+ } else if len(output) > 0 {
+ log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
+ ctx.JSON(http.StatusForbidden, private.Response{
+ Err: fmt.Sprintf("branch %s is protected from force push", branchName),
+ })
+ return
+
+ }
+ }
+
+ // 3. Enforce require signed commits
+ if protectBranch.RequireSignedCommits {
+ err := verifyCommits(oldCommitID, newCommitID, gitRepo, env)
+ if err != nil {
+ if !isErrUnverifiedCommit(err) {
+ log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
+ ctx.JSON(http.StatusInternalServerError, private.Response{
+ Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err),
+ })
+ return
+ }
+ unverifiedCommit := err.(*errUnverifiedCommit).sha
+ log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit)
+ ctx.JSON(http.StatusForbidden, private.Response{
+ Err: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit),
})
return
}
- unverifiedCommit := err.(*errUnverifiedCommit).sha
- log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit)
- ctx.JSON(http.StatusForbidden, private.Response{
- Err: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit),
- })
- return
}
- }
- // Now there are several tests which can be overridden:
- //
- // 4. Check protected file patterns - this is overridable from the UI
- changedProtectedfiles := false
- protectedFilePath := ""
+ // Now there are several tests which can be overridden:
+ //
+ // 4. Check protected file patterns - this is overridable from the UI
+ changedProtectedfiles := false
+ protectedFilePath := ""
- globs := protectBranch.GetProtectedFilePatterns()
- if len(globs) > 0 {
- _, err := pull_service.CheckFileProtection(oldCommitID, newCommitID, globs, 1, env, gitRepo)
- if err != nil {
- if !models.IsErrFilePathProtected(err) {
- log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
+ globs := protectBranch.GetProtectedFilePatterns()
+ if len(globs) > 0 {
+ _, err := pull_service.CheckFileProtection(oldCommitID, newCommitID, globs, 1, env, gitRepo)
+ if err != nil {
+ if !models.IsErrFilePathProtected(err) {
+ log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
+ ctx.JSON(http.StatusInternalServerError, private.Response{
+ Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
+ })
+ return
+ }
+
+ changedProtectedfiles = true
+ protectedFilePath = err.(models.ErrFilePathProtected).Path
+ }
+ }
+
+ // 5. Check if the doer is allowed to push
+ canPush := false
+ if opts.IsDeployKey {
+ canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
+ } else {
+ canPush = !changedProtectedfiles && protectBranch.CanUserPush(opts.UserID)
+ }
+
+ // 6. If we're not allowed to push directly
+ if !canPush {
+ // Is this is a merge from the UI/API?
+ if opts.PullRequestID == 0 {
+ // 6a. If we're not merging from the UI/API then there are two ways we got here:
+ //
+ // We are changing a protected file and we're not allowed to do that
+ if changedProtectedfiles {
+ log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
+ ctx.JSON(http.StatusForbidden, private.Response{
+ Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
+ })
+ return
+ }
+
+ // Or we're simply not able to push to this protected branch
+ log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", opts.UserID, branchName, repo)
+ ctx.JSON(http.StatusForbidden, private.Response{
+ Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
+ })
+ return
+ }
+ // 6b. Merge (from UI or API)
+
+ // Get the PR, user and permissions for the user in the repository
+ pr, err := models.GetPullRequestByID(opts.PullRequestID)
+ if err != nil {
+ log.Error("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
+ Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err),
+ })
+ return
+ }
+ user, err := models.GetUserByID(opts.UserID)
+ if err != nil {
+ log.Error("Unable to get User id %d Error: %v", opts.UserID, err)
+ ctx.JSON(http.StatusInternalServerError, private.Response{
+ Err: fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err),
+ })
+ return
+ }
+ perm, err := models.GetUserRepoPermission(repo, user)
+ if err != nil {
+ log.Error("Unable to get Repo permission of repo %s/%s of User %s", repo.OwnerName, repo.Name, user.Name, err)
+ ctx.JSON(http.StatusInternalServerError, private.Response{
+ Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err),
})
return
}
- changedProtectedfiles = true
- protectedFilePath = err.(models.ErrFilePathProtected).Path
- }
- }
+ // Now check if the user is allowed to merge PRs for this repository
+ allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, user)
+ if err != nil {
+ log.Error("Error calculating if allowed to merge: %v", err)
+ ctx.JSON(http.StatusInternalServerError, private.Response{
+ Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err),
+ })
+ return
+ }
- // 5. Check if the doer is allowed to push
- canPush := false
- if opts.IsDeployKey {
- canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
- } else {
- canPush = !changedProtectedfiles && protectBranch.CanUserPush(opts.UserID)
- }
+ if !allowedMerge {
+ log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", opts.UserID, branchName, repo, pr.Index)
+ ctx.JSON(http.StatusForbidden, private.Response{
+ Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
+ })
+ return
+ }
- // 6. If we're not allowed to push directly
- if !canPush {
- // Is this is a merge from the UI/API?
- if opts.PullRequestID == 0 {
- // 6a. If we're not merging from the UI/API then there are two ways we got here:
- //
- // We are changing a protected file and we're not allowed to do that
+ // If we're an admin for the repository we can ignore status checks, reviews and override protected files
+ if perm.IsAdmin() {
+ continue
+ }
+
+ // Now if we're not an admin - we can't overwrite protected files so fail now
if changedProtectedfiles {
log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
ctx.JSON(http.StatusForbidden, private.Response{
@@ -282,88 +359,44 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
return
}
- // Or we're simply not able to push to this protected branch
- log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", opts.UserID, branchName, repo)
- ctx.JSON(http.StatusForbidden, private.Response{
- Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
- })
- return
- }
- // 6b. Merge (from UI or API)
-
- // Get the PR, user and permissions for the user in the repository
- pr, err := models.GetPullRequestByID(opts.PullRequestID)
- if err != nil {
- log.Error("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err)
- ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err),
- })
- return
- }
- user, err := models.GetUserByID(opts.UserID)
- if err != nil {
- log.Error("Unable to get User id %d Error: %v", opts.UserID, err)
- ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err),
- })
- return
- }
- perm, err := models.GetUserRepoPermission(repo, user)
- if err != nil {
- log.Error("Unable to get Repo permission of repo %s/%s of User %s", repo.OwnerName, repo.Name, user.Name, err)
- ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err),
- })
- return
- }
-
- // Now check if the user is allowed to merge PRs for this repository
- allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, user)
- if err != nil {
- log.Error("Error calculating if allowed to merge: %v", err)
- ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err),
- })
- return
- }
-
- if !allowedMerge {
- log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", opts.UserID, branchName, repo, pr.Index)
- ctx.JSON(http.StatusForbidden, private.Response{
- Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
- })
- return
- }
-
- // If we're an admin for the repository we can ignore status checks, reviews and override protected files
- if perm.IsAdmin() {
- continue
- }
-
- // Now if we're not an admin - we can't overwrite protected files so fail now
- if changedProtectedfiles {
- log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
- ctx.JSON(http.StatusForbidden, private.Response{
- Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
- })
- return
- }
-
- // Check all status checks and reviews are ok
- if err := pull_service.CheckPRReadyToMerge(pr, true); err != nil {
- if models.IsErrNotAllowedToMerge(err) {
- log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", opts.UserID, branchName, repo, pr.Index, err.Error())
- ctx.JSON(http.StatusForbidden, private.Response{
- Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.PullRequestID, err.Error()),
+ // Check all status checks and reviews are ok
+ if err := pull_service.CheckPRReadyToMerge(pr, true); err != nil {
+ if models.IsErrNotAllowedToMerge(err) {
+ log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", opts.UserID, branchName, repo, pr.Index, err.Error())
+ ctx.JSON(http.StatusForbidden, private.Response{
+ Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.PullRequestID, err.Error()),
+ })
+ return
+ }
+ log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", opts.UserID, branchName, repo, pr.Index, err)
+ ctx.JSON(http.StatusInternalServerError, private.Response{
+ Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.PullRequestID, err),
})
return
}
- log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", opts.UserID, branchName, repo, pr.Index, err)
+ }
+ } else if strings.HasPrefix(refFullName, git.TagPrefix) {
+ tagName := strings.TrimPrefix(refFullName, git.TagPrefix)
+
+ isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, tagName, opts.UserID)
+ if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.PullRequestID, err),
+ Err: err.Error(),
})
return
}
+ if !isAllowed {
+ log.Warn("Forbidden: Tag %s in %-v is protected", tagName, repo)
+ ctx.JSON(http.StatusForbidden, private.Response{
+ Err: fmt.Sprintf("Tag %s is protected", tagName),
+ })
+ return
+ }
+ } else {
+ log.Error("Unexpected ref: %s", refFullName)
+ ctx.JSON(http.StatusInternalServerError, private.Response{
+ Err: fmt.Sprintf("Unexpected ref: %s", refFullName),
+ })
}
}
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index 3b700e8016..0665496d44 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -322,6 +322,18 @@ func NewReleasePost(ctx *context.Context) {
return
}
+ if models.IsErrInvalidTagName(err) {
+ ctx.Flash.Error(ctx.Tr("repo.release.tag_name_invalid"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
+ return
+ }
+
+ if models.IsErrProtectedTagName(err) {
+ ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
+ return
+ }
+
ctx.ServerError("releaseservice.CreateNewTag", err)
return
}
@@ -333,7 +345,9 @@ func NewReleasePost(ctx *context.Context) {
rel = &models.Release{
RepoID: ctx.Repo.Repository.ID,
+ Repo: ctx.Repo.Repository,
PublisherID: ctx.User.ID,
+ Publisher: ctx.User,
Title: form.Title,
TagName: form.TagName,
Target: form.Target,
@@ -350,6 +364,8 @@ func NewReleasePost(ctx *context.Context) {
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
case models.IsErrInvalidTagName(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
+ case models.IsErrProtectedTagName(err):
+ ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form)
default:
ctx.ServerError("CreateRelease", err)
}
diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go
index c48b19b63c..5e8c2c5276 100644
--- a/routers/web/repo/setting.go
+++ b/routers/web/repo/setting.go
@@ -40,6 +40,7 @@ const (
tplSettingsOptions base.TplName = "repo/settings/options"
tplCollaboration base.TplName = "repo/settings/collaboration"
tplBranches base.TplName = "repo/settings/branches"
+ tplTags base.TplName = "repo/settings/tags"
tplGithooks base.TplName = "repo/settings/githooks"
tplGithookEdit base.TplName = "repo/settings/githook_edit"
tplDeployKeys base.TplName = "repo/settings/deploy_keys"
diff --git a/routers/web/repo/tag.go b/routers/web/repo/tag.go
new file mode 100644
index 0000000000..7928591371
--- /dev/null
+++ b/routers/web/repo/tag.go
@@ -0,0 +1,182 @@
+// Copyright 2021 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 repo
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/forms"
+)
+
+// Tags render the page to protect tags
+func Tags(ctx *context.Context) {
+ if setTagsContext(ctx) != nil {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplTags)
+}
+
+// NewProtectedTagPost handles creation of a protect tag
+func NewProtectedTagPost(ctx *context.Context) {
+ if setTagsContext(ctx) != nil {
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplTags)
+ return
+ }
+
+ repo := ctx.Repo.Repository
+ form := web.GetForm(ctx).(*forms.ProtectTagForm)
+
+ pt := &models.ProtectedTag{
+ RepoID: repo.ID,
+ NamePattern: strings.TrimSpace(form.NamePattern),
+ }
+
+ if strings.TrimSpace(form.AllowlistUsers) != "" {
+ pt.AllowlistUserIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistUsers, ","))
+ }
+ if strings.TrimSpace(form.AllowlistTeams) != "" {
+ pt.AllowlistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistTeams, ","))
+ }
+
+ if err := models.InsertProtectedTag(pt); err != nil {
+ ctx.ServerError("InsertProtectedTag", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+}
+
+// EditProtectedTag render the page to edit a protect tag
+func EditProtectedTag(ctx *context.Context) {
+ if setTagsContext(ctx) != nil {
+ return
+ }
+
+ ctx.Data["PageIsEditProtectedTag"] = true
+
+ pt := selectProtectedTagByContext(ctx)
+ if pt == nil {
+ return
+ }
+
+ ctx.Data["name_pattern"] = pt.NamePattern
+ ctx.Data["allowlist_users"] = strings.Join(base.Int64sToStrings(pt.AllowlistUserIDs), ",")
+ ctx.Data["allowlist_teams"] = strings.Join(base.Int64sToStrings(pt.AllowlistTeamIDs), ",")
+
+ ctx.HTML(http.StatusOK, tplTags)
+}
+
+// EditProtectedTagPost handles creation of a protect tag
+func EditProtectedTagPost(ctx *context.Context) {
+ if setTagsContext(ctx) != nil {
+ return
+ }
+
+ ctx.Data["PageIsEditProtectedTag"] = true
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplTags)
+ return
+ }
+
+ pt := selectProtectedTagByContext(ctx)
+ if pt == nil {
+ return
+ }
+
+ form := web.GetForm(ctx).(*forms.ProtectTagForm)
+
+ pt.NamePattern = strings.TrimSpace(form.NamePattern)
+ pt.AllowlistUserIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistUsers, ","))
+ pt.AllowlistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistTeams, ","))
+
+ if err := models.UpdateProtectedTag(pt); err != nil {
+ ctx.ServerError("UpdateProtectedTag", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
+}
+
+// DeleteProtectedTagPost handles deletion of a protected tag
+func DeleteProtectedTagPost(ctx *context.Context) {
+ pt := selectProtectedTagByContext(ctx)
+ if pt == nil {
+ return
+ }
+
+ if err := models.DeleteProtectedTag(pt); err != nil {
+ ctx.ServerError("DeleteProtectedTag", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
+}
+
+func setTagsContext(ctx *context.Context) error {
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsTags"] = true
+
+ protectedTags, err := ctx.Repo.Repository.GetProtectedTags()
+ if err != nil {
+ ctx.ServerError("GetProtectedTags", err)
+ return err
+ }
+ ctx.Data["ProtectedTags"] = protectedTags
+
+ users, err := ctx.Repo.Repository.GetReaders()
+ if err != nil {
+ ctx.ServerError("Repo.Repository.GetReaders", err)
+ return err
+ }
+ ctx.Data["Users"] = users
+
+ if ctx.Repo.Owner.IsOrganization() {
+ teams, err := ctx.Repo.Owner.TeamsWithAccessToRepo(ctx.Repo.Repository.ID, models.AccessModeRead)
+ if err != nil {
+ ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
+ return err
+ }
+ ctx.Data["Teams"] = teams
+ }
+
+ return nil
+}
+
+func selectProtectedTagByContext(ctx *context.Context) *models.ProtectedTag {
+ id := ctx.QueryInt64("id")
+ if id == 0 {
+ id = ctx.ParamsInt64(":id")
+ }
+
+ tag, err := models.GetProtectedTagByID(id)
+ if err != nil {
+ ctx.ServerError("GetProtectedTagByID", err)
+ return nil
+ }
+
+ if tag != nil && tag.RepoID == ctx.Repo.Repository.ID {
+ return tag
+ }
+
+ ctx.NotFound("", fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, ctx.Repo.Repository))
+
+ return nil
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index 883213479c..627c88aab1 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -594,12 +594,21 @@ func RegisterRoutes(m *web.Route) {
m.Post("/delete", repo.DeleteTeam)
})
})
+
m.Group("/branches", func() {
m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
m.Combo("/*").Get(repo.SettingsProtectedBranch).
Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost)
}, repo.MustBeNotEmpty)
+ m.Group("/tags", func() {
+ m.Get("", repo.Tags)
+ m.Post("", bindIgnErr(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.NewProtectedTagPost)
+ m.Post("/delete", context.RepoMustNotBeArchived(), repo.DeleteProtectedTagPost)
+ m.Get("/{id}", repo.EditProtectedTag)
+ m.Post("/{id}", bindIgnErr(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.EditProtectedTagPost)
+ })
+
m.Group("/hooks/git", func() {
m.Get("", repo.GitHooks)
m.Combo("/{name}").Get(repo.GitHooksEdit).
diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go
new file mode 100644
index 0000000000..337e7fe1ea
--- /dev/null
+++ b/services/forms/repo_tag_form.go
@@ -0,0 +1,27 @@
+// Copyright 2021 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 forms
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/web/middleware"
+
+ "gitea.com/go-chi/binding"
+)
+
+// ProtectTagForm form for changing protected tag settings
+type ProtectTagForm struct {
+ NamePattern string `binding:"Required;GlobOrRegexPattern"`
+ AllowlistUsers string
+ AllowlistTeams string
+}
+
+// Validate validates the fields
+func (f *ProtectTagForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
+ ctx := context.GetContext(req)
+ return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
+}
diff --git a/services/release/release.go b/services/release/release.go
index 9d201edf6d..6f5aa02c85 100644
--- a/services/release/release.go
+++ b/services/release/release.go
@@ -23,6 +23,25 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool,
// Only actual create when publish.
if !rel.IsDraft {
if !gitRepo.IsTagExist(rel.TagName) {
+ if err := rel.LoadAttributes(); err != nil {
+ log.Error("LoadAttributes: %v", err)
+ return false, err
+ }
+
+ protectedTags, err := rel.Repo.GetProtectedTags()
+ if err != nil {
+ return false, fmt.Errorf("GetProtectedTags: %v", err)
+ }
+ isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, rel.TagName, rel.PublisherID)
+ if err != nil {
+ return false, err
+ }
+ if !isAllowed {
+ return false, models.ErrProtectedTagName{
+ TagName: rel.TagName,
+ }
+ }
+
commit, err := gitRepo.GetCommit(rel.Target)
if err != nil {
return false, fmt.Errorf("GetCommit: %v", err)
@@ -49,11 +68,7 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool,
}
created = true
rel.LowerTagName = strings.ToLower(rel.TagName)
- // Prepare Notify
- if err := rel.LoadAttributes(); err != nil {
- log.Error("LoadAttributes: %v", err)
- return false, err
- }
+
notification.NotifyPushCommits(
rel.Publisher, rel.Repo,
&repository.PushUpdateOptions{
@@ -137,7 +152,9 @@ func CreateNewTag(doer *models.User, repo *models.Repository, commit, tagName, m
rel := &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: doer.ID,
+ Publisher: doer,
TagName: tagName,
Target: commit,
IsDraft: false,
diff --git a/services/release/release_test.go b/services/release/release_test.go
index 085be55cb4..9f665fabab 100644
--- a/services/release/release_test.go
+++ b/services/release/release_test.go
@@ -33,7 +33,9 @@ func TestRelease_Create(t *testing.T) {
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v0.1",
Target: "master",
Title: "v0.1 is released",
@@ -45,7 +47,9 @@ func TestRelease_Create(t *testing.T) {
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v0.1.1",
Target: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
Title: "v0.1.1 is released",
@@ -57,7 +61,9 @@ func TestRelease_Create(t *testing.T) {
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v0.1.2",
Target: "65f1bf2",
Title: "v0.1.2 is released",
@@ -69,7 +75,9 @@ func TestRelease_Create(t *testing.T) {
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v0.1.3",
Target: "65f1bf2",
Title: "v0.1.3 is released",
@@ -81,7 +89,9 @@ func TestRelease_Create(t *testing.T) {
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v0.1.4",
Target: "65f1bf2",
Title: "v0.1.4 is released",
@@ -99,7 +109,9 @@ func TestRelease_Create(t *testing.T) {
var release = models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v0.1.5",
Target: "65f1bf2",
Title: "v0.1.5 is released",
@@ -125,7 +137,9 @@ func TestRelease_Update(t *testing.T) {
// Test a changed release
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v1.1.1",
Target: "master",
Title: "v1.1.1 is released",
@@ -147,7 +161,9 @@ func TestRelease_Update(t *testing.T) {
// Test a changed draft
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v1.2.1",
Target: "65f1bf2",
Title: "v1.2.1 is draft",
@@ -169,7 +185,9 @@ func TestRelease_Update(t *testing.T) {
// Test a changed pre-release
assert.NoError(t, CreateRelease(gitRepo, &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v1.3.1",
Target: "65f1bf2",
Title: "v1.3.1 is pre-released",
@@ -192,7 +210,9 @@ func TestRelease_Update(t *testing.T) {
// Test create release
release = &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v1.1.2",
Target: "master",
Title: "v1.1.2 is released",
@@ -258,7 +278,9 @@ func TestRelease_createTag(t *testing.T) {
// Test a changed release
release := &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v2.1.1",
Target: "master",
Title: "v2.1.1 is released",
@@ -280,7 +302,9 @@ func TestRelease_createTag(t *testing.T) {
// Test a changed draft
release = &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v2.2.1",
Target: "65f1bf2",
Title: "v2.2.1 is draft",
@@ -301,7 +325,9 @@ func TestRelease_createTag(t *testing.T) {
// Test a changed pre-release
release = &models.Release{
RepoID: repo.ID,
+ Repo: repo,
PublisherID: user.ID,
+ Publisher: user,
TagName: "v2.3.1",
Target: "65f1bf2",
Title: "v2.3.1 is pre-released",
diff --git a/templates/repo/settings/nav.tmpl b/templates/repo/settings/nav.tmpl
index 4b89ece349..31672cb5ea 100644
--- a/templates/repo/settings/nav.tmpl
+++ b/templates/repo/settings/nav.tmpl
@@ -5,6 +5,7 @@
{{.i18n.Tr "repo.settings.options"}}
{{.i18n.Tr "repo.settings.collaboration"}}
{{.i18n.Tr "repo.settings.branches"}}
+ {{.i18n.Tr "repo.settings.tags"}}
{{if not DisableWebhooks}}
{{.i18n.Tr "repo.settings.hooks"}}
{{end}}
diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl
index 501c3c4630..d8cdf21871 100644
--- a/templates/repo/settings/navbar.tmpl
+++ b/templates/repo/settings/navbar.tmpl
@@ -11,6 +11,9 @@
{{.i18n.Tr "repo.settings.branches"}}
{{end}}
+
+ {{.i18n.Tr "repo.settings.tags"}}
+
{{if not DisableWebhooks}}
{{.i18n.Tr "repo.settings.hooks"}}
diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl
new file mode 100644
index 0000000000..a2c887b1f8
--- /dev/null
+++ b/templates/repo/settings/tags.tmpl
@@ -0,0 +1,132 @@
+{{template "base/head" .}}
+
+ {{template "repo/header" .}}
+ {{template "repo/settings/navbar" .}}
+
+ {{template "base/alert" .}}
+ {{if .Repository.IsArchived}}
+
+ {{.i18n.Tr "repo.settings.archive.tagsettings_unavailable"}}
+
+ {{else}}
+
+
+
+
+
+
+
+
+
+ {{.i18n.Tr "repo.settings.tags.protection.pattern"}}
+ {{.i18n.Tr "repo.settings.tags.protection.allowed"}}
+
+
+
+ {{range .ProtectedTags}}
+
+ {{.NamePattern}}
+
+ {{if or .AllowlistUserIDs (and $.Owner.IsOrganization .AllowlistTeamIDs)}}
+ {{$userIDs := .AllowlistUserIDs}}
+ {{range $.Users}}
+ {{if contain $userIDs .ID }}
+ {{avatar . 26}} {{.GetDisplayName}}
+ {{end}}
+ {{end}}
+ {{if $.Owner.IsOrganization}}
+ {{$teamIDs := .AllowlistTeamIDs}}
+ {{range $.Teams}}
+ {{if contain $teamIDs .ID }}
+ {{.Name}}
+ {{end}}
+ {{end}}
+ {{end}}
+ {{else}}
+ {{$.i18n.Tr "repo.settings.tags.protection.allowed.noone"}}
+ {{end}}
+
+
+ {{$.i18n.Tr "edit"}}
+
+
+
+ {{else}}
+ {{.i18n.Tr "repo.settings.tags.protection.none"}}
+ {{end}}
+
+
+
+
+
+ {{end}}
+
+
+{{template "base/footer" .}}
From 3ef23d5411732b4b714d6fc9739bc5dac75aadd4 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Fri, 25 Jun 2021 18:54:08 +0200
Subject: [PATCH 19/33] Use gitea logging module for git module (#16243)
remove log() func from gogs times and switch to proper logging
Signed-off-by: Andrew Thornton
Co-authored-by: Andrew Thornton
---
modules/git/batch_reader.go | 6 ++++--
modules/git/blob_nogogit.go | 6 ++++--
modules/git/command.go | 10 +++++-----
modules/git/commit_info_nogogit.go | 4 +++-
modules/git/diff.go | 3 ++-
modules/git/git.go | 16 ----------------
modules/git/git_test.go | 4 ++++
modules/git/hook.go | 6 ++++--
modules/git/last_commit_cache.go | 4 +++-
modules/git/last_commit_cache_gogit.go | 6 ++++--
modules/git/last_commit_cache_nogogit.go | 6 ++++--
modules/git/parse_nogogit.go | 4 +++-
modules/git/repo_base_nogogit.go | 6 ++++--
modules/git/repo_branch_nogogit.go | 6 ++++--
modules/git/repo_commit_nogogit.go | 4 +++-
modules/git/repo_language_stats_nogogit.go | 9 +++++----
modules/git/repo_tag.go | 4 +++-
routers/init.go | 12 +-----------
services/gitdiff/gitdiff_test.go | 2 --
19 files changed, 60 insertions(+), 58 deletions(-)
diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go
index 678b184708..bdf82bde89 100644
--- a/modules/git/batch_reader.go
+++ b/modules/git/batch_reader.go
@@ -12,6 +12,8 @@ import (
"strconv"
"strings"
+ "code.gitea.io/gitea/modules/log"
+
"github.com/djherbis/buffer"
"github.com/djherbis/nio/v3"
)
@@ -99,7 +101,7 @@ func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err er
}
idx := strings.IndexByte(typ, ' ')
if idx < 0 {
- log("missing space typ: %s", typ)
+ log.Debug("missing space typ: %s", typ)
err = ErrNotExist{ID: string(sha)}
return
}
@@ -230,7 +232,7 @@ func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fn
}
idx := bytes.IndexByte(readBytes, ' ')
if idx < 0 {
- log("missing space in readBytes ParseTreeLine: %s", readBytes)
+ log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes)
err = &ErrNotExist{}
return
diff --git a/modules/git/blob_nogogit.go b/modules/git/blob_nogogit.go
index cdaeb636a9..5b42920ebe 100644
--- a/modules/git/blob_nogogit.go
+++ b/modules/git/blob_nogogit.go
@@ -12,6 +12,8 @@ import (
"io"
"io/ioutil"
"math"
+
+ "code.gitea.io/gitea/modules/log"
)
// Blob represents a Git object.
@@ -69,12 +71,12 @@ func (b *Blob) Size() int64 {
defer cancel()
_, err := wr.Write([]byte(b.ID.String() + "\n"))
if err != nil {
- log("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
+ log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
return 0
}
_, _, b.size, err = ReadBatchLine(rd)
if err != nil {
- log("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
+ log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
return 0
}
diff --git a/modules/git/command.go b/modules/git/command.go
index ef78464d5f..2e375fd4f9 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -15,6 +15,7 @@ import (
"strings"
"time"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
)
@@ -114,9 +115,9 @@ func (c *Command) RunInDirTimeoutEnvFullPipelineFunc(env []string, timeout time.
}
if len(dir) == 0 {
- log(c.String())
+ log.Debug("%s", c)
} else {
- log("%s: %v", dir, c)
+ log.Debug("%s: %v", dir, c)
}
ctx, cancel := context.WithTimeout(c.parentContext, timeout)
@@ -197,9 +198,8 @@ func (c *Command) RunInDirTimeoutEnv(env []string, timeout time.Duration, dir st
if err := c.RunInDirTimeoutEnvPipeline(env, timeout, dir, stdout, stderr); err != nil {
return nil, ConcatenateError(err, stderr.String())
}
-
- if stdout.Len() > 0 {
- log("stdout:\n%s", stdout.Bytes()[:1024])
+ if stdout.Len() > 0 && log.IsTrace() {
+ log.Trace("Stdout:\n %s", stdout.Bytes()[:1024])
}
return stdout.Bytes(), nil
}
diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go
index 2283510d96..060ecba261 100644
--- a/modules/git/commit_info_nogogit.go
+++ b/modules/git/commit_info_nogogit.go
@@ -12,6 +12,8 @@ import (
"io"
"path"
"sort"
+
+ "code.gitea.io/gitea/modules/log"
)
// GetCommitsInfo gets information of all commits that are corresponding to these entries
@@ -78,7 +80,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
commitsInfo[i].SubModuleFile = subModuleFile
}
} else {
- log("missing commit for %s", entry.Name())
+ log.Debug("missing commit for %s", entry.Name())
}
}
diff --git a/modules/git/diff.go b/modules/git/diff.go
index 5da53568e5..20f25c1bee 100644
--- a/modules/git/diff.go
+++ b/modules/git/diff.go
@@ -15,6 +15,7 @@ import (
"strconv"
"strings"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
)
@@ -113,7 +114,7 @@ func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHu
righHunk, _ = strconv.Atoi(rightRange[1])
}
} else {
- log("Parse line number failed: %v", diffhunk)
+ log.Debug("Parse line number failed: %v", diffhunk)
rightLine = leftLine
righHunk = leftHunk
}
diff --git a/modules/git/git.go b/modules/git/git.go
index 6b15138a5c..ce1b15c953 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -19,9 +19,6 @@ import (
)
var (
- // Debug enables verbose logging on everything.
- // This should be false in case Gogs starts in SSH mode.
- Debug = false
// Prefix the log prefix
Prefix = "[git-module] "
// GitVersionRequired is the minimum Git version required
@@ -41,19 +38,6 @@ var (
goVersionLessThan115 = true
)
-func log(format string, args ...interface{}) {
- if !Debug {
- return
- }
-
- fmt.Print(Prefix)
- if len(args) == 0 {
- fmt.Println(format)
- } else {
- fmt.Printf(format+"\n", args...)
- }
-}
-
// LocalVersion returns current Git version from shell.
func LocalVersion() (*version.Version, error) {
if err := LoadGitVersion(); err != nil {
diff --git a/modules/git/git_test.go b/modules/git/git_test.go
index 27951d639b..c62a55badc 100644
--- a/modules/git/git_test.go
+++ b/modules/git/git_test.go
@@ -9,6 +9,8 @@ import (
"fmt"
"os"
"testing"
+
+ "code.gitea.io/gitea/modules/log"
)
func fatalTestError(fmtStr string, args ...interface{}) {
@@ -17,6 +19,8 @@ func fatalTestError(fmtStr string, args ...interface{}) {
}
func TestMain(m *testing.M) {
+ _ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`)
+
if err := Init(context.Background()); err != nil {
fatalTestError("Init failed: %v", err)
}
diff --git a/modules/git/hook.go b/modules/git/hook.go
index c23fbf8aa1..7007d23be2 100644
--- a/modules/git/hook.go
+++ b/modules/git/hook.go
@@ -1,4 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2021 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.
@@ -12,6 +13,7 @@ import (
"path/filepath"
"strings"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
@@ -126,11 +128,11 @@ const (
// SetUpdateHook writes given content to update hook of the repository.
func SetUpdateHook(repoPath, content string) (err error) {
- log("Setting update hook: %s", repoPath)
+ log.Debug("Setting update hook: %s", repoPath)
hookPath := path.Join(repoPath, HookPathUpdate)
isExist, err := util.IsExist(hookPath)
if err != nil {
- log("Unable to check if %s exists. Error: %v", hookPath, err)
+ log.Debug("Unable to check if %s exists. Error: %v", hookPath, err)
return err
}
if isExist {
diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go
index 37a59e1fa8..e2d296641f 100644
--- a/modules/git/last_commit_cache.go
+++ b/modules/git/last_commit_cache.go
@@ -7,6 +7,8 @@ package git
import (
"crypto/sha256"
"fmt"
+
+ "code.gitea.io/gitea/modules/log"
)
// Cache represents a caching interface
@@ -24,6 +26,6 @@ func (c *LastCommitCache) getCacheKey(repoPath, ref, entryPath string) string {
// Put put the last commit id with commit and entry path
func (c *LastCommitCache) Put(ref, entryPath, commitID string) error {
- log("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
+ log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
return c.cache.Put(c.getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl())
}
diff --git a/modules/git/last_commit_cache_gogit.go b/modules/git/last_commit_cache_gogit.go
index 16fb1c988c..b8e0db46a9 100644
--- a/modules/git/last_commit_cache_gogit.go
+++ b/modules/git/last_commit_cache_gogit.go
@@ -10,6 +10,8 @@ import (
"context"
"path"
+ "code.gitea.io/gitea/modules/log"
+
"github.com/go-git/go-git/v5/plumbing/object"
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
)
@@ -41,9 +43,9 @@ func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl func() int64,
func (c *LastCommitCache) Get(ref, entryPath string) (interface{}, error) {
v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath))
if vs, ok := v.(string); ok {
- log("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
+ log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
if commit, ok := c.commitCache[vs]; ok {
- log("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
+ log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
return commit, nil
}
id, err := c.repo.ConvertToSHA1(vs)
diff --git a/modules/git/last_commit_cache_nogogit.go b/modules/git/last_commit_cache_nogogit.go
index 84c8ee132c..ff9f9ff1cf 100644
--- a/modules/git/last_commit_cache_nogogit.go
+++ b/modules/git/last_commit_cache_nogogit.go
@@ -10,6 +10,8 @@ import (
"bufio"
"context"
"path"
+
+ "code.gitea.io/gitea/modules/log"
)
// LastCommitCache represents a cache to store last commit
@@ -39,9 +41,9 @@ func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl func() int64,
func (c *LastCommitCache) Get(ref, entryPath string, wr WriteCloserError, rd *bufio.Reader) (interface{}, error) {
v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath))
if vs, ok := v.(string); ok {
- log("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
+ log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
if commit, ok := c.commitCache[vs]; ok {
- log("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
+ log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
return commit, nil
}
id, err := c.repo.ConvertToSHA1(vs)
diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_nogogit.go
index b45b31f239..667111ec4a 100644
--- a/modules/git/parse_nogogit.go
+++ b/modules/git/parse_nogogit.go
@@ -13,6 +13,8 @@ import (
"io"
"strconv"
"strings"
+
+ "code.gitea.io/gitea/modules/log"
)
// ParseTreeEntries parses the output of a `git ls-tree -l` command.
@@ -120,7 +122,7 @@ loop:
case "40000":
entry.entryMode = EntryModeTree
default:
- log("Unknown mode: %v", string(mode))
+ log.Debug("Unknown mode: %v", string(mode))
return nil, fmt.Errorf("unknown mode: %v", string(mode))
}
diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go
index c7d6019d77..1675967d18 100644
--- a/modules/git/repo_base_nogogit.go
+++ b/modules/git/repo_base_nogogit.go
@@ -12,6 +12,8 @@ import (
"context"
"errors"
"path/filepath"
+
+ "code.gitea.io/gitea/modules/log"
)
// Repository represents a Git repository.
@@ -54,7 +56,7 @@ func OpenRepository(repoPath string) (*Repository, error) {
// CatFileBatch obtains a CatFileBatch for this repository
func (repo *Repository) CatFileBatch() (WriteCloserError, *bufio.Reader, func()) {
if repo.batchCancel == nil || repo.batchReader.Buffered() > 0 {
- log("Opening temporary cat file batch for: %s", repo.Path)
+ log.Debug("Opening temporary cat file batch for: %s", repo.Path)
return CatFileBatch(repo.Path)
}
return repo.batchWriter, repo.batchReader, func() {}
@@ -63,7 +65,7 @@ func (repo *Repository) CatFileBatch() (WriteCloserError, *bufio.Reader, func())
// CatFileBatchCheck obtains a CatFileBatchCheck for this repository
func (repo *Repository) CatFileBatchCheck() (WriteCloserError, *bufio.Reader, func()) {
if repo.checkCancel == nil || repo.checkReader.Buffered() > 0 {
- log("Opening temporary cat file batch-check: %s", repo.Path)
+ log.Debug("Opening temporary cat file batch-check: %s", repo.Path)
return CatFileBatchCheck(repo.Path)
}
return repo.checkWriter, repo.checkReader, func() {}
diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go
index dd34e48899..7d10b8ba0f 100644
--- a/modules/git/repo_branch_nogogit.go
+++ b/modules/git/repo_branch_nogogit.go
@@ -12,6 +12,8 @@ import (
"bytes"
"io"
"strings"
+
+ "code.gitea.io/gitea/modules/log"
)
// IsObjectExist returns true if given reference exists in the repository.
@@ -24,7 +26,7 @@ func (repo *Repository) IsObjectExist(name string) bool {
defer cancel()
_, err := wr.Write([]byte(name + "\n"))
if err != nil {
- log("Error writing to CatFileBatchCheck %v", err)
+ log.Debug("Error writing to CatFileBatchCheck %v", err)
return false
}
sha, _, _, err := ReadBatchLine(rd)
@@ -41,7 +43,7 @@ func (repo *Repository) IsReferenceExist(name string) bool {
defer cancel()
_, err := wr.Write([]byte(name + "\n"))
if err != nil {
- log("Error writing to CatFileBatchCheck %v", err)
+ log.Debug("Error writing to CatFileBatchCheck %v", err)
return false
}
_, _, _, err = ReadBatchLine(rd)
diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go
index d00c1bfc67..afd5166f1d 100644
--- a/modules/git/repo_commit_nogogit.go
+++ b/modules/git/repo_commit_nogogit.go
@@ -12,6 +12,8 @@ import (
"io"
"io/ioutil"
"strings"
+
+ "code.gitea.io/gitea/modules/log"
)
// ResolveReference resolves a name to a reference
@@ -110,7 +112,7 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id SHA1) (*Co
return commit, nil
default:
- log("Unknown typ: %s", typ)
+ log.Debug("Unknown typ: %s", typ)
_, err = rd.Discard(int(size) + 1)
if err != nil {
return nil, err
diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go
index 46b084cf01..1684f21d16 100644
--- a/modules/git/repo_language_stats_nogogit.go
+++ b/modules/git/repo_language_stats_nogogit.go
@@ -13,6 +13,7 @@ import (
"math"
"code.gitea.io/gitea/modules/analyze"
+ "code.gitea.io/gitea/modules/log"
"github.com/go-enry/go-enry/v2"
)
@@ -34,19 +35,19 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
}
shaBytes, typ, size, err := ReadBatchLine(batchReader)
if typ != "commit" {
- log("Unable to get commit for: %s. Err: %v", commitID, err)
+ log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
return nil, ErrNotExist{commitID, ""}
}
sha, err := NewIDFromString(string(shaBytes))
if err != nil {
- log("Unable to get commit for: %s. Err: %v", commitID, err)
+ log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
return nil, ErrNotExist{commitID, ""}
}
commit, err := CommitFromReader(repo, sha, io.LimitReader(batchReader, size))
if err != nil {
- log("Unable to get commit for: %s. Err: %v", commitID, err)
+ log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
return nil, err
}
if _, err = batchReader.Discard(1); err != nil {
@@ -79,7 +80,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
}
_, _, size, err := ReadBatchLine(batchReader)
if err != nil {
- log("Error reading blob: %s Err: %v", f.ID.String(), err)
+ log.Debug("Error reading blob: %s Err: %v", f.ID.String(), err)
return nil, err
}
diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go
index 59ab702096..d91c3ca979 100644
--- a/modules/git/repo_tag.go
+++ b/modules/git/repo_tag.go
@@ -8,6 +8,8 @@ package git
import (
"fmt"
"strings"
+
+ "code.gitea.io/gitea/modules/log"
)
// TagPrefix tags prefix path on the repository
@@ -33,7 +35,7 @@ func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error
func (repo *Repository) getTag(tagID SHA1, name string) (*Tag, error) {
t, ok := repo.tagCache.Get(tagID.String())
if ok {
- log("Hit cache: %s", tagID)
+ log.Debug("Hit cache: %s", tagID)
tagClone := *t.(*Tag)
tagClone.Name = name // This is necessary because lightweight tags may have same id
return &tagClone, nil
diff --git a/routers/init.go b/routers/init.go
index bbf39a3f50..e52e547517 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -42,16 +42,6 @@ import (
"code.gitea.io/gitea/services/webhook"
)
-func checkRunMode() {
- switch setting.RunMode {
- case "dev", "test":
- git.Debug = true
- default:
- git.Debug = false
- }
- log.Info("Run Mode: %s", strings.Title(setting.RunMode))
-}
-
// NewServices init new services
func NewServices() {
setting.NewServices()
@@ -84,7 +74,7 @@ func GlobalInit(ctx context.Context) {
log.Trace("AppWorkPath: %s", setting.AppWorkPath)
log.Trace("Custom path: %s", setting.CustomPath)
log.Trace("Log path: %s", setting.LogRootPath)
- checkRunMode()
+ log.Info("Run Mode: %s", strings.Title(setting.RunMode))
// Setup i18n
translation.InitLocales()
diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go
index f8c25a3912..2386552efe 100644
--- a/services/gitdiff/gitdiff_test.go
+++ b/services/gitdiff/gitdiff_test.go
@@ -13,7 +13,6 @@ import (
"testing"
"code.gitea.io/gitea/models"
- "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/setting"
jsoniter "github.com/json-iterator/go"
@@ -514,7 +513,6 @@ func TestDiffLine_GetCommentSide(t *testing.T) {
}
func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
- git.Debug = true
for _, behavior := range []string{"-w", "--ignore-space-at-eol", "-b", ""} {
diffs, err := GetDiffRangeWithWhitespaceBehavior("./testdata/academic-module", "559c156f8e0178b71cb44355428f24001b08fc68", "bd7063cc7c04689c4d082183d32a604ed27a24f9",
setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles, behavior)
From f573e93ed429d1d1dd0a7be8a3b0042f0817cc00 Mon Sep 17 00:00:00 2001
From: siddweiker
Date: Fri, 25 Jun 2021 12:59:25 -0400
Subject: [PATCH 20/33] Fix heatmap activity (#15252)
* Group heatmap actions by 15 minute intervals
Signed-off-by: Sidd Weiker
* Add multi-contribution test for user heatmap
Signed-off-by: Sidd Weiker
* Add timezone aware summation for activity heatmap
Signed-off-by: Sidd Weiker
* Fix api user heatmap test
Signed-off-by: Sidd Weiker
* Update variable declaration style
Co-authored-by: Lunny Xiao
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: techknowlogick
---
integrations/api_user_heatmap_test.go | 2 +-
models/fixtures/action.yml | 24 +++++++++++++++++++++++
models/user_heatmap.go | 11 ++++-------
models/user_heatmap_test.go | 28 +++++++++++++++++++--------
web_src/js/features/heatmap.js | 11 +++++++++--
5 files changed, 58 insertions(+), 18 deletions(-)
diff --git a/integrations/api_user_heatmap_test.go b/integrations/api_user_heatmap_test.go
index 105d39e9ae..a0f0552a17 100644
--- a/integrations/api_user_heatmap_test.go
+++ b/integrations/api_user_heatmap_test.go
@@ -26,7 +26,7 @@ func TestUserHeatmap(t *testing.T) {
var heatmap []*models.UserHeatmapData
DecodeJSON(t, resp, &heatmap)
var dummyheatmap []*models.UserHeatmapData
- dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1603152000, Contributions: 1})
+ dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1603227600, Contributions: 1})
assert.Equal(t, dummyheatmap, heatmap)
}
diff --git a/models/fixtures/action.yml b/models/fixtures/action.yml
index 14cfd90423..e3f3d2a971 100644
--- a/models/fixtures/action.yml
+++ b/models/fixtures/action.yml
@@ -32,3 +32,27 @@
repo_id: 22
is_private: true
created_unix: 1603267920
+
+- id: 5
+ user_id: 10
+ op_type: 1 # create repo
+ act_user_id: 10
+ repo_id: 6
+ is_private: true
+ created_unix: 1603010100
+
+- id: 6
+ user_id: 10
+ op_type: 1 # create repo
+ act_user_id: 10
+ repo_id: 7
+ is_private: true
+ created_unix: 1603011300
+
+- id: 7
+ user_id: 10
+ op_type: 1 # create repo
+ act_user_id: 10
+ repo_id: 8
+ is_private: false
+ created_unix: 1603011540 # grouped with id:7
diff --git a/models/user_heatmap.go b/models/user_heatmap.go
index 0e2767212e..306bd1819b 100644
--- a/models/user_heatmap.go
+++ b/models/user_heatmap.go
@@ -32,17 +32,14 @@ func getUserHeatmapData(user *User, team *Team, doer *User) ([]*UserHeatmapData,
return hdata, nil
}
- var groupBy string
+ // Group by 15 minute intervals which will allow the client to accurately shift the timestamp to their timezone.
+ // The interval is based on the fact that there are timezones such as UTC +5:30 and UTC +12:45.
+ groupBy := "created_unix / 900 * 900"
groupByName := "timestamp" // We need this extra case because mssql doesn't allow grouping by alias
switch {
- case setting.Database.UseSQLite3:
- groupBy = "strftime('%s', strftime('%Y-%m-%d', created_unix, 'unixepoch'))"
case setting.Database.UseMySQL:
- groupBy = "UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(created_unix)))"
- case setting.Database.UsePostgreSQL:
- groupBy = "extract(epoch from date_trunc('day', to_timestamp(created_unix)))"
+ groupBy = "created_unix DIV 900 * 900"
case setting.Database.UseMSSQL:
- groupBy = "datediff(SECOND, '19700101', dateadd(DAY, 0, datediff(day, 0, dateadd(s, created_unix, '19700101'))))"
groupByName = groupBy
}
diff --git a/models/user_heatmap_test.go b/models/user_heatmap_test.go
index 31e78a19cc..b2aaea6499 100644
--- a/models/user_heatmap_test.go
+++ b/models/user_heatmap_test.go
@@ -19,12 +19,20 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
CountResult int
JSONResult string
}{
- {2, 2, 1, `[{"timestamp":1603152000,"contributions":1}]`}, // self looks at action in private repo
- {2, 1, 1, `[{"timestamp":1603152000,"contributions":1}]`}, // admin looks at action in private repo
- {2, 3, 0, `[]`}, // other user looks at action in private repo
- {2, 0, 0, `[]`}, // nobody looks at action in private repo
- {16, 15, 1, `[{"timestamp":1603238400,"contributions":1}]`}, // collaborator looks at action in private repo
- {3, 3, 0, `[]`}, // no action action not performed by target user
+ // self looks at action in private repo
+ {2, 2, 1, `[{"timestamp":1603227600,"contributions":1}]`},
+ // admin looks at action in private repo
+ {2, 1, 1, `[{"timestamp":1603227600,"contributions":1}]`},
+ // other user looks at action in private repo
+ {2, 3, 0, `[]`},
+ // nobody looks at action in private repo
+ {2, 0, 0, `[]`},
+ // collaborator looks at action in private repo
+ {16, 15, 1, `[{"timestamp":1603267200,"contributions":1}]`},
+ // no action action not performed by target user
+ {3, 3, 0, `[]`},
+ // multiple actions performed with two grouped together
+ {10, 10, 3, `[{"timestamp":1603009800,"contributions":1},{"timestamp":1603010700,"contributions":2}]`},
}
// Prepare
assert.NoError(t, PrepareTestDatabase())
@@ -51,9 +59,13 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
// Get the heatmap and compare
heatmap, err := GetUserHeatmapDataByUser(user, doer)
+ var contributions int
+ for _, hm := range heatmap {
+ contributions += int(hm.Contributions)
+ }
assert.NoError(t, err)
- assert.Len(t, heatmap, len(actions), "invalid action count: did the test data became too old?")
- assert.Len(t, heatmap, tc.CountResult, fmt.Sprintf("testcase %d", i))
+ assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?")
+ assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase %d", i))
// Test JSON rendering
json := jsoniter.ConfigCompatibleWithStandardLibrary
diff --git a/web_src/js/features/heatmap.js b/web_src/js/features/heatmap.js
index d1cb43dde0..07ecaee461 100644
--- a/web_src/js/features/heatmap.js
+++ b/web_src/js/features/heatmap.js
@@ -7,8 +7,15 @@ export default async function initHeatmap() {
if (!el) return;
try {
- const values = JSON.parse(el.dataset.heatmapData).map(({contributions, timestamp}) => {
- return {date: new Date(timestamp * 1000), count: contributions};
+ const heatmap = {};
+ JSON.parse(el.dataset.heatmapData).forEach(({contributions, timestamp}) => {
+ // Convert to user timezone and sum contributions by date
+ const dateStr = new Date(timestamp * 1000).toDateString();
+ heatmap[dateStr] = (heatmap[dateStr] || 0) + contributions;
+ });
+
+ const values = Object.keys(heatmap).map((v) => {
+ return {date: new Date(v), count: heatmap[v]};
});
const View = Vue.extend({
From 31acd3c0c2de788fa995d5660b617f51d5b94078 Mon Sep 17 00:00:00 2001
From: Steven <61625851+justusbunsi@users.noreply.github.com>
Date: Fri, 25 Jun 2021 19:00:09 +0200
Subject: [PATCH 21/33] Prevent webhook action buttons from shifting (#16087)
On long webhook urls the action buttons (edit, delete) have been shifted
by the url text.
Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: techknowlogick
---
templates/repo/settings/webhook/base_list.tmpl | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
index 916272a97a..e77e747742 100644
--- a/templates/repo/settings/webhook/base_list.tmpl
+++ b/templates/repo/settings/webhook/base_list.tmpl
@@ -41,7 +41,7 @@
{{.Description | Str2html}}
{{range .Webhooks}}
-
+
{{if eq .LastStatus 1}}
{{svg "octicon-check"}}
{{else if eq .LastStatus 2}}
@@ -49,8 +49,8 @@
{{else}}
{{svg "octicon-dot-fill"}}
{{end}}
-
{{.URL}}
-
+
{{.URL}}
+
From 06f483d0c4d11f32faae60a2a6c77b164f7e88e6 Mon Sep 17 00:00:00 2001
From: Jimmy Praet
Date: Fri, 25 Jun 2021 19:01:43 +0200
Subject: [PATCH 22/33] Append to existing trailers in generated squash commit
message (#15980)
* Remove superfluous newline before Co-authored-by trailers
* Append to existing PR description trailer section
If the existing PR description message already contains a trailer section (e.g. Signed-off-by: ),
append to it instead of creating a new trailer section.
* Reuse compiled regexp
* Simplify regex and deal with trailing \n in PR description
* Add tests for CommitMessageTrailersPattern
- add support for Key:Value (no space after colon)
- add support for whitespace "folding"
* Update services/pull/pull_test.go
Co-authored-by: Norwin
Co-authored-by: zeripath
Co-authored-by: Norwin
Co-authored-by: Lunny Xiao
Co-authored-by: techknowlogick
---
services/pull/pull.go | 17 ++++++++---------
services/pull/pull_test.go | 23 +++++++++++++++++++++++
2 files changed, 31 insertions(+), 9 deletions(-)
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 7b5dd6a964..db216ddbf4 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -9,6 +9,7 @@ import (
"bytes"
"context"
"fmt"
+ "regexp"
"strings"
"time"
@@ -528,6 +529,8 @@ func CloseRepoBranchesPulls(doer *models.User, repo *models.Repository) error {
return nil
}
+var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`)
+
// GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one)
func GetSquashMergeCommitMessages(pr *models.PullRequest) string {
if err := pr.LoadIssue(); err != nil {
@@ -583,10 +586,13 @@ func GetSquashMergeCommitMessages(pr *models.PullRequest) string {
stringBuilder := strings.Builder{}
if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
- stringBuilder.WriteString(pr.Issue.Content)
+ message := strings.TrimSpace(pr.Issue.Content)
+ stringBuilder.WriteString(message)
if stringBuilder.Len() > 0 {
stringBuilder.WriteRune('\n')
- stringBuilder.WriteRune('\n')
+ if !commitMessageTrailersPattern.MatchString(message) {
+ stringBuilder.WriteRune('\n')
+ }
}
}
@@ -657,13 +663,6 @@ func GetSquashMergeCommitMessages(pr *models.PullRequest) string {
}
}
- if len(authors) > 0 {
- if _, err := stringBuilder.WriteRune('\n'); err != nil {
- log.Error("Unable to write to string builder Error: %v", err)
- return ""
- }
- }
-
for _, author := range authors {
if _, err := stringBuilder.Write([]byte("Co-authored-by: ")); err != nil {
log.Error("Unable to write to string builder Error: %v", err)
diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go
index 64920e3550..81627ebb77 100644
--- a/services/pull/pull_test.go
+++ b/services/pull/pull_test.go
@@ -5,4 +5,27 @@
package pull
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
// TODO TestPullRequest_PushToBaseRepo
+
+func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) {
+ // Not a valid trailer section
+ assert.False(t, commitMessageTrailersPattern.MatchString(""))
+ assert.False(t, commitMessageTrailersPattern.MatchString("No trailer."))
+ assert.False(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob \nNot a trailer due to following text."))
+ assert.False(t, commitMessageTrailersPattern.MatchString("Message body not correctly separated from trailer section by empty line.\nSigned-off-by: Bob "))
+ // Valid trailer section
+ assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob "))
+ assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob \nOther-Trailer: Value"))
+ assert.True(t, commitMessageTrailersPattern.MatchString("Message body correctly separated from trailer section by empty line.\n\nSigned-off-by: Bob "))
+ assert.True(t, commitMessageTrailersPattern.MatchString("Multiple trailers.\n\nSigned-off-by: Bob \nOther-Trailer: Value"))
+ assert.True(t, commitMessageTrailersPattern.MatchString("Newline after trailer section.\n\nSigned-off-by: Bob \n"))
+ assert.True(t, commitMessageTrailersPattern.MatchString("No space after colon is accepted.\n\nSigned-off-by:Bob "))
+ assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob "))
+ assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value"))
+}
From 1a1ce9b7216ab80e94987270da8fc2def57237c0 Mon Sep 17 00:00:00 2001
From: zeripath
Date: Fri, 25 Jun 2021 19:14:49 +0100
Subject: [PATCH 23/33] Fuzzer finds an NPE due to incorrect URLPrefix (#16249)
The Fuzzer is running on a non-repo urlprefix which is incorrect for RenderRaw
Signed-off-by: Andrew Thornton
Co-authored-by: 6543 <6543@obermui.de>
---
tools/fuzz.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/fuzz.go b/tools/fuzz.go
index 4b5b72d1d0..b48ae0add9 100644
--- a/tools/fuzz.go
+++ b/tools/fuzz.go
@@ -23,7 +23,7 @@ import (
var (
renderContext = markup.RenderContext{
- URLPrefix: "https://example.com",
+ URLPrefix: "https://example.com/go-gitea/gitea",
Metas: map[string]string{
"user": "go-gitea",
"repo": "gitea",
From 9b33d18899b7e825e4754969ffcc9d7b541d2d28 Mon Sep 17 00:00:00 2001
From: ayb
Date: Sat, 26 Jun 2021 00:38:27 +0200
Subject: [PATCH 24/33] Added support for gopher URLs. (#14749)
* Added support for gopher URLs.
* Add setting and make this user settable instead
Signed-off-by: Andrew Thornton
Co-authored-by: Andrew Thornton
---
custom/conf/app.example.ini | 2 ++
.../doc/advanced/config-cheat-sheet.en-us.md | 1 +
modules/setting/service.go | 12 ++++++++++++
modules/validation/binding.go | 19 +++++++++++++++++++
modules/validation/helpers.go | 19 +++++++++++++++++++
services/forms/user_form.go | 2 +-
6 files changed, 54 insertions(+), 1 deletion(-)
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 5adfb0546f..fa6a9e3fac 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -705,6 +705,8 @@ PATH =
;;
;; Minimum amount of time a user must exist before comments are kept when the user is deleted.
;USER_DELETE_WITH_COMMENTS_MAX_TIME = 0
+;; Valid site url schemes for user profiles
+;VALID_SITE_URL_SCHEMES=http,https
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 5e976174fb..aa9eb7e0ca 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -519,6 +519,7 @@ relation to port exhaustion.
- `NO_REPLY_ADDRESS`: **noreply.DOMAIN** Value for the domain part of the user's email address in the git log if user has set KeepEmailPrivate to true. DOMAIN resolves to the value in server.DOMAIN.
The user's email will be replaced with a concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS.
- `USER_DELETE_WITH_COMMENTS_MAX_TIME`: **0** Minimum amount of time a user must exist before comments are kept when the user is deleted.
+- `VALID_SITE_URL_SCHEMES`: **http, https**: Valid site url schemes for user profiles
### Service - Expore (`service.explore`)
diff --git a/modules/setting/service.go b/modules/setting/service.go
index 41e834e8e6..bd70c7e6eb 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -6,6 +6,7 @@ package setting
import (
"regexp"
+ "strings"
"time"
"code.gitea.io/gitea/modules/log"
@@ -55,6 +56,7 @@ var Service struct {
AutoWatchOnChanges bool
DefaultOrgMemberVisible bool
UserDeleteWithCommentsMaxTime time.Duration
+ ValidSiteURLSchemes []string
// OpenID settings
EnableOpenIDSignIn bool
@@ -120,6 +122,16 @@ func newService() {
Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility]
Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool()
Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0)
+ sec.Key("VALID_SITE_URL_SCHEMES").MustString("http,https")
+ Service.ValidSiteURLSchemes = sec.Key("VALID_SITE_URL_SCHEMES").Strings(",")
+ schemes := make([]string, len(Service.ValidSiteURLSchemes))
+ for _, scheme := range Service.ValidSiteURLSchemes {
+ scheme = strings.ToLower(strings.TrimSpace(scheme))
+ if scheme != "" {
+ schemes = append(schemes, scheme)
+ }
+ }
+ Service.ValidSiteURLSchemes = schemes
if err := Cfg.Section("service.explore").MapTo(&Service.Explore); err != nil {
log.Fatal("Failed to map service.explore settings: %v", err)
diff --git a/modules/validation/binding.go b/modules/validation/binding.go
index 4cef48daf3..5d5c64611f 100644
--- a/modules/validation/binding.go
+++ b/modules/validation/binding.go
@@ -55,6 +55,7 @@ func CheckGitRefAdditionalRulesValid(name string) bool {
func AddBindingRules() {
addGitRefNameBindingRule()
addValidURLBindingRule()
+ addValidSiteURLBindingRule()
addGlobPatternRule()
addRegexPatternRule()
addGlobOrRegexPatternRule()
@@ -102,6 +103,24 @@ func addValidURLBindingRule() {
})
}
+func addValidSiteURLBindingRule() {
+ // URL validation rule
+ binding.AddRule(&binding.Rule{
+ IsMatch: func(rule string) bool {
+ return strings.HasPrefix(rule, "ValidSiteUrl")
+ },
+ IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
+ str := fmt.Sprintf("%v", val)
+ if len(str) != 0 && !IsValidSiteURL(str) {
+ errs.Add([]string{name}, binding.ERR_URL, "Url")
+ return false, errs
+ }
+
+ return true, errs
+ },
+ })
+}
+
func addGlobPatternRule() {
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go
index c22e667a2e..343261aac5 100644
--- a/modules/validation/helpers.go
+++ b/modules/validation/helpers.go
@@ -52,6 +52,25 @@ func IsValidURL(uri string) bool {
return true
}
+// IsValidSiteURL checks if URL is valid
+func IsValidSiteURL(uri string) bool {
+ u, err := url.ParseRequestURI(uri)
+ if err != nil {
+ return false
+ }
+
+ if !validPort(portOnly(u.Host)) {
+ return false
+ }
+
+ for _, scheme := range setting.Service.ValidSiteURLSchemes {
+ if scheme == u.Scheme {
+ return true
+ }
+ }
+ return false
+}
+
// IsAPIURL checks if URL is current Gitea instance API URL
func IsAPIURL(uri string) bool {
return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(setting.AppURL+"api"))
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index 2c065dc511..903a625da0 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -226,7 +226,7 @@ type UpdateProfileForm struct {
Name string `binding:"AlphaDashDot;MaxSize(40)"`
FullName string `binding:"MaxSize(100)"`
KeepEmailPrivate bool
- Website string `binding:"ValidUrl;MaxSize(255)"`
+ Website string `binding:"ValidSiteUrl;MaxSize(255)"`
Location string `binding:"MaxSize(50)"`
Language string
Description string `binding:"MaxSize(255)"`
From 62a4879e84c7474dd72ab0eb4c54923f2690510c Mon Sep 17 00:00:00 2001
From: zeripath
Date: Sat, 26 Jun 2021 00:11:33 +0100
Subject: [PATCH 25/33] Improve efficiency in FindRenderizableReferenceNumeric
and getReferences (#16251)
* Fuzzer finds an NPE due to incorrect URLPrefix
The Fuzzer is running on a non-repo urlprefix which is incorrect for RenderRaw
* Make FindRenderizableReferenceNumeric and getReferences more efficient
Signed-off-by: Andrew Thornton
Co-authored-by: techknowlogick
---
modules/references/references.go | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/modules/references/references.go b/modules/references/references.go
index 106e66b47b..ef859abcc7 100644
--- a/modules/references/references.go
+++ b/modules/references/references.go
@@ -5,6 +5,7 @@
package references
import (
+ "bytes"
"net/url"
"regexp"
"strconv"
@@ -14,6 +15,8 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/mdstripper"
"code.gitea.io/gitea/modules/setting"
+
+ "github.com/yuin/goldmark/util"
)
var (
@@ -321,7 +324,7 @@ func FindRenderizableReferenceNumeric(content string, prOnly bool) (bool, *Rende
return false, nil
}
}
- r := getCrossReference([]byte(content), match[2], match[3], false, prOnly)
+ r := getCrossReference(util.StringToReadOnlyBytes(content), match[2], match[3], false, prOnly)
if r == nil {
return false, nil
}
@@ -465,18 +468,17 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference
}
func getCrossReference(content []byte, start, end int, fromLink bool, prOnly bool) *rawReference {
- refid := string(content[start:end])
- sep := strings.IndexAny(refid, "#!")
+ sep := bytes.IndexAny(content[start:end], "#!")
if sep < 0 {
return nil
}
- isPull := refid[sep] == '!'
+ isPull := content[start+sep] == '!'
if prOnly && !isPull {
return nil
}
- repo := refid[:sep]
- issue := refid[sep+1:]
- index, err := strconv.ParseInt(issue, 10, 64)
+ repo := string(content[start : start+sep])
+ issue := string(content[start+sep+1 : end])
+ index, err := strconv.ParseInt(string(issue), 10, 64)
if err != nil {
return nil
}
From 622f1e764c6230023cc1944ad727cd2ad1544b68 Mon Sep 17 00:00:00 2001
From: John Olheiser
Date: Fri, 25 Jun 2021 23:16:36 -0500
Subject: [PATCH 26/33] Add better errors for disabled account recovery
(#15117)
Signed-off-by: jolheiser
Co-authored-by: Andrew Thornton
Co-authored-by: 6543 <6543@obermui.de>
---
options/locale/locale_en-US.ini | 4 ++--
routers/web/user/auth.go | 1 +
templates/user/auth/forgot_passwd.tmpl | 8 +++++++-
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index a809f49eeb..4a79ffa7eb 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -303,7 +303,8 @@ openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new
openid_register_title = Create new account
openid_register_desc = The chosen OpenID URI is unknown. Associate it with a new account here.
openid_signin_desc = Enter your OpenID URI. For example: https://anne.me, bob.openid.org.cn or gnusocial.net/carry.
-disable_forgot_password_mail = Account recovery is disabled. Please contact your site administrator.
+disable_forgot_password_mail = Account recovery is disabled because no email is set up. Please contact your site administrator.
+disable_forgot_password_mail_admin = Account recovery is only available when email is set up. Please set up email to enable account recovery.
email_domain_blacklisted = You cannot register with your email address.
authorize_application = Authorize Application
authorize_redirect_notice = You will be redirected to %s if you authorize this application.
@@ -312,7 +313,6 @@ authorize_application_description = If you grant the access, it will be able to
authorize_title = Authorize "%s" to access your account?
authorization_failed = Authorization failed
authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you've tried to authorize.
-disable_forgot_password_mail = Account recovery is disabled. Please contact your site administrator.
sspi_auth_failed = SSPI authentication failed
password_pwned = The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.
password_pwned_err = Could not complete request to HaveIBeenPwned
diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go
index 827b7cdef0..6b4beff0e0 100644
--- a/routers/web/user/auth.go
+++ b/routers/web/user/auth.go
@@ -1478,6 +1478,7 @@ func ForgotPasswd(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
if setting.MailService == nil {
+ log.Warn(ctx.Tr("auth.disable_forgot_password_mail_admin"))
ctx.Data["IsResetDisable"] = true
ctx.HTML(http.StatusOK, tplForgotPassword)
return
diff --git a/templates/user/auth/forgot_passwd.tmpl b/templates/user/auth/forgot_passwd.tmpl
index 241deeed4a..2ff7acb97d 100644
--- a/templates/user/auth/forgot_passwd.tmpl
+++ b/templates/user/auth/forgot_passwd.tmpl
@@ -22,7 +22,13 @@
{{.i18n.Tr "auth.send_reset_mail"}}
{{else if .IsResetDisable}}
-
{{.i18n.Tr "auth.disable_forgot_password_mail"}}
+
+ {{if $.IsAdmin}}
+ {{.i18n.Tr "auth.disable_forgot_password_mail_admin"}}
+ {{else}}
+ {{.i18n.Tr "auth.disable_forgot_password_mail"}}
+ {{end}}
+
{{else if .ResendLimited}}
{{.i18n.Tr "auth.resent_limit_prompt"}}
{{end}}
From e673e42f7efafb184ffbe84f6998087713d8e373 Mon Sep 17 00:00:00 2001
From: KN4CK3R
Date: Sat, 26 Jun 2021 11:13:51 +0200
Subject: [PATCH 27/33] Fixed issues not updated by commits (#16254)
`UpdateIssuesCommit` may get called with fewer commits because of `FeedMaxCommitNum` and therefore may miss some commands.
---
services/repository/push.go | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/services/repository/push.go b/services/repository/push.go
index f031073b2e..dcb3bc779f 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -193,16 +193,17 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
}
commits = repo_module.ListToPushCommits(l)
+
+ if err := repofiles.UpdateIssuesCommit(pusher, repo, commits.Commits, refName); err != nil {
+ log.Error("updateIssuesCommit: %v", err)
+ }
+
if len(commits.Commits) > setting.UI.FeedMaxCommitNum {
commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
}
commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
notification.NotifyPushCommits(pusher, repo, opts, commits)
- if err := repofiles.UpdateIssuesCommit(pusher, repo, commits.Commits, refName); err != nil {
- log.Error("updateIssuesCommit: %v", err)
- }
-
if err = models.RemoveDeletedBranch(repo.ID, branch); err != nil {
log.Error("models.RemoveDeletedBranch %s/%s failed: %v", repo.ID, branch, err)
}
From e3c626834b34fae7728ee7869ed73ee4d1b26a26 Mon Sep 17 00:00:00 2001
From: Lunny Xiao
Date: Sat, 26 Jun 2021 19:28:55 +0800
Subject: [PATCH 28/33] Let package git depend on setting but not opposite
(#15241)
* Let package git depend on setting but not opposite
* private some package variables
---
contrib/pr/checkout.go | 3 +-
integrations/git_test.go | 6 +--
integrations/integration_test.go | 3 +-
integrations/lfs_getobject_test.go | 13 ++++---
integrations/migration-test/migration_test.go | 3 +-
models/migrations/migrations_test.go | 3 +-
modules/git/command.go | 6 +--
modules/git/git.go | 33 ++++++++++++++++
modules/git/lfs.go | 37 ++++++++++++++++++
modules/git/repo_commit.go | 14 +++----
modules/setting/git.go | 38 ++-----------------
modules/setting/lfs.go | 22 -----------
routers/api/v1/repo/commits.go | 4 +-
routers/init.go | 4 +-
routers/web/repo/branch.go | 7 ++--
routers/web/repo/commit.go | 6 +--
routers/web/repo/wiki.go | 3 +-
17 files changed, 113 insertions(+), 92 deletions(-)
create mode 100644 modules/git/lfs.go
diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go
index 9ce84f762c..902c9ea623 100644
--- a/contrib/pr/checkout.go
+++ b/contrib/pr/checkout.go
@@ -26,6 +26,7 @@ import (
"time"
"code.gitea.io/gitea/models"
+ gitea_git "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/external"
"code.gitea.io/gitea/modules/setting"
@@ -79,7 +80,7 @@ func runPR() {
setting.RunUser = curUser.Username
log.Printf("[PR] Loading fixtures data ...\n")
- setting.CheckLFSVersion()
+ gitea_git.CheckLFSVersion()
//models.LoadConfigs()
/*
setting.Database.Type = "sqlite3"
diff --git a/integrations/git_test.go b/integrations/git_test.go
index 13a60076a7..a9848eaa4c 100644
--- a/integrations/git_test.go
+++ b/integrations/git_test.go
@@ -143,7 +143,7 @@ func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string
func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) {
t.Run("LFS", func(t *testing.T) {
defer PrintCurrentTest(t)()
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
if !setting.LFS.StartServer {
t.Skip()
return
@@ -213,7 +213,7 @@ func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS s
resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, littleSize, resp.Length)
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS))
resp := session.MakeRequest(t, req, http.StatusOK)
@@ -255,7 +255,7 @@ func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS
resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, littleSize, resp.Length)
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
diff --git a/integrations/integration_test.go b/integrations/integration_test.go
index d755977d1a..8a008ac621 100644
--- a/integrations/integration_test.go
+++ b/integrations/integration_test.go
@@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/queue"
@@ -162,7 +163,7 @@ func initIntegrationTest() {
setting.SetCustomPathAndConf("", "", "")
setting.NewContext()
util.RemoveAll(models.LocalCopyPath())
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
setting.InitDBConfig()
if err := storage.Init(); err != nil {
fmt.Printf("Init storage failed: %v", err)
diff --git a/integrations/lfs_getobject_test.go b/integrations/lfs_getobject_test.go
index 337a93567a..c99500f469 100644
--- a/integrations/lfs_getobject_test.go
+++ b/integrations/lfs_getobject_test.go
@@ -13,6 +13,7 @@ import (
"testing"
"code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/web"
@@ -81,7 +82,7 @@ func checkResponseTestContentEncoding(t *testing.T, content *[]byte, resp *httpt
func TestGetLFSSmall(t *testing.T) {
defer prepareTestEnv(t)()
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
if !setting.LFS.StartServer {
t.Skip()
return
@@ -94,7 +95,7 @@ func TestGetLFSSmall(t *testing.T) {
func TestGetLFSLarge(t *testing.T) {
defer prepareTestEnv(t)()
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
if !setting.LFS.StartServer {
t.Skip()
return
@@ -110,7 +111,7 @@ func TestGetLFSLarge(t *testing.T) {
func TestGetLFSGzip(t *testing.T) {
defer prepareTestEnv(t)()
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
if !setting.LFS.StartServer {
t.Skip()
return
@@ -131,7 +132,7 @@ func TestGetLFSGzip(t *testing.T) {
func TestGetLFSZip(t *testing.T) {
defer prepareTestEnv(t)()
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
if !setting.LFS.StartServer {
t.Skip()
return
@@ -154,7 +155,7 @@ func TestGetLFSZip(t *testing.T) {
func TestGetLFSRangeNo(t *testing.T) {
defer prepareTestEnv(t)()
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
if !setting.LFS.StartServer {
t.Skip()
return
@@ -167,7 +168,7 @@ func TestGetLFSRangeNo(t *testing.T) {
func TestGetLFSRange(t *testing.T) {
defer prepareTestEnv(t)()
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
if !setting.LFS.StartServer {
t.Skip()
return
diff --git a/integrations/migration-test/migration_test.go b/integrations/migration-test/migration_test.go
index 852c0b737c..209ff5a058 100644
--- a/integrations/migration-test/migration_test.go
+++ b/integrations/migration-test/migration_test.go
@@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/models/migrations"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -61,7 +62,7 @@ func initMigrationTest(t *testing.T) func() {
assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
assert.NoError(t, util.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath))
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
setting.InitDBConfig()
setting.NewLogServices(true)
return deferFn
diff --git a/models/migrations/migrations_test.go b/models/migrations/migrations_test.go
index 641d972b8b..26066580d8 100644
--- a/models/migrations/migrations_test.go
+++ b/models/migrations/migrations_test.go
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -55,7 +56,7 @@ func TestMain(m *testing.M) {
setting.SetCustomPathAndConf("", "", "")
setting.NewContext()
- setting.CheckLFSVersion()
+ git.CheckLFSVersion()
setting.InitDBConfig()
setting.NewLogServices(true)
diff --git a/modules/git/command.go b/modules/git/command.go
index 2e375fd4f9..127e95ecfb 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -23,8 +23,8 @@ var (
// GlobalCommandArgs global command args for external package setting
GlobalCommandArgs []string
- // DefaultCommandExecutionTimeout default command execution timeout duration
- DefaultCommandExecutionTimeout = 360 * time.Second
+ // defaultCommandExecutionTimeout default command execution timeout duration
+ defaultCommandExecutionTimeout = 360 * time.Second
)
// DefaultLocale is the default LC_ALL to run git commands in.
@@ -111,7 +111,7 @@ func (c *Command) RunInDirTimeoutEnvFullPipeline(env []string, timeout time.Dura
// it pipes stdout and stderr to given io.Writer and passes in an io.Reader as stdin. Between cmd.Start and cmd.Wait the passed in function is run.
func (c *Command) RunInDirTimeoutEnvFullPipelineFunc(env []string, timeout time.Duration, dir string, stdout, stderr io.Writer, stdin io.Reader, fn func(context.Context, context.CancelFunc) error) error {
if timeout == -1 {
- timeout = DefaultCommandExecutionTimeout
+ timeout = defaultCommandExecutionTimeout
}
if len(dir) == 0 {
diff --git a/modules/git/git.go b/modules/git/git.go
index ce1b15c953..ef6ec0c2bf 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -14,6 +14,7 @@ import (
"time"
"code.gitea.io/gitea/modules/process"
+ "code.gitea.io/gitea/modules/setting"
"github.com/hashicorp/go-version"
)
@@ -106,10 +107,42 @@ func SetExecutablePath(path string) error {
return nil
}
+// VersionInfo returns git version information
+func VersionInfo() string {
+ var format = "Git Version: %s"
+ var args = []interface{}{gitVersion.Original()}
+ // Since git wire protocol has been released from git v2.18
+ if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
+ format += ", Wire Protocol %s Enabled"
+ args = append(args, "Version 2") // for focus color
+ }
+
+ return fmt.Sprintf(format, args...)
+}
+
// Init initializes git module
func Init(ctx context.Context) error {
DefaultContext = ctx
+ defaultCommandExecutionTimeout = time.Duration(setting.Git.Timeout.Default) * time.Second
+
+ if err := SetExecutablePath(setting.Git.Path); err != nil {
+ return err
+ }
+
+ // force cleanup args
+ GlobalCommandArgs = []string{}
+
+ if CheckGitVersionAtLeast("2.9") == nil {
+ // Explicitly disable credential helper, otherwise Git credentials might leak
+ GlobalCommandArgs = append(GlobalCommandArgs, "-c", "credential.helper=")
+ }
+
+ // Since git wire protocol has been released from git v2.18
+ if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
+ GlobalCommandArgs = append(GlobalCommandArgs, "-c", "protocol.version=2")
+ }
+
// Save current git version on init to gitVersion otherwise it would require an RWMutex
if err := LoadGitVersion(); err != nil {
return err
diff --git a/modules/git/lfs.go b/modules/git/lfs.go
new file mode 100644
index 0000000000..79049c9824
--- /dev/null
+++ b/modules/git/lfs.go
@@ -0,0 +1,37 @@
+// Copyright 2021 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 git
+
+import (
+ "sync"
+
+ logger "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+var once sync.Once
+
+// CheckLFSVersion will check lfs version, if not satisfied, then disable it.
+func CheckLFSVersion() {
+ if setting.LFS.StartServer {
+ //Disable LFS client hooks if installed for the current OS user
+ //Needs at least git v2.1.2
+
+ err := LoadGitVersion()
+ if err != nil {
+ logger.Fatal("Error retrieving git version: %v", err)
+ }
+
+ if CheckGitVersionAtLeast("2.1.2") != nil {
+ setting.LFS.StartServer = false
+ logger.Error("LFS server support needs at least Git v2.1.2")
+ } else {
+ once.Do(func() {
+ GlobalCommandArgs = append(GlobalCommandArgs, "-c", "filter.lfs.required=",
+ "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=")
+ })
+ }
+ }
+}
diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go
index 664a7445dd..815aa141e5 100644
--- a/modules/git/repo_commit.go
+++ b/modules/git/repo_commit.go
@@ -12,6 +12,8 @@ import (
"io/ioutil"
"strconv"
"strings"
+
+ "code.gitea.io/gitea/modules/setting"
)
// GetBranchCommitID returns last commit ID string of given branch.
@@ -85,12 +87,6 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) {
return commits.Front().Value.(*Commit), nil
}
-// CommitsRangeSize the default commits range size
-var CommitsRangeSize = 50
-
-// BranchesRangeSize the default branches range size
-var BranchesRangeSize = 20
-
func (repo *Repository) commitsByRange(id SHA1, page, pageSize int) (*list.List, error) {
stdout, err := NewCommand("log", id.String(), "--skip="+strconv.Itoa((page-1)*pageSize),
"--max-count="+strconv.Itoa(pageSize), prettyLogFormat).RunInDirBytes(repo.Path)
@@ -206,7 +202,7 @@ func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) {
// CommitsByFileAndRange return the commits according revison file and the page
func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) (*list.List, error) {
- skip := (page - 1) * CommitsRangeSize
+ skip := (page - 1) * setting.Git.CommitsRangeSize
stdoutReader, stdoutWriter := io.Pipe()
defer func() {
@@ -216,7 +212,7 @@ func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) (
go func() {
stderr := strings.Builder{}
err := NewCommand("log", revision, "--follow",
- "--max-count="+strconv.Itoa(CommitsRangeSize*page),
+ "--max-count="+strconv.Itoa(setting.Git.CommitsRangeSize*page),
prettyLogFormat, "--", file).
RunInDirPipeline(repo.Path, stdoutWriter, &stderr)
if err != nil {
@@ -247,7 +243,7 @@ func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) (
// CommitsByFileAndRangeNoFollow return the commits according revison file and the page
func (repo *Repository) CommitsByFileAndRangeNoFollow(revision, file string, page int) (*list.List, error) {
stdout, err := NewCommand("log", revision, "--skip="+strconv.Itoa((page-1)*50),
- "--max-count="+strconv.Itoa(CommitsRangeSize), prettyLogFormat, "--", file).RunInDirBytes(repo.Path)
+ "--max-count="+strconv.Itoa(setting.Git.CommitsRangeSize), prettyLogFormat, "--", file).RunInDirBytes(repo.Path)
if err != nil {
return nil, err
}
diff --git a/modules/setting/git.go b/modules/setting/git.go
index 308d94894b..7383996cb9 100644
--- a/modules/setting/git.go
+++ b/modules/setting/git.go
@@ -7,7 +7,6 @@ package setting
import (
"time"
- "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
)
@@ -19,8 +18,8 @@ var (
MaxGitDiffLines int
MaxGitDiffLineCharacters int
MaxGitDiffFiles int
- CommitsRangeSize int
- BranchesRangeSize int
+ CommitsRangeSize int // CommitsRangeSize the default commits range size
+ BranchesRangeSize int // BranchesRangeSize the default branches range size
VerbosePush bool
VerbosePushDelay time.Duration
GCArgs []string `ini:"GC_ARGS" delim:" "`
@@ -54,7 +53,7 @@ var (
Pull int
GC int `ini:"GC"`
}{
- Default: int(git.DefaultCommandExecutionTimeout / time.Second),
+ Default: 360,
Migrate: 600,
Mirror: 300,
Clone: 300,
@@ -68,35 +67,4 @@ func newGit() {
if err := Cfg.Section("git").MapTo(&Git); err != nil {
log.Fatal("Failed to map Git settings: %v", err)
}
- if err := git.SetExecutablePath(Git.Path); err != nil {
- log.Fatal("Failed to initialize Git settings: %v", err)
- }
- git.DefaultCommandExecutionTimeout = time.Duration(Git.Timeout.Default) * time.Second
-
- version, err := git.LocalVersion()
- if err != nil {
- log.Fatal("Error retrieving git version: %v", err)
- }
-
- // force cleanup args
- git.GlobalCommandArgs = []string{}
-
- if git.CheckGitVersionAtLeast("2.9") == nil {
- // Explicitly disable credential helper, otherwise Git credentials might leak
- git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "credential.helper=")
- }
-
- var format = "Git Version: %s"
- var args = []interface{}{version.Original()}
- // Since git wire protocol has been released from git v2.18
- if Git.EnableAutoGitWireProtocol && git.CheckGitVersionAtLeast("2.18") == nil {
- git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "protocol.version=2")
- format += ", Wire Protocol %s Enabled"
- args = append(args, "Version 2") // for focus color
- }
-
- git.CommitsRangeSize = Git.CommitsRangeSize
- git.BranchesRangeSize = Git.BranchesRangeSize
-
- log.Info(format, args...)
}
diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go
index 8b9224b86a..a4bbd3c3ff 100644
--- a/modules/setting/lfs.go
+++ b/modules/setting/lfs.go
@@ -9,7 +9,6 @@ import (
"time"
"code.gitea.io/gitea/modules/generate"
- "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
ini "gopkg.in/ini.v1"
@@ -67,24 +66,3 @@ func newLFSService() {
}
}
}
-
-// CheckLFSVersion will check lfs version, if not satisfied, then disable it.
-func CheckLFSVersion() {
- if LFS.StartServer {
- //Disable LFS client hooks if installed for the current OS user
- //Needs at least git v2.1.2
-
- err := git.LoadGitVersion()
- if err != nil {
- log.Fatal("Error retrieving git version: %v", err)
- }
-
- if git.CheckGitVersionAtLeast("2.1.2") != nil {
- LFS.StartServer = false
- log.Error("LFS server support needs at least Git v2.1.2")
- } else {
- git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "filter.lfs.required=",
- "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=")
- }
- }
-}
diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go
index a16cca0f4e..9a0fd1d0b6 100644
--- a/routers/api/v1/repo/commits.go
+++ b/routers/api/v1/repo/commits.go
@@ -143,8 +143,8 @@ func GetAllCommits(ctx *context.APIContext) {
listOptions.Page = 1
}
- if listOptions.PageSize > git.CommitsRangeSize {
- listOptions.PageSize = git.CommitsRangeSize
+ if listOptions.PageSize > setting.Git.CommitsRangeSize {
+ listOptions.PageSize = setting.Git.CommitsRangeSize
}
sha := ctx.Query("sha")
diff --git a/routers/init.go b/routers/init.go
index e52e547517..cc9f703214 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -69,7 +69,9 @@ func GlobalInit(ctx context.Context) {
if err := git.Init(ctx); err != nil {
log.Fatal("Git module init failed: %v", err)
}
- setting.CheckLFSVersion()
+ log.Info(git.VersionInfo())
+
+ git.CheckLFSVersion()
log.Trace("AppPath: %s", setting.AppPath)
log.Trace("AppWorkPath: %s", setting.AppWorkPath)
log.Trace("Custom path: %s", setting.CustomPath)
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index 4625b1a272..da72940144 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repofiles"
repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
@@ -62,8 +63,8 @@ func Branches(ctx *context.Context) {
}
limit := ctx.QueryInt("limit")
- if limit <= 0 || limit > git.BranchesRangeSize {
- limit = git.BranchesRangeSize
+ if limit <= 0 || limit > setting.Git.BranchesRangeSize {
+ limit = setting.Git.BranchesRangeSize
}
skip := (page - 1) * limit
@@ -73,7 +74,7 @@ func Branches(ctx *context.Context) {
return
}
ctx.Data["Branches"] = branches
- pager := context.NewPagination(int(branchesCount), git.BranchesRangeSize, page, 5)
+ pager := context.NewPagination(int(branchesCount), setting.Git.BranchesRangeSize, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 3e6148bcbb..45ef22f498 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -63,7 +63,7 @@ func Commits(ctx *context.Context) {
pageSize := ctx.QueryInt("limit")
if pageSize <= 0 {
- pageSize = git.CommitsRangeSize
+ pageSize = setting.Git.CommitsRangeSize
}
// Both `git log branchName` and `git log commitId` work.
@@ -82,7 +82,7 @@ func Commits(ctx *context.Context) {
ctx.Data["CommitCount"] = commitsCount
ctx.Data["Branch"] = ctx.Repo.BranchName
- pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
+ pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
@@ -250,7 +250,7 @@ func FileHistory(ctx *context.Context) {
ctx.Data["CommitCount"] = commitsCount
ctx.Data["Branch"] = branchName
- pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
+ pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index cceb8451e5..5271fe9b4a 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -316,7 +317,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
ctx.Data["Commits"] = commitsHistory
- pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
+ pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
From 19ac575d572af655ab691f829d0b4de38a1f10be Mon Sep 17 00:00:00 2001
From: zeripath
Date: Sat, 26 Jun 2021 13:47:56 +0100
Subject: [PATCH 29/33] Limit stdout tracelog to actual stdout (#16258)
Related #16243
Signed-off-by: Andrew Thornton
---
modules/git/command.go | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/modules/git/command.go b/modules/git/command.go
index 127e95ecfb..d83c42fdc2 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -199,7 +199,11 @@ func (c *Command) RunInDirTimeoutEnv(env []string, timeout time.Duration, dir st
return nil, ConcatenateError(err, stderr.String())
}
if stdout.Len() > 0 && log.IsTrace() {
- log.Trace("Stdout:\n %s", stdout.Bytes()[:1024])
+ tracelen := stdout.Len()
+ if tracelen > 1024 {
+ tracelen = 1024
+ }
+ log.Trace("Stdout:\n %s", stdout.Bytes()[:tracelen])
}
return stdout.Bytes(), nil
}
From 22a0636544237bcffb46b36b593a501e77ae02cc Mon Sep 17 00:00:00 2001
From: Sergey Dryabzhinsky
Date: Sat, 26 Jun 2021 22:53:14 +0300
Subject: [PATCH 30/33] Add Visible modes function from Organisation to Users
too (#16069)
You can limit or hide organisations. This pull make it also posible for users
- new strings to translte
- add checkbox to user profile form
- add checkbox to admin user.edit form
- filter explore page user search
- filter api admin and public user searches
- allow admins view "hidden" users
- add app option DEFAULT_USER_VISIBILITY
- rewrite many files to use Visibility field
- check for teams intersection
- fix context output
- right fake 404 if not visible
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: Andrew Thornton
---
custom/conf/app.example.ini | 12 +-
.../doc/advanced/config-cheat-sheet.en-us.md | 1 +
integrations/api_user_search_test.go | 31 ++++++
models/fixtures/user.yml | 18 ++-
models/org.go | 16 +--
models/org_test.go | 18 +--
models/repo.go | 3 +-
models/repo_permission.go | 6 +-
models/user.go | 105 +++++++++++++++---
modules/convert/user.go | 4 +
modules/convert/user_test.go | 8 ++
modules/setting/service.go | 4 +
modules/structs/admin_user.go | 2 +
modules/structs/user.go | 2 +
options/locale/locale_en-US.ini | 8 ++
routers/api/v1/admin/user.go | 15 ++-
routers/api/v1/org/org.go | 4 +-
routers/api/v1/repo/repo.go | 4 +-
routers/api/v1/user/helper.go | 2 +-
routers/api/v1/user/user.go | 7 ++
routers/web/admin/orgs.go | 3 +-
routers/web/admin/users.go | 10 +-
routers/web/admin/users_test.go | 81 ++++++++++++++
routers/web/org/home.go | 4 +-
routers/web/user/profile.go | 16 ++-
routers/web/user/setting/profile.go | 1 +
services/forms/admin.go | 3 +
services/forms/user_form.go | 2 +
templates/admin/user/edit.tmpl | 27 +++++
templates/admin/user/new.tmpl | 19 ++++
templates/swagger/v1_json.tmpl | 13 +++
templates/user/settings/profile.tmpl | 59 ++++++++--
32 files changed, 440 insertions(+), 68 deletions(-)
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index fa6a9e3fac..e7fe9206ed 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -651,9 +651,15 @@ PATH =
;DEFAULT_ALLOW_CREATE_ORGANIZATION = true
;;
;; Either "public", "limited" or "private", default is "public"
-;; Limited is for signed user only
-;; Private is only for member of the organization
-;; Public is for everyone
+;; Limited is for users visible only to signed users
+;; Private is for users visible only to members of their organizations
+;; Public is for users visible for everyone
+;DEFAULT_USER_VISIBILITY = public
+;;
+;; Either "public", "limited" or "private", default is "public"
+;; Limited is for organizations visible only to signed users
+;; Private is for organizations visible only to members of the organization
+;; Public is for organizations visible to everyone
;DEFAULT_ORG_VISIBILITY = public
;;
;; Default value for DefaultOrgMemberVisible
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index aa9eb7e0ca..21359dcab1 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -512,6 +512,7 @@ relation to port exhaustion.
- `SHOW_MILESTONES_DASHBOARD_PAGE`: **true** Enable this to show the milestones dashboard page - a view of all the user's milestones
- `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created
- `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it
+- `DEFAULT_USER_VISIBILITY`: **public**: Set default visibility mode for users, either "public", "limited" or "private".
- `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private".
- `DEFAULT_ORG_MEMBER_VISIBLE`: **false** True will make the membership of the users visible when added to the organisation.
- `ALLOW_ONLY_INTERNAL_REGISTRATION`: **false** Set to true to force registration only via gitea.
diff --git a/integrations/api_user_search_test.go b/integrations/api_user_search_test.go
index c5295fbba5..f7349827e5 100644
--- a/integrations/api_user_search_test.go
+++ b/integrations/api_user_search_test.go
@@ -59,3 +59,34 @@ func TestAPIUserSearchNotLoggedIn(t *testing.T) {
}
}
}
+
+func TestAPIUserSearchAdminLoggedInUserHidden(t *testing.T) {
+ defer prepareTestEnv(t)()
+ adminUsername := "user1"
+ session := loginUser(t, adminUsername)
+ token := getTokenForLoggedInUser(t, session)
+ query := "user31"
+ req := NewRequestf(t, "GET", "/api/v1/users/search?token=%s&q=%s", token, query)
+ req.SetBasicAuth(token, "x-oauth-basic")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var results SearchResults
+ DecodeJSON(t, resp, &results)
+ assert.NotEmpty(t, results.Data)
+ for _, user := range results.Data {
+ assert.Contains(t, user.UserName, query)
+ assert.NotEmpty(t, user.Email)
+ assert.EqualValues(t, "private", user.Visibility)
+ }
+}
+
+func TestAPIUserSearchNotLoggedInUserHidden(t *testing.T) {
+ defer prepareTestEnv(t)()
+ query := "user31"
+ req := NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var results SearchResults
+ DecodeJSON(t, resp, &results)
+ assert.Empty(t, results.Data)
+}
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index d903a7942f..850ee4041d 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -508,7 +508,6 @@
num_repos: 0
is_active: true
-
-
id: 30
lower_name: user30
@@ -525,3 +524,20 @@
avatar_email: user30@example.com
num_repos: 2
is_active: true
+
+-
+ id: 31
+ lower_name: user31
+ name: user31
+ full_name: "user31"
+ email: user31@example.com
+ passwd_hash_algo: argon2
+ passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password
+ type: 0 # individual
+ salt: ZogKvWdyEx
+ is_admin: false
+ visibility: 2
+ avatar: avatar31
+ avatar_email: user31@example.com
+ num_repos: 0
+ is_active: true
diff --git a/models/org.go b/models/org.go
index 7f9e3cce5b..073b26c2f8 100644
--- a/models/org.go
+++ b/models/org.go
@@ -455,22 +455,22 @@ func getOwnedOrgsByUserID(sess *xorm.Session, userID int64) ([]*User, error) {
Find(&orgs)
}
-// HasOrgVisible tells if the given user can see the given org
-func HasOrgVisible(org, user *User) bool {
- return hasOrgVisible(x, org, user)
+// HasOrgOrUserVisible tells if the given user can see the given org or user
+func HasOrgOrUserVisible(org, user *User) bool {
+ return hasOrgOrUserVisible(x, org, user)
}
-func hasOrgVisible(e Engine, org, user *User) bool {
+func hasOrgOrUserVisible(e Engine, orgOrUser, user *User) bool {
// Not SignedUser
if user == nil {
- return org.Visibility == structs.VisibleTypePublic
+ return orgOrUser.Visibility == structs.VisibleTypePublic
}
- if user.IsAdmin {
+ if user.IsAdmin || orgOrUser.ID == user.ID {
return true
}
- if (org.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !org.hasMemberWithUserID(e, user.ID) {
+ if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !orgOrUser.hasMemberWithUserID(e, user.ID) {
return false
}
return true
@@ -483,7 +483,7 @@ func HasOrgsVisible(orgs []*User, user *User) bool {
}
for _, org := range orgs {
- if HasOrgVisible(org, user) {
+ if HasOrgOrUserVisible(org, user) {
return true
}
}
diff --git a/models/org_test.go b/models/org_test.go
index bed7a6eb86..e494e502dd 100644
--- a/models/org_test.go
+++ b/models/org_test.go
@@ -586,9 +586,9 @@ func TestHasOrgVisibleTypePublic(t *testing.T) {
assert.NoError(t, CreateOrganization(org, owner))
org = AssertExistsAndLoadBean(t,
&User{Name: org.Name, Type: UserTypeOrganization}).(*User)
- test1 := HasOrgVisible(org, owner)
- test2 := HasOrgVisible(org, user3)
- test3 := HasOrgVisible(org, nil)
+ test1 := HasOrgOrUserVisible(org, owner)
+ test2 := HasOrgOrUserVisible(org, user3)
+ test3 := HasOrgOrUserVisible(org, nil)
assert.True(t, test1) // owner of org
assert.True(t, test2) // user not a part of org
assert.True(t, test3) // logged out user
@@ -609,9 +609,9 @@ func TestHasOrgVisibleTypeLimited(t *testing.T) {
assert.NoError(t, CreateOrganization(org, owner))
org = AssertExistsAndLoadBean(t,
&User{Name: org.Name, Type: UserTypeOrganization}).(*User)
- test1 := HasOrgVisible(org, owner)
- test2 := HasOrgVisible(org, user3)
- test3 := HasOrgVisible(org, nil)
+ test1 := HasOrgOrUserVisible(org, owner)
+ test2 := HasOrgOrUserVisible(org, user3)
+ test3 := HasOrgOrUserVisible(org, nil)
assert.True(t, test1) // owner of org
assert.True(t, test2) // user not a part of org
assert.False(t, test3) // logged out user
@@ -632,9 +632,9 @@ func TestHasOrgVisibleTypePrivate(t *testing.T) {
assert.NoError(t, CreateOrganization(org, owner))
org = AssertExistsAndLoadBean(t,
&User{Name: org.Name, Type: UserTypeOrganization}).(*User)
- test1 := HasOrgVisible(org, owner)
- test2 := HasOrgVisible(org, user3)
- test3 := HasOrgVisible(org, nil)
+ test1 := HasOrgOrUserVisible(org, owner)
+ test2 := HasOrgOrUserVisible(org, user3)
+ test3 := HasOrgOrUserVisible(org, nil)
assert.True(t, test1) // owner of org
assert.False(t, test2) // user not a part of org
assert.False(t, test3) // logged out user
diff --git a/models/repo.go b/models/repo.go
index 4ce3d4839b..92d8427fab 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -585,8 +585,7 @@ func (repo *Repository) getReviewers(e Engine, doerID, posterID int64) ([]*User,
var users []*User
- if repo.IsPrivate ||
- (repo.Owner.IsOrganization() && repo.Owner.Visibility == api.VisibleTypePrivate) {
+ if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate {
// This a private repository:
// Anyone who can read the repository is a requestable reviewer
if err := e.
diff --git a/models/repo_permission.go b/models/repo_permission.go
index 138613b2e9..4f043a58cc 100644
--- a/models/repo_permission.go
+++ b/models/repo_permission.go
@@ -176,9 +176,9 @@ func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permiss
return
}
- // Prevent strangers from checking out public repo of private orginization
- // Allow user if they are collaborator of a repo within a private orginization but not a member of the orginization itself
- if repo.Owner.IsOrganization() && !hasOrgVisible(e, repo.Owner, user) && !isCollaborator {
+ // Prevent strangers from checking out public repo of private orginization/users
+ // Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself
+ if !hasOrgOrUserVisible(e, repo.Owner, user) && !isCollaborator {
perm.AccessMode = AccessModeNone
return
}
diff --git a/models/user.go b/models/user.go
index 5998341422..221c840a7f 100644
--- a/models/user.go
+++ b/models/user.go
@@ -432,6 +432,62 @@ func (u *User) IsPasswordSet() bool {
return len(u.Passwd) != 0
}
+// IsVisibleToUser check if viewer is able to see user profile
+func (u *User) IsVisibleToUser(viewer *User) bool {
+ return u.isVisibleToUser(x, viewer)
+}
+
+func (u *User) isVisibleToUser(e Engine, viewer *User) bool {
+ if viewer != nil && viewer.IsAdmin {
+ return true
+ }
+
+ switch u.Visibility {
+ case structs.VisibleTypePublic:
+ return true
+ case structs.VisibleTypeLimited:
+ if viewer == nil || viewer.IsRestricted {
+ return false
+ }
+ return true
+ case structs.VisibleTypePrivate:
+ if viewer == nil || viewer.IsRestricted {
+ return false
+ }
+
+ // If they follow - they see each over
+ follower := IsFollowing(u.ID, viewer.ID)
+ if follower {
+ return true
+ }
+
+ // Now we need to check if they in some organization together
+ count, err := x.Table("team_user").
+ Where(
+ builder.And(
+ builder.Eq{"uid": viewer.ID},
+ builder.Or(
+ builder.Eq{"org_id": u.ID},
+ builder.In("org_id",
+ builder.Select("org_id").
+ From("team_user", "t2").
+ Where(builder.Eq{"uid": u.ID}))))).
+ Count(new(TeamUser))
+ if err != nil {
+ return false
+ }
+
+ if count < 0 {
+ // No common organization
+ return false
+ }
+
+ // they are in an organization together
+ return true
+ }
+ return false
+}
+
// IsOrganization returns true if user is actually a organization.
func (u *User) IsOrganization() bool {
return u.Type == UserTypeOrganization
@@ -796,8 +852,13 @@ func IsUsableUsername(name string) error {
return isUsableName(reservedUsernames, reservedUserPatterns, name)
}
+// CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation
+type CreateUserOverwriteOptions struct {
+ Visibility structs.VisibleType
+}
+
// CreateUser creates record of a new user.
-func CreateUser(u *User) (err error) {
+func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
if err = IsUsableUsername(u.Name); err != nil {
return err
}
@@ -831,8 +892,6 @@ func CreateUser(u *User) (err error) {
return ErrEmailAlreadyUsed{u.Email}
}
- u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
-
u.LowerName = strings.ToLower(u.Name)
u.AvatarEmail = u.Email
if u.Rands, err = GetUserSalt(); err != nil {
@@ -841,10 +900,18 @@ func CreateUser(u *User) (err error) {
if err = u.SetPassword(u.Passwd); err != nil {
return err
}
+
+ // set system defaults
+ u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
+ u.Visibility = setting.Service.DefaultUserVisibilityMode
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
u.MaxRepoCreation = -1
u.Theme = setting.UI.DefaultTheme
+ // overwrite defaults if set
+ if len(overwriteDefault) != 0 && overwriteDefault[0] != nil {
+ u.Visibility = overwriteDefault[0].Visibility
+ }
if _, err = sess.Insert(u); err != nil {
return err
@@ -1527,10 +1594,9 @@ func (opts *SearchUserOptions) toConds() builder.Cond {
cond = cond.And(keywordCond)
}
+ // If visibility filtered
if len(opts.Visible) > 0 {
cond = cond.And(builder.In("visibility", opts.Visible))
- } else {
- cond = cond.And(builder.In("visibility", structs.VisibleTypePublic))
}
if opts.Actor != nil {
@@ -1543,16 +1609,27 @@ func (opts *SearchUserOptions) toConds() builder.Cond {
exprCond = builder.Expr("org_user.org_id = \"user\".id")
}
- var accessCond builder.Cond
- if !opts.Actor.IsRestricted {
- accessCond = builder.Or(
- builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))),
- builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
- } else {
- // restricted users only see orgs they are a member of
- accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID})))
+ // If Admin - they see all users!
+ if !opts.Actor.IsAdmin {
+ // Force visiblity for privacy
+ var accessCond builder.Cond
+ if !opts.Actor.IsRestricted {
+ accessCond = builder.Or(
+ builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))),
+ builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
+ } else {
+ // restricted users only see orgs they are a member of
+ accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID})))
+ }
+ // Don't forget about self
+ accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID})
+ cond = cond.And(accessCond)
}
- cond = cond.And(accessCond)
+
+ } else {
+ // Force visiblity for privacy
+ // Not logged in - only public users
+ cond = cond.And(builder.In("visibility", structs.VisibleTypePublic))
}
if opts.UID > 0 {
diff --git a/modules/convert/user.go b/modules/convert/user.go
index 894be3bd44..164ffb71fd 100644
--- a/modules/convert/user.go
+++ b/modules/convert/user.go
@@ -62,10 +62,14 @@ func toUser(user *models.User, signed, authed bool) *api.User {
Following: user.NumFollowing,
StarredRepos: user.NumStars,
}
+
+ result.Visibility = user.Visibility.String()
+
// hide primary email if API caller is anonymous or user keep email private
if signed && (!user.KeepEmailPrivate || authed) {
result.Email = user.Email
}
+
// only site admin will get these information and possibly user himself
if authed {
result.IsAdmin = user.IsAdmin
diff --git a/modules/convert/user_test.go b/modules/convert/user_test.go
index 7837910ffe..679c4f9894 100644
--- a/modules/convert/user_test.go
+++ b/modules/convert/user_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"code.gitea.io/gitea/models"
+ api "code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert"
)
@@ -27,4 +28,11 @@ func TestUser_ToUser(t *testing.T) {
apiUser = toUser(user1, false, false)
assert.False(t, apiUser.IsAdmin)
+ assert.EqualValues(t, api.VisibleTypePublic.String(), apiUser.Visibility)
+
+ user31 := models.AssertExistsAndLoadBean(t, &models.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate}).(*models.User)
+
+ apiUser = toUser(user31, true, true)
+ assert.False(t, apiUser.IsAdmin)
+ assert.EqualValues(t, api.VisibleTypePrivate.String(), apiUser.Visibility)
}
diff --git a/modules/setting/service.go b/modules/setting/service.go
index bd70c7e6eb..3f689212f3 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -15,6 +15,8 @@ import (
// Service settings
var Service struct {
+ DefaultUserVisibility string
+ DefaultUserVisibilityMode structs.VisibleType
DefaultOrgVisibility string
DefaultOrgVisibilityMode structs.VisibleType
ActiveCodeLives int
@@ -118,6 +120,8 @@ func newService() {
Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)
Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true)
Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false)
+ Service.DefaultUserVisibility = sec.Key("DEFAULT_USER_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes))
+ Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility]
Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes))
Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility]
Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool()
diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go
index 5da4e9608b..facf16a395 100644
--- a/modules/structs/admin_user.go
+++ b/modules/structs/admin_user.go
@@ -19,6 +19,7 @@ type CreateUserOption struct {
Password string `json:"password" binding:"Required;MaxSize(255)"`
MustChangePassword *bool `json:"must_change_password"`
SendNotify bool `json:"send_notify"`
+ Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
}
// EditUserOption edit user options
@@ -43,4 +44,5 @@ type EditUserOption struct {
ProhibitLogin *bool `json:"prohibit_login"`
AllowCreateOrganization *bool `json:"allow_create_organization"`
Restricted *bool `json:"restricted"`
+ Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
}
diff --git a/modules/structs/user.go b/modules/structs/user.go
index 0d8b0300c3..a3c8f0c32a 100644
--- a/modules/structs/user.go
+++ b/modules/structs/user.go
@@ -43,6 +43,8 @@ type User struct {
Website string `json:"website"`
// the user's description
Description string `json:"description"`
+ // User visibility level option: public, limited, private
+ Visibility string `json:"visibility"`
// user counts
Followers int `json:"followers_count"`
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 4a79ffa7eb..e0ece8f9f0 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -724,6 +724,14 @@ email_notifications.onmention = Only Email on Mention
email_notifications.disable = Disable Email Notifications
email_notifications.submit = Set Email Preference
+visibility = User visibility
+visibility.public = Public
+visibility.public_tooltip = Visible to all users
+visibility.limited = Limited
+visibility.limited_tooltip = Visible to logged in users only
+visibility.private = Private
+visibility.private_tooltip = Visible only to organization members
+
[repo]
new_repo_helper = A repository contains all project files, including revision history. Already have it elsewhere? Migrate repository.
owner = Owner
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 4bbe7f77ba..6bc9b849b1 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -66,6 +66,7 @@ func CreateUser(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateUserOption)
+
u := &models.User{
Name: form.Username,
FullName: form.FullName,
@@ -97,7 +98,15 @@ func CreateUser(ctx *context.APIContext) {
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
return
}
- if err := models.CreateUser(u); err != nil {
+
+ var overwriteDefault *models.CreateUserOverwriteOptions
+ if form.Visibility != "" {
+ overwriteDefault = &models.CreateUserOverwriteOptions{
+ Visibility: api.VisibilityModes[form.Visibility],
+ }
+ }
+
+ if err := models.CreateUser(u, overwriteDefault); err != nil {
if models.IsErrUserAlreadyExist(err) ||
models.IsErrEmailAlreadyUsed(err) ||
models.IsErrNameReserved(err) ||
@@ -209,6 +218,9 @@ func EditUser(ctx *context.APIContext) {
if form.Active != nil {
u.IsActive = *form.Active
}
+ if len(form.Visibility) != 0 {
+ u.Visibility = api.VisibilityModes[form.Visibility]
+ }
if form.Admin != nil {
u.IsAdmin = *form.Admin
}
@@ -395,6 +407,7 @@ func GetAllUsers(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
users, maxResults, err := models.SearchUsers(&models.SearchUserOptions{
+ Actor: ctx.User,
Type: models.UserTypeIndividual,
OrderBy: models.SearchOrderByAlphabetically,
ListOptions: listOptions,
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index f4a634f4d5..5c16594f89 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -225,8 +225,8 @@ func Get(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/Organization"
- if !models.HasOrgVisible(ctx.Org.Organization, ctx.User) {
- ctx.NotFound("HasOrgVisible", nil)
+ if !models.HasOrgOrUserVisible(ctx.Org.Organization, ctx.User) {
+ ctx.NotFound("HasOrgOrUserVisible", nil)
return
}
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx.Org.Organization))
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 7a3160fa99..35d3490510 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -375,8 +375,8 @@ func CreateOrgRepo(ctx *context.APIContext) {
return
}
- if !models.HasOrgVisible(org, ctx.User) {
- ctx.NotFound("HasOrgVisible", nil)
+ if !models.HasOrgOrUserVisible(org, ctx.User) {
+ ctx.NotFound("HasOrgOrUserVisible", nil)
return
}
diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go
index fcdac257ed..a3500e0ee6 100644
--- a/routers/api/v1/user/helper.go
+++ b/routers/api/v1/user/helper.go
@@ -17,7 +17,7 @@ func GetUserByParamsName(ctx *context.APIContext, name string) *models.User {
user, err := models.GetUserByName(username)
if err != nil {
if models.IsErrUserNotExist(err) {
- if redirectUserID, err := models.LookupUserRedirect(username); err == nil {
+ if redirectUserID, err2 := models.LookupUserRedirect(username); err2 == nil {
context.RedirectToUser(ctx.Context, username, redirectUserID)
} else {
ctx.NotFound("GetUserByName", err)
diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go
index 4adae532fd..ac543d597d 100644
--- a/routers/api/v1/user/user.go
+++ b/routers/api/v1/user/user.go
@@ -57,6 +57,7 @@ func Search(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
opts := &models.SearchUserOptions{
+ Actor: ctx.User,
Keyword: strings.Trim(ctx.Query("q"), " "),
UID: ctx.QueryInt64("uid"),
Type: models.UserTypeIndividual,
@@ -102,10 +103,16 @@ func GetInfo(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
u := GetUserByParams(ctx)
+
if ctx.Written() {
return
}
+ if !u.IsVisibleToUser(ctx.User) {
+ // fake ErrUserNotExist error message to not leak information about existence
+ ctx.NotFound("GetUserByName", models.ErrUserNotExist{Name: ctx.Params(":username")})
+ return
+ }
ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.User))
}
diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go
index 618f945704..a2b3ed1bcc 100644
--- a/routers/web/admin/orgs.go
+++ b/routers/web/admin/orgs.go
@@ -25,7 +25,8 @@ func Organizations(ctx *context.Context) {
ctx.Data["PageIsAdminOrganizations"] = true
explore.RenderUserSearch(ctx, &models.SearchUserOptions{
- Type: models.UserTypeOrganization,
+ Actor: ctx.User,
+ Type: models.UserTypeOrganization,
ListOptions: models.ListOptions{
PageSize: setting.UI.Admin.OrgPagingNum,
},
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index 1b65795865..dc2a97e526 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -37,7 +37,8 @@ func Users(ctx *context.Context) {
ctx.Data["PageIsAdminUsers"] = true
explore.RenderUserSearch(ctx, &models.SearchUserOptions{
- Type: models.UserTypeIndividual,
+ Actor: ctx.User,
+ Type: models.UserTypeIndividual,
ListOptions: models.ListOptions{
PageSize: setting.UI.Admin.UserPagingNum,
},
@@ -50,6 +51,7 @@ func NewUser(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminUsers"] = true
+ ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode
ctx.Data["login_type"] = "0-0"
@@ -70,6 +72,7 @@ func NewUserPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminUsers"] = true
+ ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode
sources, err := models.LoginSources()
if err != nil {
@@ -126,7 +129,8 @@ func NewUserPost(ctx *context.Context) {
}
u.MustChangePassword = form.MustChangePassword
}
- if err := models.CreateUser(u); err != nil {
+
+ if err := models.CreateUser(u, &models.CreateUserOverwriteOptions{Visibility: form.Visibility}); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
@@ -312,6 +316,8 @@ func EditUserPost(ctx *context.Context) {
u.AllowImportLocal = form.AllowImportLocal
u.AllowCreateOrganization = form.AllowCreateOrganization
+ u.Visibility = form.Visibility
+
// skip self Prohibit Login
if ctx.User.ID == u.ID {
u.ProhibitLogin = false
diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go
index b19dcb886b..17c5a309b4 100644
--- a/routers/web/admin/users_test.go
+++ b/routers/web/admin/users_test.go
@@ -8,6 +8,8 @@ import (
"testing"
"code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
@@ -121,3 +123,82 @@ func TestNewUserPost_InvalidEmail(t *testing.T) {
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
+
+func TestNewUserPost_VisiblityDefaultPublic(t *testing.T) {
+
+ models.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "admin/users/new")
+
+ u := models.AssertExistsAndLoadBean(t, &models.User{
+ IsAdmin: true,
+ ID: 2,
+ }).(*models.User)
+
+ ctx.User = u
+
+ username := "gitea"
+ email := "gitea@gitea.io"
+
+ form := forms.AdminCreateUserForm{
+ LoginType: "local",
+ LoginName: "local",
+ UserName: username,
+ Email: email,
+ Password: "abc123ABC!=$",
+ SendNotify: false,
+ MustChangePassword: false,
+ }
+
+ web.SetForm(ctx, &form)
+ NewUserPost(ctx)
+
+ assert.NotEmpty(t, ctx.Flash.SuccessMsg)
+
+ u, err := models.GetUserByName(username)
+
+ assert.NoError(t, err)
+ assert.Equal(t, username, u.Name)
+ assert.Equal(t, email, u.Email)
+ // As default user visibility
+ assert.Equal(t, setting.Service.DefaultUserVisibilityMode, u.Visibility)
+}
+
+func TestNewUserPost_VisibilityPrivate(t *testing.T) {
+
+ models.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "admin/users/new")
+
+ u := models.AssertExistsAndLoadBean(t, &models.User{
+ IsAdmin: true,
+ ID: 2,
+ }).(*models.User)
+
+ ctx.User = u
+
+ username := "gitea"
+ email := "gitea@gitea.io"
+
+ form := forms.AdminCreateUserForm{
+ LoginType: "local",
+ LoginName: "local",
+ UserName: username,
+ Email: email,
+ Password: "abc123ABC!=$",
+ SendNotify: false,
+ MustChangePassword: false,
+ Visibility: api.VisibleTypePrivate,
+ }
+
+ web.SetForm(ctx, &form)
+ NewUserPost(ctx)
+
+ assert.NotEmpty(t, ctx.Flash.SuccessMsg)
+
+ u, err := models.GetUserByName(username)
+
+ assert.NoError(t, err)
+ assert.Equal(t, username, u.Name)
+ assert.Equal(t, email, u.Email)
+ // As default user visibility
+ assert.True(t, u.Visibility.IsPrivate())
+}
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index ad14f18454..aad0a2a90b 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -30,8 +30,8 @@ func Home(ctx *context.Context) {
org := ctx.Org.Organization
- if !models.HasOrgVisible(org, ctx.User) {
- ctx.NotFound("HasOrgVisible", nil)
+ if !models.HasOrgOrUserVisible(org, ctx.User) {
+ ctx.NotFound("HasOrgOrUserVisible", nil)
return
}
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 72d0066645..631ca21135 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -75,6 +75,17 @@ func Profile(ctx *context.Context) {
return
}
+ if ctxUser.IsOrganization() {
+ org.Home(ctx)
+ return
+ }
+
+ // check view permissions
+ if !ctxUser.IsVisibleToUser(ctx.User) {
+ ctx.NotFound("user", fmt.Errorf(uname))
+ return
+ }
+
// Show SSH keys.
if isShowKeys {
ShowSSHKeys(ctx, ctxUser.ID)
@@ -87,11 +98,6 @@ func Profile(ctx *context.Context) {
return
}
- if ctxUser.IsOrganization() {
- org.Home(ctx)
- return
- }
-
// Show OpenID URIs
openIDs, err := models.GetUserOpenIDs(ctxUser.ID)
if err != nil {
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 20042caca4..463c4ec203 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -114,6 +114,7 @@ func ProfilePost(ctx *context.Context) {
}
ctx.User.Description = form.Description
ctx.User.KeepActivityPrivate = form.KeepActivityPrivate
+ ctx.User.Visibility = form.Visibility
if err := models.UpdateUserSetting(ctx.User); err != nil {
if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
diff --git a/services/forms/admin.go b/services/forms/admin.go
index 2e6bbaf172..5abef0550e 100644
--- a/services/forms/admin.go
+++ b/services/forms/admin.go
@@ -8,6 +8,7 @@ import (
"net/http"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web/middleware"
"gitea.com/go-chi/binding"
@@ -22,6 +23,7 @@ type AdminCreateUserForm struct {
Password string `binding:"MaxSize(255)"`
SendNotify bool
MustChangePassword bool
+ Visibility structs.VisibleType
}
// Validate validates form fields
@@ -49,6 +51,7 @@ type AdminEditUserForm struct {
AllowCreateOrganization bool
ProhibitLogin bool
Reset2FA bool `form:"reset_2fa"`
+ Visibility structs.VisibleType
}
// Validate validates form fields
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index 903a625da0..439ddfc7c6 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web/middleware"
"gitea.com/go-chi/binding"
@@ -230,6 +231,7 @@ type UpdateProfileForm struct {
Location string `binding:"MaxSize(50)"`
Language string
Description string `binding:"MaxSize(255)"`
+ Visibility structs.VisibleType
KeepActivityPrivate bool
}
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index af01489c0a..dba24d9837 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -28,6 +28,33 @@
+
+