From 181b7c99eddb373b4415b160dde9109cd4191878 Mon Sep 17 00:00:00 2001 From: suisseWalter <42143099+suisseWalter@users.noreply.github.com> Date: Thu, 23 May 2019 19:12:59 +0200 Subject: [PATCH 01/44] Added Note about arm7 version to doc (#6983) Appended the Troubleshooting section with a subsection about the problems with the arm7 version and the recommendation to switch to arm6. --- docs/content/doc/installation/from-binary.en-us.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/content/doc/installation/from-binary.en-us.md b/docs/content/doc/installation/from-binary.en-us.md index 7e795029db171..12e38e8096d52 100644 --- a/docs/content/doc/installation/from-binary.en-us.md +++ b/docs/content/doc/installation/from-binary.en-us.md @@ -143,6 +143,15 @@ bind: address already in use` Gitea needs to be started on another free port. Th is possible using `./gitea web -p $PORT`. It's possible another instance of Gitea is already running. +### Running Gitea on Raspbian + +As of v1.8, there is a problem with the arm7 version of Gitea and it doesn't run on Raspberry Pi and similar devices. + +It is therefore recommended to switch to the arm6 version which has been tested and shown to work on Raspberry Pi and similar devices. + + ### Git error after updating to a new version of Gitea If the binary file name has been changed during the update to a new version of Gitea, From d5a98a29690e13f717fb72e8635fbea57e58f546 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 24 May 2019 12:15:26 +0800 Subject: [PATCH 02/44] Add support of utf8mb4 for mysql (#6992) --- custom/conf/app.ini.sample | 3 +++ .../doc/advanced/config-cheat-sheet.en-us.md | 1 + .../doc/advanced/config-cheat-sheet.zh-cn.md | 3 ++- models/models.go | 9 +++++---- modules/auth/user_form.go | 1 + options/locale/locale_en-US.ini | 3 ++- public/js/index.js | 6 +++--- routers/install.go | 1 + templates/install.tmpl | 17 ++++++++++++++++- 9 files changed, 34 insertions(+), 10 deletions(-) diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 3ce7268d9d386..547bc9e935076 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -260,6 +260,9 @@ PASSWD = ; For Postgres, either "disable" (default), "require", or "verify-full" ; For MySQL, either "false" (default), "true", or "skip-verify" SSL_MODE = disable +; For MySQL only, either "utf8" or "utf8mb4", default is "utf8". +; NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this. +CHARSET = utf8 ; For "sqlite3" and "tidb", use an absolute path when you start gitea as service PATH = data/gitea.db ; For "sqlite3" only. Query timeout 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 882f8a8a925cf..9b9578ca485dc 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -160,6 +160,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `USER`: **root**: Database username. - `PASSWD`: **\**: Database user password. Use \`your password\` for quoting if you use special characters in the password. - `SSL_MODE`: **disable**: For PostgreSQL and MySQL only. +- `CHARSET`: **utf8**: For MySQL only, either "utf8" or "utf8mb4", default is "utf8". NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this. - `PATH`: **data/gitea.db**: For SQLite3 only, the database file path. - `LOG_SQL`: **true**: Log the executed SQL. - `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed. 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 021233f2d2c32..4f34e0b905632 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -78,7 +78,8 @@ menu: - `NAME`: 数据库名称。 - `USER`: 数据库用户名。 - `PASSWD`: 数据库用户密码。 -- `SSL_MODE`: PostgreSQL数据库是否启用SSL模式。 +- `SSL_MODE`: MySQL 或 PostgreSQL数据库是否启用SSL模式。 +- `CHARSET`: **utf8**: 仅当数据库为 MySQL 时有效, 可以为 "utf8" 或 "utf8mb4"。注意:如果使用 "utf8mb4",你的 MySQL InnoDB 版本必须在 5.6 以上。 - `PATH`: Tidb 或者 SQLite3 数据文件存放路径。 - `LOG_SQL`: **true**: 显示生成的SQL,默认为真。 diff --git a/models/models.go b/models/models.go index c7e58737ede83..85318af870dc3 100644 --- a/models/models.go +++ b/models/models.go @@ -59,8 +59,8 @@ var ( // DbCfg holds the database settings DbCfg struct { - Type, Host, Name, User, Passwd, Path, SSLMode string - Timeout int + Type, Host, Name, User, Passwd, Path, SSLMode, Charset string + Timeout int } // EnableSQLite3 use SQLite3 @@ -160,6 +160,7 @@ func LoadConfigs() { DbCfg.Passwd = sec.Key("PASSWD").String() } DbCfg.SSLMode = sec.Key("SSL_MODE").MustString("disable") + DbCfg.Charset = sec.Key("CHARSET").In("utf8", []string{"utf8", "utf8mb4"}) DbCfg.Path = sec.Key("PATH").MustString(filepath.Join(setting.AppDataPath, "gitea.db")) DbCfg.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) } @@ -222,8 +223,8 @@ func getEngine() (*xorm.Engine, error) { if tls == "disable" { // allow (Postgres-inspired) default value to work in MySQL tls = "false" } - connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%scharset=utf8&parseTime=true&tls=%s", - DbCfg.User, DbCfg.Passwd, connType, DbCfg.Host, DbCfg.Name, Param, tls) + connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%scharset=%s&parseTime=true&tls=%s", + DbCfg.User, DbCfg.Passwd, connType, DbCfg.Host, DbCfg.Name, Param, DbCfg.Charset, tls) case "postgres": connStr = getPostgreSQLConnectionString(DbCfg.Host, DbCfg.User, DbCfg.Passwd, DbCfg.Name, Param, DbCfg.SSLMode) case "mssql": diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go index 38ee5415d954f..8b9e5877d9565 100644 --- a/modules/auth/user_form.go +++ b/modules/auth/user_form.go @@ -23,6 +23,7 @@ type InstallForm struct { DbPasswd string DbName string SSLMode string + Charset string `binding:"Required;In(utf8,utf8mb4)"` DbPath string AppName string `binding:"Required" locale:"install.app_name"` diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a85221ab74b96..f08bc6b5a5cf0 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -86,8 +86,9 @@ host = Host user = Username password = Password db_name = Database Name -db_helper = Note to MySQL users: please use the InnoDB storage engine and the 'utf8_general_ci' character set. +db_helper = Note to MySQL users: please use the InnoDB storage engine and if you use "utf8mb4", your InnoDB version must be greater than 5.6 . ssl_mode = SSL +charset = Charset path = Path sqlite_helper = File path for the SQLite3 database.
Enter an absolute path if you run Gitea as a service. err_empty_db_path = The SQLite3 database path cannot be empty. diff --git a/public/js/index.js b/public/js/index.js index 96d55eca877cd..745a631435733 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -587,15 +587,14 @@ function initInstall() { var tidbDefault = 'data/gitea_tidb'; var dbType = $(this).val(); - if (dbType === "SQLite3" || dbType === "TiDB") { + if (dbType === "SQLite3") { $('#sql_settings').hide(); $('#pgsql_settings').hide(); + $('#mysql_settings').hide(); $('#sqlite_settings').show(); if (dbType === "SQLite3" && $('#db_path').val() == tidbDefault) { $('#db_path').val(sqliteDefault); - } else if (dbType === "TiDB" && $('#db_path').val() == sqliteDefault) { - $('#db_path').val(tidbDefault); } return; } @@ -610,6 +609,7 @@ function initInstall() { $('#sql_settings').show(); $('#pgsql_settings').toggle(dbType === "PostgreSQL"); + $('#mysql_settings').toggle(dbType === "MySQL"); $.each(dbDefaults, function(_type, defaultHost) { if ($('#db_host').val() == defaultHost) { $('#db_host').val(dbDefaults[dbType]); diff --git a/routers/install.go b/routers/install.go index 28bca2b4f7243..a404e96b5111e 100644 --- a/routers/install.go +++ b/routers/install.go @@ -150,6 +150,7 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) { models.DbCfg.Passwd = form.DbPasswd models.DbCfg.Name = form.DbName models.DbCfg.SSLMode = form.SSLMode + models.DbCfg.Charset = form.Charset models.DbCfg.Path = form.DbPath if (models.DbCfg.Type == "sqlite3") && diff --git a/templates/install.tmpl b/templates/install.tmpl index f45052ccd07ff..f8d1ef04e4e21 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -28,7 +28,7 @@ -
+
@@ -64,6 +64,21 @@
+
+
+ + +
+
+
From a98e085031bedb53a9776031afde7073af81feaf Mon Sep 17 00:00:00 2001 From: Vladimir Panteleev Date: Fri, 24 May 2019 10:52:05 +0300 Subject: [PATCH 03/44] Show git-notes (#6984) * Show git-notes * Make git-notes heading text localizable * Refactor git-notes data fetching to a separate function * Display the author and time of git notes * Move note bubble inside the commit bubble * Revert "Move note bubble inside the commit bubble" This reverts commit c0951fe0e3b4dea38064515546b1825c1bcf19e1. * Add test for git-notes * testing ui * Polish CSS * Apply suggestions from code review Co-Authored-By: Lauris BH --- modules/git/notes.go | 60 ++++++++++++++++++ modules/git/notes_test.go | 24 +++++++ modules/git/repo_ref_test.go | 3 +- .../28/345b214c5967bd9cdd98cc7f88f2f1ac574e02 | Bin 0 -> 85 bytes .../a4/79ead1abb694ffca26f67b09c8313b12fa2a13 | Bin 0 -> 30 bytes .../ca/6b5ddf303169a72d2a2971acde4f6eea194e5c | 4 ++ .../tests/repos/repo1_bare/refs/notes/commits | 1 + modules/templates/helper.go | 12 ++++ options/locale/locale_en-US.ini | 1 + public/css/index.css | 2 + public/less/_repository.less | 9 +++ routers/repo/commit.go | 10 +++ templates/repo/diff/page.tmpl | 21 ++++++ 13 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 modules/git/notes.go create mode 100644 modules/git/notes_test.go create mode 100644 modules/git/tests/repos/repo1_bare/objects/28/345b214c5967bd9cdd98cc7f88f2f1ac574e02 create mode 100644 modules/git/tests/repos/repo1_bare/objects/a4/79ead1abb694ffca26f67b09c8313b12fa2a13 create mode 100644 modules/git/tests/repos/repo1_bare/objects/ca/6b5ddf303169a72d2a2971acde4f6eea194e5c create mode 100644 modules/git/tests/repos/repo1_bare/refs/notes/commits diff --git a/modules/git/notes.go b/modules/git/notes.go new file mode 100644 index 0000000000000..7aa5d89a79fd9 --- /dev/null +++ b/modules/git/notes.go @@ -0,0 +1,60 @@ +// Copyright 2019 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 ( + "io/ioutil" + + "gopkg.in/src-d/go-git.v4/plumbing" +) + +// NotesRef is the git ref where Gitea will look for git-notes data. +// The value ("refs/notes/commits") is the default ref used by git-notes. +const NotesRef = "refs/notes/commits" + +// Note stores information about a note created using git-notes. +type Note struct { + Message []byte + Commit *Commit +} + +// GetNote retrieves the git-notes data for a given commit. +func GetNote(repo *Repository, commitID string, note *Note) error { + notes, err := repo.GetCommit(NotesRef) + if err != nil { + return err + } + + entry, err := notes.GetTreeEntryByPath(commitID) + if err != nil { + return err + } + + blob := entry.Blob() + dataRc, err := blob.DataAsync() + if err != nil { + return err + } + + defer dataRc.Close() + d, err := ioutil.ReadAll(dataRc) + if err != nil { + return err + } + note.Message = d + + commit, err := repo.gogitRepo.CommitObject(plumbing.Hash(notes.ID)) + if err != nil { + return err + } + + lastCommits, err := getLastCommitForPaths(commit, "", []string{commitID}) + if err != nil { + return err + } + note.Commit = convertCommit(lastCommits[commitID]) + + return nil +} diff --git a/modules/git/notes_test.go b/modules/git/notes_test.go new file mode 100644 index 0000000000000..a954377f543d3 --- /dev/null +++ b/modules/git/notes_test.go @@ -0,0 +1,24 @@ +// Copyright 2019 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 ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetNotes(t *testing.T) { + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + bareRepo1, err := OpenRepository(bareRepo1Path) + assert.NoError(t, err) + + note := Note{} + err = GetNote(bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", ¬e) + assert.NoError(t, err) + assert.Equal(t, []byte("Note contents\n"), note.Message) + assert.Equal(t, "Vladimir Panteleev", note.Commit.Author.Name) +} diff --git a/modules/git/repo_ref_test.go b/modules/git/repo_ref_test.go index 2a3ea26a768e8..d32b34994c391 100644 --- a/modules/git/repo_ref_test.go +++ b/modules/git/repo_ref_test.go @@ -19,13 +19,14 @@ func TestRepository_GetRefs(t *testing.T) { refs, err := bareRepo1.GetRefs() assert.NoError(t, err) - assert.Len(t, refs, 4) + assert.Len(t, refs, 5) expectedRefs := []string{ BranchPrefix + "branch1", BranchPrefix + "branch2", BranchPrefix + "master", TagPrefix + "test", + NotesRef, } for _, ref := range refs { diff --git a/modules/git/tests/repos/repo1_bare/objects/28/345b214c5967bd9cdd98cc7f88f2f1ac574e02 b/modules/git/tests/repos/repo1_bare/objects/28/345b214c5967bd9cdd98cc7f88f2f1ac574e02 new file mode 100644 index 0000000000000000000000000000000000000000..05dc4725eaa5c817fd8af3f6c047b4dc9bad3216 GIT binary patch literal 85 zcmV-b0IL6Z0V^p=O;s?nU@$Z=Ff%bxury6dGD$JEG&8YCO*Kz7HZnCdFiS}@FibQv rGE6o#Hn1==PBb$$W>`}B>f-8cQ~sY)`&P|)!q8gimzFR9EFKteb^{)W literal 0 HcmV?d00001 diff --git a/modules/git/tests/repos/repo1_bare/objects/a4/79ead1abb694ffca26f67b09c8313b12fa2a13 b/modules/git/tests/repos/repo1_bare/objects/a4/79ead1abb694ffca26f67b09c8313b12fa2a13 new file mode 100644 index 0000000000000000000000000000000000000000..35d27dcbe7a6557b8143cd5f57ee6851674b3743 GIT binary patch literal 30 mcmb= 1 diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f08bc6b5a5cf0..d2079415d06c2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1314,6 +1314,7 @@ settings.unarchive.error = An error occured while trying to un-archive the repo. diff.browse_source = Browse Source diff.parent = parent diff.commit = commit +diff.git-notes = Notes diff.data_not_available = Diff Content Not Available diff.show_diff_stats = Show Diff Stats diff.show_split_view = Split View diff --git a/public/css/index.css b/public/css/index.css index fa449ec69f8df..8cea4e2c1d31a 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -803,6 +803,8 @@ footer .ui.left,footer .ui.right{line-height:40px} .stats-table .table-cell.tiny{height:.5em} tbody.commit-list{vertical-align:baseline} .commit-body{white-space:pre-wrap} +.git-notes.top{text-align:left} +.git-notes .commit-body{margin:0} @media only screen and (max-width:767px){.ui.stackable.menu.mobile--margin-between-items>.item{margin-top:5px;margin-bottom:5px} .ui.stackable.menu.mobile--no-negative-margins{margin-left:0;margin-right:0} } diff --git a/public/less/_repository.less b/public/less/_repository.less index 5970b366e2c85..9956bbce7492f 100644 --- a/public/less/_repository.less +++ b/public/less/_repository.less @@ -2219,6 +2219,15 @@ tbody.commit-list { white-space: pre-wrap; } +.git-notes { + &.top { + text-align: left; + } + .commit-body { + margin: 0; + } +} + @media only screen and (max-width: 767px) { .ui.stackable.menu { &.mobile--margin-between-items > .item { diff --git a/routers/repo/commit.go b/routers/repo/commit.go index 2978eda6c0105..870ff568f3516 100644 --- a/routers/repo/commit.go +++ b/routers/repo/commit.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" ) const ( @@ -246,6 +247,15 @@ func Diff(ctx *context.Context) { ctx.Data["Parents"] = parents ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0 ctx.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", "commit", commitID) + + note := &git.Note{} + err = git.GetNote(ctx.Repo.GitRepo, commitID, note) + if err == nil { + ctx.Data["Note"] = string(templates.ToUTF8WithFallback(note.Message)) + ctx.Data["NoteCommit"] = note.Commit + ctx.Data["NoteAuthor"] = models.ValidateCommitWithEmail(note.Commit) + } + if commit.ParentCount() > 0 { ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", "commit", parents[0]) } diff --git a/templates/repo/diff/page.tmpl b/templates/repo/diff/page.tmpl index c8f5a3d9f0efc..c35e2a415b4b9 100644 --- a/templates/repo/diff/page.tmpl +++ b/templates/repo/diff/page.tmpl @@ -65,6 +65,27 @@
{{end}} {{end}} + {{if .Note}} +
+ + {{.i18n.Tr "repo.diff.git-notes"}}: + {{if .NoteAuthor}} + + {{if .NoteAuthor.FullName}} + {{.NoteAuthor.FullName}} + {{else}} + {{.NoteCommit.Author.Name}} + {{end}} + + {{else}} + {{.NoteCommit.Author.Name}} + {{end}} + {{TimeSince .NoteCommit.Author.When $.Lang}} +
+
+
{{RenderNote .Note $.RepoLink $.Repository.ComposeMetas}}
+
+ {{end}} {{end}} {{template "repo/diff/box" .}} From 6ff9349a0941a25d5f0832936dd501bc8842927c Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 24 May 2019 09:12:13 +0000 Subject: [PATCH 04/44] [skip ci] Updated translations via Crowdin --- options/locale/locale_cs-CZ.ini | 1 - options/locale/locale_de-DE.ini | 1 - options/locale/locale_es-ES.ini | 1 - options/locale/locale_fi-FI.ini | 1 - options/locale/locale_fr-FR.ini | 1 - options/locale/locale_id-ID.ini | 1 - options/locale/locale_it-IT.ini | 1 - options/locale/locale_ja-JP.ini | 1 - options/locale/locale_lv-LV.ini | 1 - options/locale/locale_nb-NO.ini | 1 - options/locale/locale_nl-NL.ini | 1 - options/locale/locale_pl-PL.ini | 1 - options/locale/locale_pt-BR.ini | 1 - options/locale/locale_ru-RU.ini | 1 - options/locale/locale_sv-SE.ini | 1 - options/locale/locale_uk-UA.ini | 1 - options/locale/locale_zh-CN.ini | 1 - options/locale/locale_zh-TW.ini | 1 - 18 files changed, 18 deletions(-) diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 9c9740c1e8f0f..bd503ac5615d5 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -86,7 +86,6 @@ host=Hostitel user=Uživatelské jméno password=Heslo db_name=Název databáze -db_helper=Poznámka pro uživatele MySQL: použijte InnoDB engine a "utf8_general_ci" znakovou sadu. ssl_mode=SSL path=Cesta sqlite_helper=Cesta k souboru SQLite3 databáze.
Pokud spouštíte Gitea jako službu, zadejte absolutní cestu. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 5ab5dd39cff22..4df3f16825625 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -86,7 +86,6 @@ host=Host user=Benutzername password=Passwort db_name=Datenbankname -db_helper=Hinweis für MySQL-Benutzer: Bitte verwende das InnoDB-Speichersubsystem und den Zeichensatz „utf8_general_ci“. ssl_mode=SSL path=Pfad sqlite_helper=Dateipfad zur SQLite3 Datenbank.
Gebe einen absoluten Pfad an, wenn Gitea als Service gestartet wird. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 8fadb94a59b3a..4853680492bb4 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -70,7 +70,6 @@ host=Servidor user=Nombre de usuario password=Contraseña db_name=Nombre de la base de datos -db_helper=Nota para usuarios de la base de datos MySQL: por favor use el motor InnoDB y el esquema de caracteres 'utf8_general_ci'. ssl_mode=SSL path=Ruta diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 72c20e448ec76..1537d49608008 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -70,7 +70,6 @@ host=Isäntä user=Käyttäjätunnus password=Salasana db_name=Tietokannan nimi -db_helper=Huomio MySQL-käyttäjille: käytäthän InnoDB-kantamoottoria ja 'utf8_general_ci'-merkistöä. ssl_mode=SSL path=Polku no_admin_and_disable_registration=Et voi kytkeä rekisteröintiä pois luomatta sitä ennen ylläpitotiliä. diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index a6c57d2d04614..c8dbfd9839f9b 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -86,7 +86,6 @@ host=Hôte user=Nom d'utilisateur password=Mot de passe db_name=Nom de base de données -db_helper=Note aux utilisateurs de MySQL : veuillez utiliser le moteur de stockage InnoDB et le jeu de caractères 'utf8_general_ci'. ssl_mode=SSL path=Emplacement sqlite_helper=Chemin d'accès pour la base de données SQLite3.
Entrer un chemin absolu si vous exécutez Gitea en tant que service. diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 86a1ef8bfe85e..381d838834325 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -73,7 +73,6 @@ host=Host user=Nama Pengguna password=Kata Sandi db_name=Nama Basis Data -db_helper=Catatan untuk pengguna MySQL: Gunakan mesin penyimpanan InnoDB dan karakter set 'utf8_general_ci'. ssl_mode=SSL path=Jalur no_admin_and_disable_registration=Anda tidak dapat menonaktifkan pendaftaran tanpa membuat akun admin. diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 6028cb2c09cd6..a5e74f9b58242 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -73,7 +73,6 @@ host=Host user=Nome utente password=Password db_name=Nome del database -db_helper=Nota agli utenti MySQL: si prega di utilizza l'engine InnoDB ed il carattere di tipo 'utf8_general_ci'. ssl_mode=SSL path=Percorso no_admin_and_disable_registration=Non puoi disabilitare l'auto-registrazione degli utenti senza creare un account amministratore. diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index e87b2c9231858..b58fdf3ff59d0 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -86,7 +86,6 @@ host=ホスト user=ユーザー名 password=パスワード db_name=データベース名 -db_helper=MySQLユーザーへの注意: InnoDBストレージエンジンを使用し、キャラクターセットは 'utf8_general_ci' にしてください。 ssl_mode=SSL path=パス sqlite_helper=SQLite3のデータベースファイルパス。
Giteaをサービスとして実行する場合は絶対パスを入力します。 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index bb996bddb0620..87df76d789757 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -86,7 +86,6 @@ host=Resursdators user=Lietotāja vārds password=Parole db_name=Datu bāzes nosaukums -db_helper=MySQL lietotājiem: izmantojiet InnoDB dzini ar rakstzīmju kopu 'utf8_general_ci'. ssl_mode=SSL path=Ceļš sqlite_helper=Faila ceļš SQLite3 datu bāzei.
Ievadiet absolūto ceļu, ja Gitea tiek startēts kā serviss. diff --git a/options/locale/locale_nb-NO.ini b/options/locale/locale_nb-NO.ini index 2f0d8395f2856..5e316bf786ed5 100644 --- a/options/locale/locale_nb-NO.ini +++ b/options/locale/locale_nb-NO.ini @@ -86,7 +86,6 @@ host=Tjener user=Brukernavn password=Passord db_name=Databasenavn -db_helper=Merknad til MySQL brukere: Bruk InnoDB lagringsmotoren og 'utf8_general_ci' tegnsett. ssl_mode=SSL path=Bane sqlite_helper=Filbanen for SQLite3 databasen.
angi en absolutt bane hvis du kjører Gitea som en tjeneste. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index a78ec7b73c332..7f5fd504ae7aa 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -86,7 +86,6 @@ host=Server user=Gebruikersnaam password=Wachtwoord db_name=Database naam -db_helper=Opmerking voor MySQL-gebruikers: gebruik het InnoDB opslagsysteem en de "utf8_general_ci" tekenset. ssl_mode=SSL path=Pad sqlite_helper=Bestandspad voor de SQLite3-database.
Vul een volledig pad in als je GItea als een service uitvoert. diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 84d866c844ae2..75fba79d02f38 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -84,7 +84,6 @@ host=Serwer user=Nazwa użytkownika password=Hasło db_name=Nazwa bazy danych -db_helper=Informacja dla użytkowników MySQL: używaj silnika bazy danych InnoDB oraz zestawu znaków "utf8_general_ci". ssl_mode=SSL path=Ścieżka no_admin_and_disable_registration=Nie możesz wyłączyć możliwości samodzielnej rejestracji kont użytkowników bez stworzenia konta administratora. diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 66ada006d8449..bcd7ee4dba542 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -86,7 +86,6 @@ host=Servidor user=Nome de usuário password=Senha db_name=Nome do banco de dados -db_helper=Nota para usuários do MySQL: por favor, use o mecanismo de armazenamento InnoDB e o conjunto de caracteres 'utf8_general_ci'. ssl_mode=SSL path=Caminho sqlite_helper=Caminho do arquivo do banco de dados SQLite3.
Informe um caminho absoluto se você executar o Gitea como um serviço. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index d761e93bf44ac..b1e2508db512d 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -86,7 +86,6 @@ host=Хост user=Имя пользователя password=Пароль db_name=Имя базы данных -db_helper=Примечание для пользователей MySQL: пожалуйста, используйте хранилище InnoDB и набор символов 'utf8_general_ci'. ssl_mode=SSL path=Путь sqlite_helper=Путь к файлу базы данных SQLite3.
Введите абсолютный путь, если вы запускаете Gitea как службу. diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index a32854ccb5f24..d50e8b1c3ab35 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -82,7 +82,6 @@ host=Server user=Användarnamn password=Lösenord db_name=Databasens namn -db_helper=Notis för MySQL-användare: Använd InnoDB-lagringsmotorn och teckenuppsättning 'utf8_general_ci'. ssl_mode=SSL path=Filväg sqlite_helper=Sökväg för SQLite3-databasen.
Ange en absolut sökväg om du kör Gitea som en tjänst. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 648e5f66ef00b..6c5f45c5c47ec 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -86,7 +86,6 @@ host=Хост user=Ім'я кристувача password=Пароль db_name=Ім'я бази даних -db_helper=Примітка для користувачів MySQL: будь ласка, використовуйте InnoDB механізм зберігання і набір символів 'utf8_general_ci'. ssl_mode=SSL path=Шлях sqlite_helper=Шлях до файлу для бази даних SQLite3.
Введіть абсолютний шлях, якщо ви запускаєте Gіtea як сервіс. diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 92cac39779a67..5ac415460d721 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -86,7 +86,6 @@ host=数据库主机 user=用户名 password=数据库用户密码 db_name=数据库名称 -db_helper=MySQL 用户注意: 请使用 InnoDB 存储引擎和 "utf8_general_ci" 字符集。 ssl_mode=SSL path=数据库文件路径 sqlite_helper=SQLite3 数据库的文件路径。
如果以服务的方式运行 Gitea,请输入绝对路径。 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 87d3edaf94987..91e42a38573f1 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -74,7 +74,6 @@ host=主機 user=使用者名稱 password=密碼 db_name=資料庫名稱 -db_helper=MySQL 使用者注意: 請使用 InnoDB 儲存引擎和 "utf8_general_ci" 字元集。 ssl_mode=SSL path=資料庫文件路徑 no_admin_and_disable_registration=您不能夠在未建立管理員使用者的情況下禁止註冊。 From 8cd4c2242c6c0b3f53d9bd28b4b4e2cebfe46a04 Mon Sep 17 00:00:00 2001 From: jpicht Date: Fri, 24 May 2019 18:40:45 +0200 Subject: [PATCH 05/44] Fix default for allowing new organization creation for new users (#7017) Fixed #6542 When creating users DefaultAllowCreateOrganization was ignored. Signed-off-by: Julian Picht * fix TestCreateUser_Issue5882 Signed-off-by: Julian Picht --- models/user.go | 3 +-- models/user_test.go | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/models/user.go b/models/user.go index 90ca189ef0e02..7c7e81830ec30 100644 --- a/models/user.go +++ b/models/user.go @@ -849,10 +849,9 @@ func CreateUser(u *User) (err error) { return err } u.HashPassword(u.Passwd) - u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization + u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation u.MaxRepoCreation = -1 u.Theme = setting.UI.DefaultTheme - u.AllowCreateOrganization = !setting.Admin.DisableRegularOrgCreation if _, err = sess.Insert(u); err != nil { return err diff --git a/models/user_test.go b/models/user_test.go index f0a8dbdd47c71..6af9752c9b21e 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -261,6 +261,8 @@ func TestCreateUser_Issue5882(t *testing.T) { {&User{Name: "GiteaBot2", Email: "GiteaBot2@gitea.io", Passwd: passwd, MustChangePassword: false}, true}, } + setting.Service.DefaultAllowCreateOrganization = true + for _, v := range tt { setting.Admin.DisableRegularOrgCreation = v.disableOrgCreation From 61f00bc238ac045332c350ae817338be161893e7 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 24 May 2019 16:44:08 +0000 Subject: [PATCH 06/44] [skip ci] Updated translations via Crowdin --- options/locale/locale_ja-JP.ini | 3 +++ options/locale/locale_pt-BR.ini | 3 +++ 2 files changed, 6 insertions(+) diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index b58fdf3ff59d0..f7c2bcd31e580 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -86,7 +86,9 @@ host=ホスト user=ユーザー名 password=パスワード db_name=データベース名 +db_helper=MySQLユーザーへの注意事項: InnoDBストレージエンジンを使用してください。 "utf8mb4"を使用する場合、InnoDBのバージョンは5.7以降にしてください。 ssl_mode=SSL +charset=文字セット path=パス sqlite_helper=SQLite3のデータベースファイルパス。
Giteaをサービスとして実行する場合は絶対パスを入力します。 err_empty_db_path=SQLite3のデータベースパスを空にすることはできません。 @@ -1311,6 +1313,7 @@ settings.unarchive.error=リポジトリのアーカイブ解除でエラーが diff.browse_source=ソースを参照 diff.parent=親 diff.commit=コミット +diff.git-notes=Notes diff.data_not_available=差分はありません diff.show_diff_stats=差分情報を表示 diff.show_split_view=分割表示 diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index bcd7ee4dba542..782d3a9bb95c7 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -86,7 +86,9 @@ host=Servidor user=Nome de usuário password=Senha db_name=Nome do banco de dados +db_helper=Informação para os usuários do MySQL: por favor use o mecanismo de armazenamento InnoDB e se você usar "utf8mb4", sua versão do InnoDB deve ser maior que 5.6. ssl_mode=SSL +charset=Charset path=Caminho sqlite_helper=Caminho do arquivo do banco de dados SQLite3.
Informe um caminho absoluto se você executar o Gitea como um serviço. err_empty_db_path=O caminho do banco de dados SQLite3 não pode ser em branco. @@ -1311,6 +1313,7 @@ settings.unarchive.error=Um erro ocorreu enquanto estava sendo desarquivado o re diff.browse_source=Ver código fonte diff.parent=pai diff.commit=commit +diff.git-notes=Notas diff.data_not_available=Conteúdo de diff não disponível diff.show_diff_stats=Mostrar estatísticas do Diff diff.show_split_view=Visão dividida From 844f9a4bd8c5c35a9dab598442b4b3d58153f026 Mon Sep 17 00:00:00 2001 From: Marat Radchenko Date: Sat, 25 May 2019 00:21:00 +0300 Subject: [PATCH 07/44] Fix /verify LFS handler expecting wrong content-type (#7015) Fixes #6960 According to [spec][1], /verify requests must have `Accept: application/vnd.git-lfs+json` Previous code works because `git-lfs` also [violates spec and doesn't send any Accept header at all][2] For other clients that DO set `Accept: application/vnd.git-lfs+json`, addition of `Accept: application/vnd.git-lfs` either forces them to violate the spec or is ignored, depending on order in what they create header list. [1]: https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md#verification [2]: https://github.com/git-lfs/git-lfs/issues/3662 --- modules/lfs/server.go | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/modules/lfs/server.go b/modules/lfs/server.go index 8ae6326842930..7e20aa851593e 100644 --- a/modules/lfs/server.go +++ b/modules/lfs/server.go @@ -22,8 +22,7 @@ import ( ) const ( - contentMediaType = "application/vnd.git-lfs" - metaMediaType = contentMediaType + "+json" + metaMediaType = "application/vnd.git-lfs+json" ) // RequestVars contain variables from the HTTP request. Variables from routing, json body decoding, and @@ -101,11 +100,10 @@ func ObjectOidHandler(ctx *context.Context) { getMetaHandler(ctx) return } - if ContentMatcher(ctx.Req) || len(ctx.Params("filename")) > 0 { - getContentHandler(ctx) - return - } - } else if ctx.Req.Method == "PUT" && ContentMatcher(ctx.Req) { + + getContentHandler(ctx) + return + } else if ctx.Req.Method == "PUT" { PutHandler(ctx) return } @@ -348,7 +346,7 @@ func VerifyHandler(ctx *context.Context) { return } - if !ContentMatcher(ctx.Req) { + if !MetaMatcher(ctx.Req) { writeStatus(ctx, 400) return } @@ -385,7 +383,6 @@ func Represent(rv *RequestVars, meta *models.LFSMetaObject, download, upload boo } header := make(map[string]string) - header["Accept"] = contentMediaType if rv.Authorization == "" { //https://github.com/github/git-lfs/issues/1088 @@ -404,20 +401,20 @@ func Represent(rv *RequestVars, meta *models.LFSMetaObject, download, upload boo if upload && !download { // Force client side verify action while gitea lacks proper server side verification - rep.Actions["verify"] = &link{Href: rv.VerifyLink(), Header: header} + verifyHeader := make(map[string]string) + for k, v := range header { + verifyHeader[k] = v + } + + // This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662 + verifyHeader["Accept"] = metaMediaType + + rep.Actions["verify"] = &link{Href: rv.VerifyLink(), Header: verifyHeader} } return rep } -// ContentMatcher provides a mux.MatcherFunc that only allows requests that contain -// an Accept header with the contentMediaType -func ContentMatcher(r macaron.Request) bool { - mediaParts := strings.Split(r.Header.Get("Accept"), ";") - mt := mediaParts[0] - return mt == contentMediaType -} - // MetaMatcher provides a mux.MatcherFunc that only allows requests that contain // an Accept header with the metaMediaType func MetaMatcher(r macaron.Request) bool { From a83c80a00032093034d20cf79116f02fd72419e0 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Sat, 25 May 2019 04:38:15 -0400 Subject: [PATCH 08/44] Disable arm7 builds (#7037) * Disable arm7 builds As arm6 works on arm7 platforms with no noticeable performance difference * 386 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 718836be65c75..cf1a6458055b3 100644 --- a/Makefile +++ b/Makefile @@ -335,7 +335,7 @@ release-linux: @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ $(GO) get -u src.techknowlogick.com/xgo; \ fi - xgo -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/*' -out gitea-$(VERSION) . + xgo -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/mips64le,linux/mips,linux/mipsle' -out gitea-$(VERSION) . ifeq ($(CI),drone) cp /build/* $(DIST)/binaries endif From 5f05aa13e00eb9f8098066c81d2cd916d91e9874 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 25 May 2019 08:41:06 +0000 Subject: [PATCH 09/44] [skip ci] Updated translations via Crowdin --- options/locale/locale_de-DE.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 4df3f16825625..2e4222c200717 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -86,7 +86,9 @@ host=Host user=Benutzername password=Passwort db_name=Datenbankname +db_helper=Hinweis für MySQL-Nutzer: Es sollte die InnoDB Storage Engine verwendet werden. Wird der "utf8mb4" Zeichensatz verwendet, so sollte die InnoDB Version neuer sein als 5.6 . ssl_mode=SSL +charset=Zeichensatz path=Pfad sqlite_helper=Dateipfad zur SQLite3 Datenbank.
Gebe einen absoluten Pfad an, wenn Gitea als Service gestartet wird. err_empty_db_path=Der SQLite3 Datenbankpfad darf nicht leer sein. @@ -595,6 +597,8 @@ form.name_pattern_not_allowed='%s' ist nicht erlaubt für Repository-Namen. need_auth=Authentifizierung zum Klonen benötigt migrate_type=Migrationstyp migrate_type_helper=Dieses Repository wird ein Mirror sein +migrate_items_wiki=Wiki +migrate_items_milestones=Meilensteine migrate_repo=Repository migrieren migrate.clone_address=Migrations- / Klon-URL migrate.clone_address_desc=Die HTTP(S)- oder „git clone“-URL eines bereits existierenden Repositorys From df2557835b2235b48d1ed979abb1a1d42607e96a Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Sat, 25 May 2019 13:46:14 +0200 Subject: [PATCH 10/44] Improve handling of non-square avatars (#7025) * Crop avatar before resizing (#1268) Signed-off-by: Rob Watson * Fix spelling error Signed-off-by: Rob Watson --- go.mod | 1 + go.sum | 2 + models/user.go | 22 +-- modules/avatar/avatar.go | 50 +++++ modules/avatar/avatar_test.go | 49 +++++ modules/avatar/testdata/avatar.jpeg | Bin 0 -> 521 bytes modules/avatar/testdata/avatar.png | Bin 0 -> 159 bytes vendor/github.com/oliamb/cutter/.gitignore | 22 +++ vendor/github.com/oliamb/cutter/.travis.yml | 6 + vendor/github.com/oliamb/cutter/LICENSE | 20 ++ vendor/github.com/oliamb/cutter/README.md | 107 +++++++++++ vendor/github.com/oliamb/cutter/cutter.go | 192 ++++++++++++++++++++ vendor/modules.txt | 2 + 13 files changed, 454 insertions(+), 19 deletions(-) create mode 100644 modules/avatar/testdata/avatar.jpeg create mode 100644 modules/avatar/testdata/avatar.png create mode 100644 vendor/github.com/oliamb/cutter/.gitignore create mode 100644 vendor/github.com/oliamb/cutter/.travis.yml create mode 100644 vendor/github.com/oliamb/cutter/LICENSE create mode 100644 vendor/github.com/oliamb/cutter/README.md create mode 100644 vendor/github.com/oliamb/cutter/cutter.go diff --git a/go.mod b/go.mod index d02765fb10f44..299a4b29f9494 100644 --- a/go.mod +++ b/go.mod @@ -90,6 +90,7 @@ require ( github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae // indirect github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 + github.com/oliamb/cutter v0.2.2 github.com/philhofer/fwd v1.0.0 // indirect github.com/pkg/errors v0.8.1 // indirect github.com/pquerna/otp v0.0.0-20160912161815-54653902c20e diff --git a/go.sum b/go.sum index 6b0a59d5b5138..94d332cbc955d 100644 --- a/go.sum +++ b/go.sum @@ -244,6 +244,8 @@ github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc h1:z1PgdCCmYYVL0BoJT github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc/go.mod h1:np1wUFZ6tyoke22qDJZY40URn9Ae51gX7ljIWXN5TJs= github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY= github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k= +github.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/models/user.go b/models/user.go index 7c7e81830ec30..f57c5a615d117 100644 --- a/models/user.go +++ b/models/user.go @@ -6,7 +6,6 @@ package models import ( - "bytes" "container/list" "crypto/md5" "crypto/sha256" @@ -14,7 +13,6 @@ import ( "encoding/hex" "errors" "fmt" - "image" // Needed for jpeg support _ "image/jpeg" @@ -39,7 +37,6 @@ import ( "github.com/go-xorm/builder" "github.com/go-xorm/core" "github.com/go-xorm/xorm" - "github.com/nfnt/resize" "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/ssh" ) @@ -457,24 +454,11 @@ func (u *User) IsPasswordSet() bool { // UploadAvatar saves custom avatar for user. // FIXME: split uploads to different subdirs in case we have massive users. func (u *User) UploadAvatar(data []byte) error { - imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data)) + m, err := avatar.Prepare(data) if err != nil { - return fmt.Errorf("DecodeConfig: %v", err) - } - if imgCfg.Width > setting.AvatarMaxWidth { - return fmt.Errorf("Image width is to large: %d > %d", imgCfg.Width, setting.AvatarMaxWidth) - } - if imgCfg.Height > setting.AvatarMaxHeight { - return fmt.Errorf("Image height is to large: %d > %d", imgCfg.Height, setting.AvatarMaxHeight) - } - - img, _, err := image.Decode(bytes.NewReader(data)) - if err != nil { - return fmt.Errorf("Decode: %v", err) + return err } - m := resize.Resize(avatar.AvatarSize, avatar.AvatarSize, img, resize.NearestNeighbor) - sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { @@ -497,7 +481,7 @@ func (u *User) UploadAvatar(data []byte) error { } defer fw.Close() - if err = png.Encode(fw, m); err != nil { + if err = png.Encode(fw, *m); err != nil { return fmt.Errorf("Encode: %v", err) } diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index f426978b3252a..cf3da6df5ed9d 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -5,13 +5,20 @@ package avatar import ( + "bytes" "fmt" "image" "image/color/palette" + // Enable PNG support: + _ "image/png" "math/rand" "time" + "code.gitea.io/gitea/modules/setting" + "github.com/issue9/identicon" + "github.com/nfnt/resize" + "github.com/oliamb/cutter" ) // AvatarSize returns avatar's size @@ -42,3 +49,46 @@ func RandomImageSize(size int, data []byte) (image.Image, error) { func RandomImage(data []byte) (image.Image, error) { return RandomImageSize(AvatarSize, data) } + +// Prepare accepts a byte slice as input, validates it contains an image of an +// acceptable format, and crops and resizes it appropriately. +func Prepare(data []byte) (*image.Image, error) { + imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("DecodeConfig: %v", err) + } + if imgCfg.Width > setting.AvatarMaxWidth { + return nil, fmt.Errorf("Image width is too large: %d > %d", imgCfg.Width, setting.AvatarMaxWidth) + } + if imgCfg.Height > setting.AvatarMaxHeight { + return nil, fmt.Errorf("Image height is too large: %d > %d", imgCfg.Height, setting.AvatarMaxHeight) + } + + img, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("Decode: %v", err) + } + + if imgCfg.Width != imgCfg.Height { + var newSize, ax, ay int + if imgCfg.Width > imgCfg.Height { + newSize = imgCfg.Height + ax = (imgCfg.Width - imgCfg.Height) / 2 + } else { + newSize = imgCfg.Width + ay = (imgCfg.Height - imgCfg.Width) / 2 + } + + img, err = cutter.Crop(img, cutter.Config{ + Width: newSize, + Height: newSize, + Anchor: image.Point{ax, ay}, + }) + if err != nil { + return nil, err + } + } + + img = resize.Resize(AvatarSize, AvatarSize, img, resize.NearestNeighbor) + return &img, nil +} diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index 9eff5bc2be947..662d50faddb99 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -5,8 +5,11 @@ package avatar import ( + "io/ioutil" "testing" + "code.gitea.io/gitea/modules/setting" + "github.com/stretchr/testify/assert" ) @@ -17,3 +20,49 @@ func Test_RandomImage(t *testing.T) { _, err = RandomImageSize(0, []byte("gogs@local")) assert.Error(t, err) } + +func Test_PrepareWithPNG(t *testing.T) { + setting.AvatarMaxWidth = 4096 + setting.AvatarMaxHeight = 4096 + + data, err := ioutil.ReadFile("testdata/avatar.png") + assert.NoError(t, err) + + imgPtr, err := Prepare(data) + assert.NoError(t, err) + + assert.Equal(t, 290, (*imgPtr).Bounds().Max.X) + assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y) +} + +func Test_PrepareWithJPEG(t *testing.T) { + setting.AvatarMaxWidth = 4096 + setting.AvatarMaxHeight = 4096 + + data, err := ioutil.ReadFile("testdata/avatar.jpeg") + assert.NoError(t, err) + + imgPtr, err := Prepare(data) + assert.NoError(t, err) + + assert.Equal(t, 290, (*imgPtr).Bounds().Max.X) + assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y) +} + +func Test_PrepareWithInvalidImage(t *testing.T) { + setting.AvatarMaxWidth = 5 + setting.AvatarMaxHeight = 5 + + _, err := Prepare([]byte{}) + assert.EqualError(t, err, "DecodeConfig: image: unknown format") +} +func Test_PrepareWithInvalidImageSize(t *testing.T) { + setting.AvatarMaxWidth = 5 + setting.AvatarMaxHeight = 5 + + data, err := ioutil.ReadFile("testdata/avatar.png") + assert.NoError(t, err) + + _, err = Prepare(data) + assert.EqualError(t, err, "Image width is too large: 10 > 5") +} diff --git a/modules/avatar/testdata/avatar.jpeg b/modules/avatar/testdata/avatar.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..892b7baf78e4f8e8066f26b9b0042bcfefab1c8a GIT binary patch literal 521 zcmb7Am$Yia{;v?mD?Qbha0-8e>%QS}i0dEq8zBLos{0d>4jt8YBB z<)iGuFa2{5BEm)<$~V@~N)02bWQ;YYs*J1akqs^c@4Ro?DK~9wz2_Onhm>;;llfwn z7Soi|@Cj-0RAy|uWX BK?nc< literal 0 HcmV?d00001 diff --git a/modules/avatar/testdata/avatar.png b/modules/avatar/testdata/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..c0f7922961601b6c812ac62b382d34574c14a4d4 GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V6Od#Ih`sfV^(nyK@)?F|CfP6vY8S|xv6<2KrRD=b5UwyNotBhd1gt5g1e`0K#E=} wJ5XHI)5S4F;&Sqz|Nrfo^9~$o5bp`eWLWx`S@8b_%bg&dp00i_>zopr04tR#0RR91 literal 0 HcmV?d00001 diff --git a/vendor/github.com/oliamb/cutter/.gitignore b/vendor/github.com/oliamb/cutter/.gitignore new file mode 100644 index 0000000000000..00268614f0456 --- /dev/null +++ b/vendor/github.com/oliamb/cutter/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/github.com/oliamb/cutter/.travis.yml b/vendor/github.com/oliamb/cutter/.travis.yml new file mode 100644 index 0000000000000..70e012b81e44d --- /dev/null +++ b/vendor/github.com/oliamb/cutter/.travis.yml @@ -0,0 +1,6 @@ +language: go + +go: + - 1.0 + - 1.1 + - tip diff --git a/vendor/github.com/oliamb/cutter/LICENSE b/vendor/github.com/oliamb/cutter/LICENSE new file mode 100644 index 0000000000000..5412782c6e9a6 --- /dev/null +++ b/vendor/github.com/oliamb/cutter/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Olivier Amblet + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/oliamb/cutter/README.md b/vendor/github.com/oliamb/cutter/README.md new file mode 100644 index 0000000000000..b54f9e3616c43 --- /dev/null +++ b/vendor/github.com/oliamb/cutter/README.md @@ -0,0 +1,107 @@ +Cutter +====== + +A Go library to crop images. + +[![Build Status](https://travis-ci.org/oliamb/cutter.png?branch=master)](https://travis-ci.org/oliamb/cutter) +[![GoDoc](https://godoc.org/github.com/oliamb/cutter?status.png)](https://godoc.org/github.com/oliamb/cutter) + +Cutter was initially developped to be able +to crop image resized using github.com/nfnt/resize. + +Usage +----- + +Read the doc on https://godoc.org/github.com/oliamb/cutter + +Import package with + +```go +import "github.com/oliamb/cutter" +``` + +Package cutter provides a function to crop image. + +By default, the original image will be cropped at the +given size from the top left corner. + +```go +croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, +}) +``` + +Most of the time, the cropped image will share some memory +with the original, so it should be used read only. You must +ask explicitely for a copy if nedded. + +```go +croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Options: cutter.Copy, +}) +``` + +It is possible to specify the top left position: + +```go +croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Anchor: image.Point{100, 100}, + Mode: cutter.TopLeft, // optional, default value +}) +``` + +The Anchor property can represents the center of the cropped image +instead of the top left corner: + +```go +croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Mode: cutter.Centered, +}) +``` + +The default crop use the specified dimension, but it is possible +to use Width and Heigth as a ratio instead. In this case, +the resulting image will be as big as possible to fit the asked ratio +from the anchor position. + +```go +croppedImg, err := cutter.Crop(baseImage, cutter.Config{ + Width: 4, + Height: 3, + Mode: cutter.Centered, + Options: cutter.Ratio&cutter.Copy, // Copy is useless here +}) +``` + +About resize +------------ +This lib only manage crop and won't resize image, but it works great in combination with [github.com/nfnt/resize](https://github.com/nfnt/resize) + +Contributing +------------ +I'd love to see your contributions to Cutter. If you'd like to hack on it: + +- fork the project, +- hack on it, +- ensure tests pass, +- make a pull request + +If you plan to modify the API, let's disscuss it first. + +Licensing +--------- +MIT License, Please see the file called LICENSE. + +Credits +------- +Test Picture: Gopher picture from Heidi Schuyt, http://www.flickr.com/photos/hschuyt/7674222278/, +© copyright Creative Commons(http://creativecommons.org/licenses/by-nc-sa/2.0/) + +Thanks to Urturn(http://www.urturn.com) for the time allocated to develop the library. diff --git a/vendor/github.com/oliamb/cutter/cutter.go b/vendor/github.com/oliamb/cutter/cutter.go new file mode 100644 index 0000000000000..29d9d2f75882a --- /dev/null +++ b/vendor/github.com/oliamb/cutter/cutter.go @@ -0,0 +1,192 @@ +/* +Package cutter provides a function to crop image. + +By default, the original image will be cropped at the +given size from the top left corner. + + croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + }) + +Most of the time, the cropped image will share some memory +with the original, so it should be used read only. You must +ask explicitely for a copy if nedded. + + croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Options: Copy, + }) + +It is possible to specify the top left position: + + croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Anchor: image.Point{100, 100}, + Mode: TopLeft, // optional, default value + }) + +The Anchor property can represents the center of the cropped image +instead of the top left corner: + + + croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Mode: Centered, + }) + +The default crop use the specified dimension, but it is possible +to use Width and Heigth as a ratio instead. In this case, +the resulting image will be as big as possible to fit the asked ratio +from the anchor position. + + croppedImg, err := cutter.Crop(baseImage, cutter.Config{ + Width: 4, + Height: 3, + Mode: Centered, + Options: Ratio, + }) +*/ +package cutter + +import ( + "image" + "image/draw" +) + +// Config is used to defined +// the way the crop should be realized. +type Config struct { + Width, Height int + Anchor image.Point // The Anchor Point in the source image + Mode AnchorMode // Which point in the resulting image the Anchor Point is referring to + Options Option +} + +// AnchorMode is an enumeration of the position an anchor can represent. +type AnchorMode int + +const ( + // TopLeft defines the Anchor Point + // as the top left of the cropped picture. + TopLeft AnchorMode = iota + // Centered defines the Anchor Point + // as the center of the cropped picture. + Centered = iota +) + +// Option flags to modify the way the crop is done. +type Option int + +const ( + // Ratio flag is use when Width and Height + // must be used to compute a ratio rather + // than absolute size in pixels. + Ratio Option = 1 << iota + // Copy flag is used to enforce the function + // to retrieve a copy of the selected pixels. + // This disable the use of SubImage method + // to compute the result. + Copy = 1 << iota +) + +// An interface that is +// image.Image + SubImage method. +type subImageSupported interface { + SubImage(r image.Rectangle) image.Image +} + +// Crop retrieves an image that is a +// cropped copy of the original img. +// +// The crop is made given the informations provided in config. +func Crop(img image.Image, c Config) (image.Image, error) { + maxBounds := c.maxBounds(img.Bounds()) + size := c.computeSize(maxBounds, image.Point{c.Width, c.Height}) + cr := c.computedCropArea(img.Bounds(), size) + cr = img.Bounds().Intersect(cr) + + if c.Options&Copy == Copy { + return cropWithCopy(img, cr) + } + if dImg, ok := img.(subImageSupported); ok { + return dImg.SubImage(cr), nil + } + return cropWithCopy(img, cr) +} + +func cropWithCopy(img image.Image, cr image.Rectangle) (image.Image, error) { + result := image.NewRGBA(cr) + draw.Draw(result, cr, img, cr.Min, draw.Src) + return result, nil +} + +func (c Config) maxBounds(bounds image.Rectangle) (r image.Rectangle) { + if c.Mode == Centered { + anchor := c.centeredMin(bounds) + w := min(anchor.X-bounds.Min.X, bounds.Max.X-anchor.X) + h := min(anchor.Y-bounds.Min.Y, bounds.Max.Y-anchor.Y) + r = image.Rect(anchor.X-w, anchor.Y-h, anchor.X+w, anchor.Y+h) + } else { + r = image.Rect(c.Anchor.X, c.Anchor.Y, bounds.Max.X, bounds.Max.Y) + } + return +} + +// computeSize retrieve the effective size of the cropped image. +// It is defined by Height, Width, and Ratio option. +func (c Config) computeSize(bounds image.Rectangle, ratio image.Point) (p image.Point) { + if c.Options&Ratio == Ratio { + // Ratio option is on, so we take the biggest size available that fit the given ratio. + if float64(ratio.X)/float64(bounds.Dx()) > float64(ratio.Y)/float64(bounds.Dy()) { + p = image.Point{bounds.Dx(), (bounds.Dx() / ratio.X) * ratio.Y} + } else { + p = image.Point{(bounds.Dy() / ratio.Y) * ratio.X, bounds.Dy()} + } + } else { + p = image.Point{ratio.X, ratio.Y} + } + return +} + +// computedCropArea retrieve the theorical crop area. +// It is defined by Height, Width, Mode and +func (c Config) computedCropArea(bounds image.Rectangle, size image.Point) (r image.Rectangle) { + min := bounds.Min + switch c.Mode { + case Centered: + rMin := c.centeredMin(bounds) + r = image.Rect(rMin.X-size.X/2, rMin.Y-size.Y/2, rMin.X-size.X/2+size.X, rMin.Y-size.Y/2+size.Y) + default: // TopLeft + rMin := image.Point{min.X + c.Anchor.X, min.Y + c.Anchor.Y} + r = image.Rect(rMin.X, rMin.Y, rMin.X+size.X, rMin.Y+size.Y) + } + return +} + +func (c *Config) centeredMin(bounds image.Rectangle) (rMin image.Point) { + if c.Anchor.X == 0 && c.Anchor.Y == 0 { + rMin = image.Point{ + X: bounds.Dx() / 2, + Y: bounds.Dy() / 2, + } + } else { + rMin = image.Point{ + X: c.Anchor.X, + Y: c.Anchor.Y, + } + } + return +} + +func min(a, b int) (r int) { + if a < b { + r = a + } else { + r = b + } + return +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 0013ea356f6dc..0085f7bbdadd9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -261,6 +261,8 @@ github.com/mschoch/smat github.com/msteinert/pam # github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 github.com/nfnt/resize +# github.com/oliamb/cutter v0.2.2 +github.com/oliamb/cutter # github.com/pelletier/go-buffruneio v0.2.0 github.com/pelletier/go-buffruneio # github.com/philhofer/fwd v1.0.0 From 0c432d26fe20d3e50da566cf75fa483376ed8bef Mon Sep 17 00:00:00 2001 From: Mario Lubenka Date: Sat, 25 May 2019 15:37:44 +0200 Subject: [PATCH 11/44] Bugfix: Align comment label and actions to the right (#7024) * Bugfix: Align comment label and actions to the right Signed-off-by: Mario Lubenka * Restores relative position * CSS autofixer --- public/css/index.css | 4 ++-- public/less/_repository.less | 4 ++-- templates/repo/diff/comments.tmpl | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/css/index.css b/public/css/index.css index 8cea4e2c1d31a..20b4836909a6b 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -531,11 +531,11 @@ footer .ui.left,footer .ui.right{line-height:40px} .repository.view.issue .comment-list .comment .actions .item.tag{margin-right:5px} .repository.view.issue .comment-list .comment .actions .item.action{margin-top:6px;margin-left:10px} .repository.view.issue .comment-list .comment .content{margin-left:4em} -.repository.view.issue .comment-list .comment .content>.header{font-weight:400;padding:auto 15px;position:relative;color:#767676;background-color:#f7f7f7;border-bottom:1px solid #eee;border-top-left-radius:3px;border-top-right-radius:3px} +.repository.view.issue .comment-list .comment .content>.header{font-weight:400;position:relative;padding:0 15px;color:#767676;background-color:#f7f7f7;border-bottom:1px solid #eee;border-top-left-radius:3px;border-top-right-radius:3px;justify-content:space-between} .repository.view.issue .comment-list .comment .content>.header:after,.repository.view.issue .comment-list .comment .content>.header:before{right:100%;top:20px;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none} .repository.view.issue .comment-list .comment .content>.header:before{border-right-color:#d3d3d4;border-width:9px;margin-top:-9px} .repository.view.issue .comment-list .comment .content>.header:after{border-right-color:#f7f7f7;border-width:8px;margin-top:-8px} -.repository.view.issue .comment-list .comment .content>.header .text{max-width:78%;padding-top:10px;padding-bottom:10px} +.repository.view.issue .comment-list .comment .content>.header .text{padding-top:10px;padding-bottom:10px} .repository.view.issue .comment-list .comment .content .markdown{font-size:14px} .repository.view.issue .comment-list .comment .content .no-content{color:#767676;font-style:italic} .repository.view.issue .comment-list .comment .content>.bottom.segment{background:#f3f4f5} diff --git a/public/less/_repository.less b/public/less/_repository.less index 9956bbce7492f..3b02b1691b54c 100644 --- a/public/less/_repository.less +++ b/public/less/_repository.less @@ -798,16 +798,16 @@ > .header { #avatar-arrow; font-weight: normal; - padding: auto 15px; position: relative; + padding: 0 15px; color: #767676; background-color: #f7f7f7; border-bottom: 1px solid #eeeeee; border-top-left-radius: 3px; border-top-right-radius: 3px; + justify-content: space-between; .text { - max-width: 78%; padding-top: 10px; padding-bottom: 10px; } diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl index 1288886a61dad..08fa1d6f52f76 100644 --- a/templates/repo/diff/comments.tmpl +++ b/templates/repo/diff/comments.tmpl @@ -8,7 +8,7 @@
{{.Poster.GetDisplayName}} {{$.root.i18n.Tr "repo.issues.commented_at" .HashTag $createdStr | Safe}} -
+
{{if and .Review}} {{if eq .Review.Type 0}}
From 355ab0c62c1d58200d68e30d112db398ea00297a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 26 May 2019 01:15:39 +0800 Subject: [PATCH 12/44] Fix wrong init dependency on markup extensions (#7038) * fix wrong init dependency on markup extensions --- cmd/web.go | 3 --- contrib/pr/checkout.go | 2 ++ modules/markup/markup.go | 8 ++++++++ routers/init.go | 2 ++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cmd/web.go b/cmd/web.go index 6da6ec942e8c8..e6d0300a15504 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -15,7 +15,6 @@ import ( "strings" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers/routes" @@ -111,8 +110,6 @@ func runWeb(ctx *cli.Context) error { routers.GlobalInit() - external.RegisterParsers() - m := routes.NewMacaron() routes.RegisterRoutes(m) diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go index 7af27c2a9e6b2..880c029510c23 100644 --- a/contrib/pr/checkout.go +++ b/contrib/pr/checkout.go @@ -20,6 +20,7 @@ import ( "strconv" "time" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers/routes" @@ -113,6 +114,7 @@ func runPR() { log.Printf("[PR] Setting up router\n") //routers.GlobalInit() external.RegisterParsers() + markup.Init() m := routes.NewMacaron() routes.RegisterRoutes(m) diff --git a/modules/markup/markup.go b/modules/markup/markup.go index 0ea4099600e17..dc43b533c022a 100644 --- a/modules/markup/markup.go +++ b/modules/markup/markup.go @@ -15,6 +15,14 @@ import ( func Init() { getIssueFullPattern() NewSanitizer() + + // since setting maybe changed extensions, this will reload all parser extensions mapping + extParsers = make(map[string]Parser) + for _, parser := range parsers { + for _, ext := range parser.Extensions() { + extParsers[strings.ToLower(ext)] = parser + } + } } // Parser defines an interface for parsering markup file to HTML diff --git a/routers/init.go b/routers/init.go index 88422cc6ede03..cfeb928819688 100644 --- a/routers/init.go +++ b/routers/init.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/mailer" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/ssh" @@ -75,6 +76,7 @@ func GlobalInit() { if setting.InstallLock { highlight.NewContext() + external.RegisterParsers() markup.Init() if err := initDBEngine(); err == nil { log.Info("ORM engine initialization successful!") From daaae2aad5b63f916826ef602c8347b6fd1e2372 Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Sat, 25 May 2019 23:18:27 +0200 Subject: [PATCH 13/44] migrations: ensure rollback on error (#7039) (#7040) --- modules/migrations/gitea.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/migrations/gitea.go b/modules/migrations/gitea.go index dcffb360e3cff..4e930fa8318ab 100644 --- a/modules/migrations/gitea.go +++ b/modules/migrations/gitea.go @@ -68,10 +68,10 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, includeWiki bool) IsPrivate: repo.IsPrivate, Wiki: includeWiki, }) + g.repo = r if err != nil { return err } - g.repo = r g.gitRepo, err = git.OpenRepository(r.RepoPath()) return err } From 5a722ae81228bd8f169fdc4f684f89578c48b185 Mon Sep 17 00:00:00 2001 From: Mura Li Date: Sun, 26 May 2019 07:13:01 +0800 Subject: [PATCH 14/44] Timeout test commands without complicated tricks (#7020) --- .drone.yml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.drone.yml b/.drone.yml index 662529bf2eb64..6d9cde5ca9539 100644 --- a/.drone.yml +++ b/.drone.yml @@ -126,9 +126,8 @@ pipeline: commands: - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash - apt-get install -y git-lfs - - (sleep 1200 && (echo 'kill -ABRT $(pidof gitea) $(pidof integrations.sqlite.test)' | sh)) & - - make test-sqlite-migration - - make test-sqlite + - timeout -s ABRT 20m make test-sqlite-migration + - timeout -s ABRT 20m make test-sqlite when: event: [ push, tag, pull_request ] @@ -158,9 +157,8 @@ pipeline: commands: - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash - apt-get install -y git-lfs - - (sleep 1200 && (echo 'kill -ABRT $(pidof gitea) $(pidof integrations.test)' | sh)) & - - make test-mysql-migration - - make test-mysql + - timeout -s ABRT 20m make test-mysql-migration + - timeout -s ABRT 20m make test-mysql when: event: [ tag ] @@ -174,9 +172,8 @@ pipeline: commands: - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash - apt-get install -y git-lfs - - (sleep 1200 && (echo 'kill -ABRT $(pidof gitea) $(pidof integrations.test)' | sh)) & - - make test-mysql8-migration - - make test-mysql8 + - timeout -s ABRT 20m make test-mysql8-migration + - timeout -s ABRT 20m make test-mysql8 when: event: [ push, tag, pull_request ] @@ -190,9 +187,8 @@ pipeline: commands: - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash - apt-get install -y git-lfs - - (sleep 1200 && (echo 'kill -ABRT $(pidof gitea) $(pidof integrations.test)' | sh)) & - - make test-pgsql-migration - - make test-pgsql + - timeout -s ABRT 20m make test-pgsql-migration + - timeout -s ABRT 20m make test-pgsql when: event: [ push, tag, pull_request ] From 6c16febe4d0e66592cdf1af0c38101f463e230b7 Mon Sep 17 00:00:00 2001 From: Tekaoh <45337851+Tekaoh@users.noreply.github.com> Date: Sun, 26 May 2019 00:23:30 -0500 Subject: [PATCH 15/44] Update config-cheat-sheet.en-us.md (#7046) --- docs/content/doc/advanced/config-cheat-sheet.en-us.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9b9578ca485dc..2b94aa8da3dcf 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -15,8 +15,8 @@ menu: # Configuration Cheat Sheet -This is a cheat sheet for the Gitea configuration file. It contains most settings -that can configured as well as their default values. +This is a cheat sheet for the Gitea configuration file. It contains most of the settings +that can be configured as well as their default values. Any changes to the Gitea configuration file should be made in `custom/conf/app.ini` or any corresponding location. When installing from a distribution, this will From 063fa9915958215fe028f1bc97afdf9f6ca2aca2 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 26 May 2019 17:50:06 +0800 Subject: [PATCH 16/44] when git version >= 2.18, git command could run with git wire protocol version 2 param if enabled (#7047) --- custom/conf/app.ini.sample | 2 + .../doc/advanced/config-cheat-sheet.en-us.md | 1 + .../doc/advanced/config-cheat-sheet.zh-cn.md | 1 + modules/setting/git.go | 37 ++++++++++++------- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 547bc9e935076..e13f5aeeda7cd 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -671,6 +671,8 @@ MAX_GIT_DIFF_FILES = 100 ; Arguments for command 'git gc', e.g. "--aggressive --auto" ; see more on http://git-scm.com/docs/git-gc/ GC_ARGS = +; If use git wire protocol version 2 when git version >= 2.18, default is true, set to false when you always want git wire protocol version 1 +EnableAutoGitWireProtocol = true ; Operation timeout in seconds [git.timeout] 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 2b94aa8da3dcf..140eb6ffb74e9 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -396,6 +396,7 @@ NB: You must `REDIRECT_MACARON_LOG` and have `DISABLE_ROUTER_LOG` set to `false` - `MAX_GIT_DIFF_LINE_CHARACTERS`: **5000**: Max character count per line highlighted in diff view. - `MAX_GIT_DIFF_FILES`: **100**: Max number of files shown in diff view. - `GC_ARGS`: **\**: Arguments for command `git gc`, e.g. `--aggressive --auto`. See more on http://git-scm.com/docs/git-gc/ +- `ENABLE_AUTO_GIT_WIRE_PROTOCOL`: **true**: If use git wire protocol version 2 when git version >= 2.18, default is true, set to false when you always want git wire protocol version 1 ## Git - Timeout settings (`git.timeout`) - `DEFAUlT`: **360**: Git operations default timeout seconds. 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 4f34e0b905632..b9a16dd844caa 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -210,6 +210,7 @@ menu: - `CLONE`: **300**: 内部仓库间克隆的超时时间,单位秒 - `PULL`: **300**: 内部仓库间拉取的超时时间,单位秒 - `GC`: **60**: git仓库GC的超时时间,单位秒 +- `ENABLE_AUTO_GIT_WIRE_PROTOCOL`: **true**: 是否根据 Git Wire Protocol协议支持情况自动切换版本,当 git 版本在 2.18 及以上时会自动切换到版本2。为 `false` 则不切换。 ## API (`api`) diff --git a/modules/setting/git.go b/modules/setting/git.go index 8625c0e780401..673bff207e539 100644 --- a/modules/setting/git.go +++ b/modules/setting/git.go @@ -16,12 +16,13 @@ import ( var ( // Git settings Git = struct { - DisableDiffHighlight bool - MaxGitDiffLines int - MaxGitDiffLineCharacters int - MaxGitDiffFiles int - GCArgs []string `delim:" "` - Timeout struct { + DisableDiffHighlight bool + MaxGitDiffLines int + MaxGitDiffLineCharacters int + MaxGitDiffFiles int + GCArgs []string `delim:" "` + EnableAutoGitWireProtocol bool + Timeout struct { Default int Migrate int Mirror int @@ -30,11 +31,12 @@ var ( GC int `ini:"GC"` } `ini:"git.timeout"` }{ - DisableDiffHighlight: false, - MaxGitDiffLines: 1000, - MaxGitDiffLineCharacters: 5000, - MaxGitDiffFiles: 100, - GCArgs: []string{}, + DisableDiffHighlight: false, + MaxGitDiffLines: 1000, + MaxGitDiffLineCharacters: 5000, + MaxGitDiffFiles: 100, + GCArgs: []string{}, + EnableAutoGitWireProtocol: true, Timeout: struct { Default int Migrate int @@ -64,10 +66,19 @@ func newGit() { log.Fatal("Error retrieving git version: %v", err) } - log.Info("Git Version: %s", binVersion) - if version.Compare(binVersion, "2.9", ">=") { // 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{}{binVersion} + // Since git wire protocol has been released from git v2.18 + if Git.EnableAutoGitWireProtocol && version.Compare(binVersion, "2.18", ">=") { + git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "protocol.version=2") + format += ", Wire Protocol %s Enabled" + args = append(args, "Version 2") // for focus color + } + + log.Info(format, args...) } From c2f3938a58aaff18b51224f05a0808d70e111d81 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 26 May 2019 21:28:33 +0800 Subject: [PATCH 17/44] fix possbile mysql invalid connnection error (#7051) --- models/models.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/models/models.go b/models/models.go index 85318af870dc3..c1d4c100d0027 100644 --- a/models/models.go +++ b/models/models.go @@ -14,6 +14,7 @@ import ( "path" "path/filepath" "strings" + "time" "code.gitea.io/gitea/modules/setting" @@ -278,6 +279,11 @@ func SetEngine() (err error) { // so use log file to instead print to stdout. x.SetLogger(NewXORMLogger(setting.LogSQL)) x.ShowSQL(setting.LogSQL) + if DbCfg.Type == "mysql" { + x.SetMaxIdleConns(0) + x.SetConnMaxLifetime(3 * time.Second) + } + return nil } From f3d87da3e24606b770c84d95afeb0789f37a89bd Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 27 May 2019 00:55:53 +0800 Subject: [PATCH 18/44] Revert "Bugfix: Align comment label and actions to the right (#7024)" (#7055) This reverts commit 0c432d26fe20d3e50da566cf75fa483376ed8bef. --- public/css/index.css | 4 ++-- public/less/_repository.less | 4 ++-- templates/repo/diff/comments.tmpl | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/css/index.css b/public/css/index.css index 20b4836909a6b..8cea4e2c1d31a 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -531,11 +531,11 @@ footer .ui.left,footer .ui.right{line-height:40px} .repository.view.issue .comment-list .comment .actions .item.tag{margin-right:5px} .repository.view.issue .comment-list .comment .actions .item.action{margin-top:6px;margin-left:10px} .repository.view.issue .comment-list .comment .content{margin-left:4em} -.repository.view.issue .comment-list .comment .content>.header{font-weight:400;position:relative;padding:0 15px;color:#767676;background-color:#f7f7f7;border-bottom:1px solid #eee;border-top-left-radius:3px;border-top-right-radius:3px;justify-content:space-between} +.repository.view.issue .comment-list .comment .content>.header{font-weight:400;padding:auto 15px;position:relative;color:#767676;background-color:#f7f7f7;border-bottom:1px solid #eee;border-top-left-radius:3px;border-top-right-radius:3px} .repository.view.issue .comment-list .comment .content>.header:after,.repository.view.issue .comment-list .comment .content>.header:before{right:100%;top:20px;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none} .repository.view.issue .comment-list .comment .content>.header:before{border-right-color:#d3d3d4;border-width:9px;margin-top:-9px} .repository.view.issue .comment-list .comment .content>.header:after{border-right-color:#f7f7f7;border-width:8px;margin-top:-8px} -.repository.view.issue .comment-list .comment .content>.header .text{padding-top:10px;padding-bottom:10px} +.repository.view.issue .comment-list .comment .content>.header .text{max-width:78%;padding-top:10px;padding-bottom:10px} .repository.view.issue .comment-list .comment .content .markdown{font-size:14px} .repository.view.issue .comment-list .comment .content .no-content{color:#767676;font-style:italic} .repository.view.issue .comment-list .comment .content>.bottom.segment{background:#f3f4f5} diff --git a/public/less/_repository.less b/public/less/_repository.less index 3b02b1691b54c..9956bbce7492f 100644 --- a/public/less/_repository.less +++ b/public/less/_repository.less @@ -798,16 +798,16 @@ > .header { #avatar-arrow; font-weight: normal; + padding: auto 15px; position: relative; - padding: 0 15px; color: #767676; background-color: #f7f7f7; border-bottom: 1px solid #eeeeee; border-top-left-radius: 3px; border-top-right-radius: 3px; - justify-content: space-between; .text { + max-width: 78%; padding-top: 10px; padding-bottom: 10px; } diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl index 08fa1d6f52f76..1288886a61dad 100644 --- a/templates/repo/diff/comments.tmpl +++ b/templates/repo/diff/comments.tmpl @@ -8,7 +8,7 @@
{{.Poster.GetDisplayName}} {{$.root.i18n.Tr "repo.issues.commented_at" .HashTag $createdStr | Safe}} -
+
{{if and .Review}} {{if eq .Review.Type 0}}
From d67fd69474d2a67e47cbc8106c15c6d217ae8a0d Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 27 May 2019 01:36:33 +0800 Subject: [PATCH 19/44] fix charset was not saved after installation finished (#7048) --- routers/install.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routers/install.go b/routers/install.go index a404e96b5111e..cc8be065a80d7 100644 --- a/routers/install.go +++ b/routers/install.go @@ -57,6 +57,7 @@ func Install(ctx *context.Context) { form.DbPasswd = models.DbCfg.Passwd form.DbName = models.DbCfg.Name form.DbPath = models.DbCfg.Path + form.Charset = models.DbCfg.Charset ctx.Data["CurDbOption"] = "MySQL" switch models.DbCfg.Type { @@ -246,6 +247,7 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) { cfg.Section("database").Key("USER").SetValue(models.DbCfg.User) cfg.Section("database").Key("PASSWD").SetValue(models.DbCfg.Passwd) cfg.Section("database").Key("SSL_MODE").SetValue(models.DbCfg.SSLMode) + cfg.Section("database").Key("CHARSET").SetValue(models.DbCfg.Charset) cfg.Section("database").Key("PATH").SetValue(models.DbCfg.Path) cfg.Section("").Key("APP_NAME").SetValue(form.AppName) From 2c412f517ae94859b1e42beb24d4bff790484f81 Mon Sep 17 00:00:00 2001 From: zeripath Date: Sun, 26 May 2019 22:49:54 +0100 Subject: [PATCH 20/44] Add FHS-compliant-script (#6923) --- contrib/fhs-compliant-script/gitea | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100755 contrib/fhs-compliant-script/gitea diff --git a/contrib/fhs-compliant-script/gitea b/contrib/fhs-compliant-script/gitea new file mode 100755 index 0000000000000..28ce651aabb95 --- /dev/null +++ b/contrib/fhs-compliant-script/gitea @@ -0,0 +1,42 @@ +#!/bin/bash + +######################################################################## +# This script some defaults for gitea to run in a FHS compliant manner # +######################################################################## + +# It assumes that you place this script as gitea in /usr/bin +# +# And place the original in /usr/lib/gitea with working files in /var/lib/gitea +# and main configuration in /etc/gitea/app.ini +GITEA="/usr/lib/gitea/gitea" +WORK_DIR="/var/lib/gitea" +APP_INI="/etc/gitea/app.ini" + +APP_INI_SET="" +for i in "$@"; do + case "$i" in + "-c") + APP_INI_SET=1 + ;; + "-c="*) + APP_INI_SET=1 + ;; + "--config") + APP_INI_SET=1 + ;; + "--config="*) + APP_INI_SET=1 + ;; + *) + ;; + esac +done + +if [ -z "$APP_INI_SET" ]; then + CONF_ARG="-c \"$APP_INI\"" +fi + +# Provide FHS compliant defaults to +GITEA_WORK_DIR="${GITEA_WORK_DIR:-$WORK_DIR}" "$GITEA" $CONF_ARG "$@" + + From cf3ffebfde3eb6d76aa898a0b55249d5c3bf649e Mon Sep 17 00:00:00 2001 From: Hui Hui <0w0@loli.pet> Date: Tue, 28 May 2019 01:00:32 +0800 Subject: [PATCH 21/44] fix issuer of OTP URI should be URI-encoded. (#6634) * fix: Issuer of OTP URI should be URI-encoded. follow this link https://github.com/google/google-authenticator/wiki/Key-Uri-Format . * filter unsafe character ':' in issuer * Use Replace rather than ReplaceAll --- routers/user/setting/security_twofa.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/routers/user/setting/security_twofa.go b/routers/user/setting/security_twofa.go index 3a590f0b08862..fca1151a04fc9 100644 --- a/routers/user/setting/security_twofa.go +++ b/routers/user/setting/security_twofa.go @@ -74,11 +74,13 @@ func twofaGenerateSecretAndQr(ctx *context.Context) bool { if uri != nil { otpKey, err = otp.NewKeyFromURL(uri.(string)) } + // Filter unsafe character ':' in issuer + issuer := strings.Replace(setting.AppName+" ("+setting.Domain+")", ":", "", -1) if otpKey == nil { err = nil // clear the error, in case the URL was invalid otpKey, err = totp.Generate(totp.GenerateOpts{ SecretSize: 40, - Issuer: setting.AppName + " (" + strings.TrimRight(setting.AppURL, "/") + ")", + Issuer: issuer, AccountName: ctx.User.Name, }) if err != nil { From 9ca7fcddbb4c9aba7a3f8e84f6c63b7504837bee Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Mon, 27 May 2019 19:43:37 +0200 Subject: [PATCH 22/44] "It's all in GitHub" isn't true anymore, update the home template (#7059) NOTE: this commit only updates the english section --- templates/home.tmpl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/home.tmpl b/templates/home.tmpl index 3ab8fc44e980c..fa48cdc1b60ba 100644 --- a/templates/home.tmpl +++ b/templates/home.tmpl @@ -382,7 +382,10 @@ Open Source

- It's all on GitHub! Join us by contributing to make this project even better. Don't be shy to be a contributor! +Go get code.gitea.io/gitea! +Join us by contributing +to make this project even better. +Don't be shy to be a contributor!

From 69d81b656978a03ff277a611f5c3d9ef1814d001 Mon Sep 17 00:00:00 2001 From: zeripath Date: Mon, 27 May 2019 22:08:38 +0100 Subject: [PATCH 23/44] Handle insecure and ports in go get (#7041) * Handle insecure and ports in go get * Fix IsExternalURL for non-standard ports --- modules/context/context.go | 10 +++++++++- modules/context/repo.go | 5 ++++- modules/util/url.go | 3 ++- modules/util/util_test.go | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/modules/context/context.go b/modules/context/context.go index c7534a16cdcd1..1699d7aeccb7d 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -257,6 +257,13 @@ func Contexter() macaron.Handler { branchName = repo.DefaultBranch } prefix := setting.AppURL + path.Join(url.PathEscape(ownerName), url.PathEscape(repoName), "src", "branch", util.PathEscapeSegments(branchName)) + + appURL, _ := url.Parse(setting.AppURL) + + insecure := "" + if appURL.Scheme == string(setting.HTTP) { + insecure = "--insecure " + } c.Header().Set("Content-Type", "text/html") c.WriteHeader(http.StatusOK) c.Write([]byte(com.Expand(` @@ -266,7 +273,7 @@ func Contexter() macaron.Handler { - go get {GoGetImport} + go get {Insecure}{GoGetImport} `, map[string]string{ @@ -274,6 +281,7 @@ func Contexter() macaron.Handler { "CloneLink": models.ComposeHTTPSCloneURL(ownerName, repoName), "GoDocDirectory": prefix + "{/dir}", "GoDocFile": prefix + "{/dir}/{file}#L{line}", + "Insecure": insecure, }))) return } diff --git a/modules/context/repo.go b/modules/context/repo.go index f9ed9327ff976..0908340879cb0 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -188,7 +188,10 @@ func RetrieveBaseRepo(ctx *Context, repo *models.Repository) { // ComposeGoGetImport returns go-get-import meta content. func ComposeGoGetImport(owner, repo string) string { - return path.Join(setting.Domain, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo)) + /// setting.AppUrl is guaranteed to be parse as url + appURL, _ := url.Parse(setting.AppURL) + + return path.Join(appURL.Host, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo)) } // EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200 diff --git a/modules/util/url.go b/modules/util/url.go index 537e4c9b527c2..263255fcd3d6d 100644 --- a/modules/util/url.go +++ b/modules/util/url.go @@ -52,7 +52,8 @@ func IsExternalURL(rawURL string) bool { if err != nil { return true } - if len(parsed.Host) != 0 && strings.Replace(parsed.Host, "www.", "", 1) != strings.Replace(setting.Domain, "www.", "", 1) { + appURL, _ := url.Parse(setting.AppURL) + if len(parsed.Host) != 0 && strings.Replace(parsed.Host, "www.", "", 1) != strings.Replace(appURL.Host, "www.", "", 1) { return true } return false diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 3a2b4b71ffa98..2475065059e88 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -46,7 +46,7 @@ func TestURLJoin(t *testing.T) { } func TestIsExternalURL(t *testing.T) { - setting.Domain = "try.gitea.io" + setting.AppURL = "https://try.gitea.io" type test struct { Expected bool RawURL string From bd9ed96da50de283485a4e274b414b7dd1d22ba6 Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 28 May 2019 07:18:40 +0100 Subject: [PATCH 24/44] Install page - Handle invalid administrator username better (#7060) * Install page - detect invalid admin username before installing * Also fix #6954 --- options/locale/locale_en-US.ini | 4 +++ routers/install.go | 48 ++++++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index d2079415d06c2..a691232cfff0b 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -94,6 +94,10 @@ sqlite_helper = File path for the SQLite3 database.
Enter an absolute path if err_empty_db_path = The SQLite3 database path cannot be empty. no_admin_and_disable_registration = You cannot disable user self-registration without creating an administrator account. err_empty_admin_password = The administrator password cannot be empty. +err_empty_admin_email = The administrator email cannot be empty. +err_admin_name_is_reserved = Administrator Username is invalid, username is reserved +err_admin_name_pattern_not_allowed = Administrator Username is invalid, username is pattern is not allowed +err_admin_name_is_invalid = Administrator Username is invalid general_title = General Settings app_name = Site Title diff --git a/routers/install.go b/routers/install.go index cc8be065a80d7..c95abebea76c2 100644 --- a/routers/install.go +++ b/routers/install.go @@ -215,18 +215,42 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) { return } - // Check admin password. - if len(form.AdminName) > 0 && len(form.AdminPasswd) == 0 { - ctx.Data["Err_Admin"] = true - ctx.Data["Err_AdminPasswd"] = true - ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_password"), tplInstall, form) - return - } - if form.AdminPasswd != form.AdminConfirmPasswd { - ctx.Data["Err_Admin"] = true - ctx.Data["Err_AdminPasswd"] = true - ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form) - return + // Check admin user creation + if len(form.AdminName) > 0 { + // Ensure AdminName is valid + if err := models.IsUsableUsername(form.AdminName); err != nil { + ctx.Data["Err_Admin"] = true + ctx.Data["Err_AdminName"] = true + if models.IsErrNameReserved(err) { + ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form) + return + } else if models.IsErrNamePatternNotAllowed(err) { + ctx.RenderWithErr(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form) + return + } + ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form) + return + } + // Check Admin email + if len(form.AdminEmail) == 0 { + ctx.Data["Err_Admin"] = true + ctx.Data["Err_AdminEmail"] = true + ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_email"), tplInstall, form) + return + } + // Check admin password. + if len(form.AdminPasswd) == 0 { + ctx.Data["Err_Admin"] = true + ctx.Data["Err_AdminPasswd"] = true + ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_password"), tplInstall, form) + return + } + if form.AdminPasswd != form.AdminConfirmPasswd { + ctx.Data["Err_Admin"] = true + ctx.Data["Err_AdminPasswd"] = true + ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form) + return + } } if form.AppURL[len(form.AppURL)-1] != '/' { From 66863ab7704c96efbbc19059d037c63e37fcd2fa Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Tue, 28 May 2019 17:41:48 +0800 Subject: [PATCH 25/44] chore: change issue mail title. (#7064) * chore: change issue mail title. Signed-off-by: Bo-Yi Wu * change to fullname method Signed-off-by: Bo-Yi Wu --- models/issue_mail.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/issue_mail.go b/models/issue_mail.go index 033c094c7562c..16f85ba37821f 100644 --- a/models/issue_mail.go +++ b/models/issue_mail.go @@ -16,7 +16,7 @@ import ( ) func (issue *Issue) mailSubject() string { - return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.Name, issue.Title, issue.Index) + return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index) } // mailIssueCommentToParticipants can be used for both new issue creation and comment. From 27b271d457f875ed76fae9660338f7df1013318a Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 28 May 2019 09:44:31 +0000 Subject: [PATCH 26/44] [skip ci] Updated translations via Crowdin --- options/locale/locale_ja-JP.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index f7c2bcd31e580..beeeb848a2169 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -94,6 +94,10 @@ sqlite_helper=SQLite3のデータベースファイルパス。
Giteaをサ err_empty_db_path=SQLite3のデータベースパスを空にすることはできません。 no_admin_and_disable_registration=管理者アカウントを作成せずに、セルフ登録を無効にすることはできません。 err_empty_admin_password=管理者パスワードは空にできません。 +err_empty_admin_email=管理者のメールアドレスは空にできません。 +err_admin_name_is_reserved=管理者のユーザー名が不正です。予約済みのユーザー名です。 +err_admin_name_pattern_not_allowed=管理者のユーザー名が不正です。使用できない形式のユーザー名です。 +err_admin_name_is_invalid=管理者のユーザー名が不正です general_title=基本設定 app_name=サイトタイトル From 31557b12744410633ceb6fc12b53fb09038cee35 Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 28 May 2019 11:32:41 +0100 Subject: [PATCH 27/44] Fix LFS Locks over SSH (#6999) * Fix LFS Locks over SSH * Mark test as skipped --- integrations/git_test.go | 100 ++++++++++++++++++++++------------ modules/lfs/locks.go | 115 ++++++++++++++++++++++++++++----------- routers/routes/routes.go | 2 +- 3 files changed, 149 insertions(+), 68 deletions(-) diff --git a/integrations/git_test.go b/integrations/git_test.go index ebbf04f9d084a..0554f9a5aead0 100644 --- a/integrations/git_test.go +++ b/integrations/git_test.go @@ -65,6 +65,10 @@ func testGit(t *testing.T, u *url.URL) { little = commitAndPush(t, littleSize, dstPath) }) t.Run("Big", func(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + return + } PrintCurrentTest(t) big = commitAndPush(t, bigSize, dstPath) }) @@ -85,10 +89,16 @@ func testGit(t *testing.T, u *url.URL) { t.Run("Little", func(t *testing.T) { PrintCurrentTest(t) littleLFS = commitAndPush(t, littleSize, dstPath) + lockFileTest(t, littleLFS, dstPath) }) t.Run("Big", func(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + return + } PrintCurrentTest(t) bigLFS = commitAndPush(t, bigSize, dstPath) + lockFileTest(t, bigLFS, dstPath) }) }) t.Run("Locks", func(t *testing.T) { @@ -105,19 +115,21 @@ func testGit(t *testing.T, u *url.URL) { resp := session.MakeRequest(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Body.Len()) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", big)) - nilResp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, nilResp.Length) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", littleLFS)) resp = session.MakeRequest(t, req, http.StatusOK) assert.NotEqual(t, littleSize, resp.Body.Len()) assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", bigLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, bigSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + if !testing.Short() { + req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", big)) + nilResp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, bigSize, nilResp.Length) + + req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", bigLFS)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEqual(t, bigSize, resp.Body.Len()) + assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + } }) t.Run("Media", func(t *testing.T) { @@ -129,17 +141,19 @@ func testGit(t *testing.T, u *url.URL) { resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Length) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", big)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Length) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", littleLFS)) resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Length) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", bigLFS)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Length) + if !testing.Short() { + req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", big)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Length) + + req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", bigLFS)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Length) + } }) }) @@ -177,6 +191,10 @@ func testGit(t *testing.T, u *url.URL) { little = commitAndPush(t, littleSize, dstPath) }) t.Run("Big", func(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + return + } PrintCurrentTest(t) big = commitAndPush(t, bigSize, dstPath) }) @@ -197,10 +215,17 @@ func testGit(t *testing.T, u *url.URL) { t.Run("Little", func(t *testing.T) { PrintCurrentTest(t) littleLFS = commitAndPush(t, littleSize, dstPath) + lockFileTest(t, littleLFS, dstPath) + }) t.Run("Big", func(t *testing.T) { + if testing.Short() { + return + } PrintCurrentTest(t) bigLFS = commitAndPush(t, bigSize, dstPath) + lockFileTest(t, bigLFS, dstPath) + }) }) t.Run("Locks", func(t *testing.T) { @@ -217,20 +242,21 @@ func testGit(t *testing.T, u *url.URL) { resp := session.MakeRequest(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Body.Len()) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", big)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Body.Len()) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", littleLFS)) resp = session.MakeRequest(t, req, http.StatusOK) assert.NotEqual(t, littleSize, resp.Body.Len()) assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", bigLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, bigSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + if !testing.Short() { + req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", big)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Body.Len()) + req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", bigLFS)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEqual(t, bigSize, resp.Body.Len()) + assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + } }) t.Run("Media", func(t *testing.T) { PrintCurrentTest(t) @@ -241,17 +267,19 @@ func testGit(t *testing.T, u *url.URL) { resp := session.MakeRequest(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Body.Len()) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", big)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Body.Len()) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", littleLFS)) resp = session.MakeRequest(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Body.Len()) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", bigLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Body.Len()) + if !testing.Short() { + req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", big)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Body.Len()) + + req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", bigLFS)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Body.Len()) + } }) }) @@ -268,15 +296,17 @@ func ensureAnonymousClone(t *testing.T, u *url.URL) { } func lockTest(t *testing.T, remote, repoPath string) { - _, err := git.NewCommand("remote").AddArguments("set-url", "origin", remote).RunInDir(repoPath) //TODO add test ssh git-lfs-creds - assert.NoError(t, err) - _, err = git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath) + lockFileTest(t, "README.md", repoPath) +} + +func lockFileTest(t *testing.T, filename, repoPath string) { + _, err := git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath) assert.NoError(t, err) - _, err = git.NewCommand("lfs").AddArguments("lock", "README.md").RunInDir(repoPath) + _, err = git.NewCommand("lfs").AddArguments("lock", filename).RunInDir(repoPath) assert.NoError(t, err) _, err = git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath) assert.NoError(t, err) - _, err = git.NewCommand("lfs").AddArguments("unlock", "README.md").RunInDir(repoPath) + _, err = git.NewCommand("lfs").AddArguments("unlock", filename).RunInDir(repoPath) assert.NoError(t, err) } diff --git a/modules/lfs/locks.go b/modules/lfs/locks.go index 525a93645f78a..b1ca2f094a25f 100644 --- a/modules/lfs/locks.go +++ b/modules/lfs/locks.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" ) @@ -44,7 +45,7 @@ func checkIsValidRequest(ctx *context.Context, post bool) bool { return true } -func handleLockListOut(ctx *context.Context, lock *models.LFSLock, err error) { +func handleLockListOut(ctx *context.Context, repo *models.Repository, lock *models.LFSLock, err error) { if err != nil { if models.IsErrLFSLockNotExist(err) { ctx.JSON(200, api.LFSLockList{ @@ -57,7 +58,7 @@ func handleLockListOut(ctx *context.Context, lock *models.LFSLock, err error) { }) return } - if ctx.Repo.Repository.ID != lock.RepoID { + if repo.ID != lock.RepoID { ctx.JSON(200, api.LFSLockList{ Locks: []*api.LFSLock{}, }) @@ -75,17 +76,21 @@ func GetListLockHandler(ctx *context.Context) { } ctx.Resp.Header().Set("Content-Type", metaMediaType) - err := models.CheckLFSAccessForRepo(ctx.User, ctx.Repo.Repository, models.AccessModeRead) + rv := unpack(ctx) + + repository, err := models.GetRepositoryByOwnerAndName(rv.User, rv.Repo) if err != nil { - if models.IsErrLFSUnauthorizedAction(err) { - ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") - ctx.JSON(401, api.LFSLockError{ - Message: "You must have pull access to list locks : " + err.Error(), - }) - return - } - ctx.JSON(500, api.LFSLockError{ - Message: "unable to list lock : " + err.Error(), + log.Debug("Could not find repository: %s/%s - %s", rv.User, rv.Repo, err) + writeStatus(ctx, 404) + return + } + repository.MustOwner() + + authenticated := authenticate(ctx, repository, rv.Authorization, false) + if !authenticated { + ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") + ctx.JSON(401, api.LFSLockError{ + Message: "You must have pull access to list locks", }) return } @@ -100,19 +105,19 @@ func GetListLockHandler(ctx *context.Context) { return } lock, err := models.GetLFSLockByID(int64(v)) - handleLockListOut(ctx, lock, err) + handleLockListOut(ctx, repository, lock, err) return } path := ctx.Query("path") if path != "" { //Case where we request a specific id - lock, err := models.GetLFSLock(ctx.Repo.Repository, path) - handleLockListOut(ctx, lock, err) + lock, err := models.GetLFSLock(repository, path) + handleLockListOut(ctx, repository, lock, err) return } //If no query params path or id - lockList, err := models.GetLFSLockByRepoID(ctx.Repo.Repository.ID) + lockList, err := models.GetLFSLockByRepoID(repository.ID) if err != nil { ctx.JSON(500, api.LFSLockError{ Message: "unable to list locks : " + err.Error(), @@ -135,16 +140,36 @@ func PostLockHandler(ctx *context.Context) { } ctx.Resp.Header().Set("Content-Type", metaMediaType) + userName := ctx.Params("username") + repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git") + authorization := ctx.Req.Header.Get("Authorization") + + repository, err := models.GetRepositoryByOwnerAndName(userName, repoName) + if err != nil { + log.Debug("Could not find repository: %s/%s - %s", userName, repoName, err) + writeStatus(ctx, 404) + return + } + repository.MustOwner() + + authenticated := authenticate(ctx, repository, authorization, true) + if !authenticated { + ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") + ctx.JSON(401, api.LFSLockError{ + Message: "You must have push access to create locks", + }) + return + } + var req api.LFSLockRequest dec := json.NewDecoder(ctx.Req.Body().ReadCloser()) - err := dec.Decode(&req) - if err != nil { + if err := dec.Decode(&req); err != nil { writeStatus(ctx, 400) return } lock, err := models.CreateLFSLock(&models.LFSLock{ - Repo: ctx.Repo.Repository, + Repo: repository, Path: req.Path, Owner: ctx.User, }) @@ -178,23 +203,29 @@ func VerifyLockHandler(ctx *context.Context) { } ctx.Resp.Header().Set("Content-Type", metaMediaType) - err := models.CheckLFSAccessForRepo(ctx.User, ctx.Repo.Repository, models.AccessModeWrite) + userName := ctx.Params("username") + repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git") + authorization := ctx.Req.Header.Get("Authorization") + + repository, err := models.GetRepositoryByOwnerAndName(userName, repoName) if err != nil { - if models.IsErrLFSUnauthorizedAction(err) { - ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") - ctx.JSON(401, api.LFSLockError{ - Message: "You must have push access to verify locks : " + err.Error(), - }) - return - } - ctx.JSON(500, api.LFSLockError{ - Message: "unable to verify lock : " + err.Error(), + log.Debug("Could not find repository: %s/%s - %s", userName, repoName, err) + writeStatus(ctx, 404) + return + } + repository.MustOwner() + + authenticated := authenticate(ctx, repository, authorization, true) + if !authenticated { + ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") + ctx.JSON(401, api.LFSLockError{ + Message: "You must have push access to verify locks", }) return } //TODO handle body json cursor and limit - lockList, err := models.GetLFSLockByRepoID(ctx.Repo.Repository.ID) + lockList, err := models.GetLFSLockByRepoID(repository.ID) if err != nil { ctx.JSON(500, api.LFSLockError{ Message: "unable to list locks : " + err.Error(), @@ -223,10 +254,30 @@ func UnLockHandler(ctx *context.Context) { } ctx.Resp.Header().Set("Content-Type", metaMediaType) + userName := ctx.Params("username") + repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git") + authorization := ctx.Req.Header.Get("Authorization") + + repository, err := models.GetRepositoryByOwnerAndName(userName, repoName) + if err != nil { + log.Debug("Could not find repository: %s/%s - %s", userName, repoName, err) + writeStatus(ctx, 404) + return + } + repository.MustOwner() + + authenticated := authenticate(ctx, repository, authorization, true) + if !authenticated { + ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs") + ctx.JSON(401, api.LFSLockError{ + Message: "You must have push access to delete locks", + }) + return + } + var req api.LFSLockDeleteRequest dec := json.NewDecoder(ctx.Req.Body().ReadCloser()) - err := dec.Decode(&req) - if err != nil { + if err := dec.Decode(&req); err != nil { writeStatus(ctx, 400) return } diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 5a5fc518b9277..d19823714b06a 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -923,7 +923,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/", lfs.PostLockHandler) m.Post("/verify", lfs.VerifyLockHandler) m.Post("/:lid/unlock", lfs.UnLockHandler) - }, context.RepoAssignment()) + }) m.Any("/*", func(ctx *context.Context) { ctx.NotFound("", nil) }) From 743697a549bda16508ab961ac79a8bc5bdca3bbd Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Tue, 28 May 2019 23:45:54 +0800 Subject: [PATCH 28/44] refactor: append, build variable and type switch (#4940) * refactor: append, build variable and type switch * fix: remove redundant space. --- integrations/links_test.go | 2 +- models/issue_assignees_test.go | 3 +-- models/migrations/v64.go | 4 ++-- models/user.go | 10 ++++------ modules/base/tool.go | 32 ++++++++++++++++---------------- modules/base/tool_test.go | 16 ++++++++-------- modules/templates/helper.go | 13 ++++++------- routers/user/auth_openid.go | 8 ++++---- 8 files changed, 42 insertions(+), 46 deletions(-) diff --git a/integrations/links_test.go b/integrations/links_test.go index 84be7e05911bf..468c8a0f21d90 100644 --- a/integrations/links_test.go +++ b/integrations/links_test.go @@ -110,7 +110,7 @@ func testLinksAsUser(userName string, t *testing.T) { reqAPI := NewRequestf(t, "GET", "/api/v1/users/%s/repos", userName) respAPI := MakeRequest(t, reqAPI, http.StatusOK) - var apiRepos []api.Repository + var apiRepos []*api.Repository DecodeJSON(t, respAPI, &apiRepos) var repoLinks = []string{ diff --git a/models/issue_assignees_test.go b/models/issue_assignees_test.go index 029c211a4b2ce..d32f41737a3e0 100644 --- a/models/issue_assignees_test.go +++ b/models/issue_assignees_test.go @@ -43,8 +43,7 @@ func TestUpdateAssignee(t *testing.T) { assert.NoError(t, err) var expectedAssignees []*User - expectedAssignees = append(expectedAssignees, user2) - expectedAssignees = append(expectedAssignees, user3) + expectedAssignees = append(expectedAssignees, user2, user3) for in, assignee := range assignees { assert.Equal(t, assignee.ID, expectedAssignees[in].ID) diff --git a/models/migrations/v64.go b/models/migrations/v64.go index 5bc7e36b516e7..e4a360f578986 100644 --- a/models/migrations/v64.go +++ b/models/migrations/v64.go @@ -83,7 +83,7 @@ func addMultipleAssignees(x *xorm.Engine) error { return err } - allIssues := []Issue{} + allIssues := []*Issue{} if err := sess.Find(&allIssues); err != nil { return err } @@ -104,7 +104,7 @@ func addMultipleAssignees(x *xorm.Engine) error { return err } - allAssignementComments := []Comment{} + allAssignementComments := []*Comment{} if err := sess.Where("type = ?", 9).Find(&allAssignementComments); err != nil { return err } diff --git a/models/user.go b/models/user.go index f57c5a615d117..9ee27ddfbd449 100644 --- a/models/user.go +++ b/models/user.go @@ -13,9 +13,7 @@ import ( "encoding/hex" "errors" "fmt" - - // Needed for jpeg support - _ "image/jpeg" + _ "image/jpeg" // Needed for jpeg support "image/png" "os" "path/filepath" @@ -1622,7 +1620,7 @@ func SyncExternalUsers() { var sshKeysNeedUpdate bool // Find all users with this login type - var users []User + var users []*User x.Where("login_type = ?", LoginLDAP). And("login_source = ?", s.ID). Find(&users) @@ -1641,7 +1639,7 @@ func SyncExternalUsers() { // Search for existing user for _, du := range users { if du.LowerName == strings.ToLower(su.Username) { - usr = &du + usr = du break } } @@ -1724,7 +1722,7 @@ func SyncExternalUsers() { log.Trace("SyncExternalUsers[%s]: Deactivating user %s", s.Name, usr.Name) usr.IsActive = false - err = UpdateUserCols(&usr, "is_active") + err = UpdateUserCols(usr, "is_active") if err != nil { log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", s.Name, usr.Name, err) } diff --git a/modules/base/tool.go b/modules/base/tool.go index 3a6e28a885e5d..dcf9155a07750 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -465,41 +465,41 @@ func Subtract(left interface{}, right interface{}) interface{} { var rleft, rright int64 var fleft, fright float64 var isInt = true - switch left := left.(type) { + switch v := left.(type) { case int: - rleft = int64(left) + rleft = int64(v) case int8: - rleft = int64(left) + rleft = int64(v) case int16: - rleft = int64(left) + rleft = int64(v) case int32: - rleft = int64(left) + rleft = int64(v) case int64: - rleft = left + rleft = v case float32: - fleft = float64(left) + fleft = float64(v) isInt = false case float64: - fleft = left + fleft = v isInt = false } - switch right := right.(type) { + switch v := right.(type) { case int: - rright = int64(right) + rright = int64(v) case int8: - rright = int64(right) + rright = int64(v) case int16: - rright = int64(right) + rright = int64(v) case int32: - rright = int64(right) + rright = int64(v) case int64: - rright = right + rright = v case float32: - fright = float64(right) + fright = float64(v) isInt = false case float64: - fright = right + fright = v isInt = false } diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index dcaf2fcbb01f3..ec9bc1eb52335 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -306,21 +306,21 @@ func TestFileSize(t *testing.T) { func TestSubtract(t *testing.T) { toFloat64 := func(n interface{}) float64 { - switch n := n.(type) { + switch v := n.(type) { case int: - return float64(n) + return float64(v) case int8: - return float64(n) + return float64(v) case int16: - return float64(n) + return float64(v) case int32: - return float64(n) + return float64(v) case int64: - return float64(n) + return float64(v) case float32: - return float64(n) + return float64(v) case float64: - return n + return v default: return 0.0 } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 098a642556863..ef4a68add0c37 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -156,8 +156,7 @@ func NewFuncMap() []template.FuncMap { var path []string index := strings.LastIndex(str, "/") if index != -1 && index != len(str) { - path = append(path, str[0:index+1]) - path = append(path, str[index+1:]) + path = append(path, str[0:index+1], str[index+1:]) } else { path = append(path, str) } @@ -330,10 +329,10 @@ func ToUTF8(content string) string { return res } -// ReplaceLeft replaces all prefixes 'old' in 's' with 'new'. -func ReplaceLeft(s, old, new string) string { - oldLen, newLen, i, n := len(old), len(new), 0, 0 - for ; i < len(s) && strings.HasPrefix(s[i:], old); n++ { +// ReplaceLeft replaces all prefixes 'oldS' in 's' with 'newS'. +func ReplaceLeft(s, oldS, newS string) string { + oldLen, newLen, i, n := len(oldS), len(newS), 0, 0 + for ; i < len(s) && strings.HasPrefix(s[i:], oldS); n++ { i += oldLen } @@ -348,7 +347,7 @@ func ReplaceLeft(s, old, new string) string { j := 0 for ; j < n*newLen; j += newLen { - copy(replacement[j:j+newLen], new) + copy(replacement[j:j+newLen], newS) } copy(replacement[j:], s[i:]) diff --git a/routers/user/auth_openid.go b/routers/user/auth_openid.go index 2612f70a67178..1351ca040b7fa 100644 --- a/routers/user/auth_openid.go +++ b/routers/user/auth_openid.go @@ -359,11 +359,11 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si } } - len := setting.MinPasswordLength - if len < 256 { - len = 256 + length := setting.MinPasswordLength + if length < 256 { + length = 256 } - password, err := generate.GetRandomString(len) + password, err := generate.GetRandomString(length) if err != nil { ctx.RenderWithErr(err.Error(), tplSignUpOID, form) return From d01e728090c72ea7f8949107958aa0aa085b6f23 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 28 May 2019 16:02:55 +0000 Subject: [PATCH 29/44] [skip ci] Updated translations via Crowdin --- options/locale/locale_de-DE.ini | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 2e4222c200717..7082e9e8a2e7e 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -94,6 +94,10 @@ sqlite_helper=Dateipfad zur SQLite3 Datenbank.
Gebe einen absoluten Pfad an, err_empty_db_path=Der SQLite3 Datenbankpfad darf nicht leer sein. no_admin_and_disable_registration=Du kannst Selbst-Registrierungen nicht deaktivieren, ohne ein Administratorkonto zu erstellen. err_empty_admin_password=Das Administrator-Passwort darf nicht leer sein. +err_empty_admin_email=Die Administrator-E-Mail darf nicht leer sein. +err_admin_name_is_reserved=Administratornutzername ist ungültig, der Nutzername ist reserviert +err_admin_name_pattern_not_allowed=Administratornutzername ist ungültig, der Nutzername entspricht einem verbotenem Muster +err_admin_name_is_invalid=Administratornutzername ist ungültig general_title=Allgemeine Einstellungen app_name=Seitentitel @@ -597,8 +601,12 @@ form.name_pattern_not_allowed='%s' ist nicht erlaubt für Repository-Namen. need_auth=Authentifizierung zum Klonen benötigt migrate_type=Migrationstyp migrate_type_helper=Dieses Repository wird ein Mirror sein +migrate_items=Migrationselemente migrate_items_wiki=Wiki migrate_items_milestones=Meilensteine +migrate_items_labels=Labels +migrate_items_issues=Issues +migrate_items_pullrequests=Pull-Requests migrate_repo=Repository migrieren migrate.clone_address=Migrations- / Klon-URL migrate.clone_address_desc=Die HTTP(S)- oder „git clone“-URL eines bereits existierenden Repositorys @@ -1303,6 +1311,7 @@ settings.unarchive.error=Beim Rückgängig machen dieses Repo-Archivs trat ein F diff.browse_source=Quellcode durchsuchen diff.parent=Ursprung diff.commit=Commit +diff.git-notes=Hinweise diff.data_not_available=Keine Diff-Daten verfügbar diff.show_diff_stats=Diff-Statistik anzeigen diff.show_split_view=Geteilte Ansicht @@ -1712,7 +1721,7 @@ config.db_path=Verzeichnis config.service_config=Service-Konfiguration config.register_email_confirm=E-Mail-Bestätigung benötigt zum Registrieren -config.disable_register=Selbstegistrierung deaktivieren +config.disable_register=Selbstregistrierung deaktivieren config.allow_only_external_registration=Registrierung nur über externe Services erlauben config.enable_openid_signup=OpenID-Selbstregistrierung aktivieren config.enable_openid_signin=OpenID-Anmeldung aktivieren From 57b2ce03d5081bdf7996879be2da300e8f4c6f58 Mon Sep 17 00:00:00 2001 From: zeripath Date: Wed, 29 May 2019 06:49:08 +0100 Subject: [PATCH 30/44] Handle early git version's lack of get-url (#7065) --- models/repo_mirror.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/models/repo_mirror.go b/models/repo_mirror.go index b58fa05dfe3b5..7579231d8cf52 100644 --- a/models/repo_mirror.go +++ b/models/repo_mirror.go @@ -20,6 +20,7 @@ import ( "github.com/Unknwon/com" "github.com/go-xorm/xorm" + "github.com/mcuadros/go-version" ) // MirrorQueue holds an UniqueQueue object of the mirror @@ -70,7 +71,17 @@ func (m *Mirror) ScheduleNextUpdate() { } func remoteAddress(repoPath string) (string, error) { - cmd := git.NewCommand("remote", "get-url", "origin") + var cmd *git.Command + binVersion, err := git.BinVersion() + if err != nil { + return "", err + } + if version.Compare(binVersion, "2.7", ">=") { + cmd = git.NewCommand("remote", "get-url", "origin") + } else { + cmd = git.NewCommand("config", "--get", "remote.origin.url") + } + result, err := cmd.RunInDir(repoPath) if err != nil { if strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { From d7494046ac8aaf3b01f0eae0039e38c6c4f9c246 Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Wed, 29 May 2019 17:16:13 +0200 Subject: [PATCH 31/44] bug fix: add single comment in split diff mode (#4745) (#7052) Signed-off-by: Rob Watson --- public/js/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/js/index.js b/public/js/index.js index 745a631435733..96a56a42415c7 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1048,6 +1048,10 @@ function initPullRequestReview() { $(this).closest('tr').removeClass('focus-lines-new focus-lines-old'); }); $('.add-code-comment').on('click', function(e) { + // https://github.com/go-gitea/gitea/issues/4745 + if ($(e.target).hasClass('btn-add-single')) { + return; + } e.preventDefault(); var isSplit = $(this).closest('.code-diff').hasClass('code-diff-split'); var side = $(this).data('side'); From 3fd18838aa5c549842e88b770b8718f693614c75 Mon Sep 17 00:00:00 2001 From: Sergey Dryabzhinsky Date: Thu, 30 May 2019 05:22:26 +0300 Subject: [PATCH 32/44] Repository avatars (#6986) * Repository avatars - first variant of code from old work for gogs - add migration 87 - add new option in app.ini - add en-US locale string - add new class in repository.less * Add changed index.css, remove unused template name * Update en-us doc about configuration options * Add comments to new functions, add new option to docker app.ini * Add comment for lint * Remove variable, not needed * Fix formatting * Update swagger api template * Check if avatar exists * Fix avatar link/path checks * Typo * TEXT column can't have a default value * Fixes: - remove old avatar file on upload - use ID in name of avatar file - users may upload same files - add simple tests * Fix fmt check * Generate PNG instead of "static" GIF * More informative comment * Fix error message * Update avatar upload checks: - add file size check - add new option - update config docs - add new string to en-us locale * Fixes: - use FileHEader field for check file size - add new test - upload big image * Fix formatting * Update comments * Update log message * Removed wrong style - not needed * Use Sync2 to migrate * Update repos list view - bigger avatar - fix html blocks alignment * A little adjust avatar size * Use small icons for explore/repo list * Use new cool avatar preparation func by @lafriks * Missing changes for new function * Remove unused import, move imports * Missed new option definition in app.ini Add file size check in user/profile avatar upload * Use smaller field length for Avatar * Use session to update repo DB data, update DeleteAvatar - use session too * Fix err variable definition * As suggested @lafriks - return as soon as possible, code readability --- custom/conf/app.ini.sample | 8 +- docker/root/etc/templates/app.ini | 1 + .../doc/advanced/config-cheat-sheet.en-us.md | 6 +- models/migrations/migrations.go | 2 + models/migrations/v87.go | 18 +++ models/repo.go | 134 ++++++++++++++++++ models/repo_test.go | 53 +++++++ modules/setting/setting.go | 24 ++-- modules/structs/repo.go | 1 + options/locale/locale_en-US.ini | 2 + public/css/index.css | 1 + public/less/_explore.less | 5 + routers/repo/setting.go | 59 ++++++++ routers/routes/routes.go | 11 ++ routers/user/setting/profile.go | 4 + templates/explore/repo_list.tmpl | 21 +-- templates/repo/header.tmpl | 4 + templates/repo/settings/options.tmpl | 16 +++ templates/swagger/v1_json.tmpl | 4 + 19 files changed, 354 insertions(+), 20 deletions(-) create mode 100644 models/migrations/v87.go diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index e13f5aeeda7cd..e8e3ffada6865 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -504,10 +504,14 @@ SESSION_LIFE_TIME = 86400 [picture] AVATAR_UPLOAD_PATH = data/avatars -; Max Width and Height of uploaded avatars. This is to limit the amount of RAM -; used when resizing the image. +REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars +; Max Width and Height of uploaded avatars. +; This is to limit the amount of RAM used when resizing the image. AVATAR_MAX_WIDTH = 4096 AVATAR_MAX_HEIGHT = 3072 +; Maximum alloved file size for uploaded avatars. +; This is to limit the amount of RAM used when resizing the image. +AVATAR_MAX_FILE_SIZE = 1048576 ; Chinese users can choose "duoshuo" ; or a custom avatar source, like: http://cn.gravatar.com/avatar/ GRAVATAR_SOURCE = gravatar diff --git a/docker/root/etc/templates/app.ini b/docker/root/etc/templates/app.ini index 589271b4a0e41..20cbb9053ce47 100644 --- a/docker/root/etc/templates/app.ini +++ b/docker/root/etc/templates/app.ini @@ -35,6 +35,7 @@ PROVIDER_CONFIG = /data/gitea/sessions [picture] AVATAR_UPLOAD_PATH = /data/gitea/avatars +REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars [attachment] PATH = /data/gitea/attachments 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 140eb6ffb74e9..052ced6e2a979 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -290,7 +290,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only. - `ENABLE_FEDERATED_AVATAR`: **false**: Enable support for federated avatars (see [http://www.libravatar.org](http://www.libravatar.org)). -- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store local and cached files. +- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. +- `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: Path to store repository avatar image files. +- `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. +- `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. +- `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): Maximum avatar image file size in bytes. ## Attachment (`attachment`) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index f3a090e41c17b..b95a74c3621db 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -227,6 +227,8 @@ var migrations = []Migration{ NewMigration("hash application token", hashAppToken), // v86 -> v87 NewMigration("add http method to webhook", addHTTPMethodToWebhook), + // v87 -> v88 + NewMigration("add avatar field to repository", addAvatarFieldToRepository), } // Migrate database to current version diff --git a/models/migrations/v87.go b/models/migrations/v87.go new file mode 100644 index 0000000000000..94711ac669035 --- /dev/null +++ b/models/migrations/v87.go @@ -0,0 +1,18 @@ +// Copyright 2019 Gitea. 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 ( + "github.com/go-xorm/xorm" +) + +func addAvatarFieldToRepository(x *xorm.Engine) error { + type Repository struct { + // ID(10-20)-md5(32) - must fit into 64 symbols + Avatar string `xorm:"VARCHAR(64)"` + } + + return x.Sync2(new(Repository)) +} diff --git a/models/repo.go b/models/repo.go index 3283223d5bbd4..b8a3714abf415 100644 --- a/models/repo.go +++ b/models/repo.go @@ -7,9 +7,14 @@ package models import ( "bytes" + "crypto/md5" "errors" "fmt" "html/template" + + // Needed for jpeg support + _ "image/jpeg" + "image/png" "io/ioutil" "net/url" "os" @@ -21,6 +26,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/modules/avatar" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -166,6 +172,9 @@ type Repository struct { CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"` Topics []string `xorm:"TEXT JSON"` + // Avatar: ID(10-20)-md5(32) - must fit into 64 symbols + Avatar string `xorm:"VARCHAR(64)"` + CreatedUnix util.TimeStamp `xorm:"INDEX created"` UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` } @@ -290,6 +299,7 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool) Created: repo.CreatedUnix.AsTime(), Updated: repo.UpdatedUnix.AsTime(), Permissions: permission, + AvatarURL: repo.AvatarLink(), } } @@ -1869,6 +1879,15 @@ func DeleteRepository(doer *User, uid, repoID int64) error { go HookQueue.Add(repo.ID) } + if len(repo.Avatar) > 0 { + avatarPath := repo.CustomAvatarPath() + if com.IsExist(avatarPath) { + if err := os.Remove(avatarPath); err != nil { + return fmt.Errorf("Failed to remove %s: %v", avatarPath, err) + } + } + } + DeleteRepoFromIndexer(repo) return nil } @@ -2452,3 +2471,118 @@ func (repo *Repository) GetUserFork(userID int64) (*Repository, error) { } return &forkedRepo, nil } + +// CustomAvatarPath returns repository custom avatar file path. +func (repo *Repository) CustomAvatarPath() string { + // Avatar empty by default + if len(repo.Avatar) <= 0 { + return "" + } + return filepath.Join(setting.RepositoryAvatarUploadPath, repo.Avatar) +} + +// RelAvatarLink returns a relative link to the user's avatar. +// The link a sub-URL to this site +// Since Gravatar support not needed here - just check for image path. +func (repo *Repository) RelAvatarLink() string { + // If no avatar - path is empty + avatarPath := repo.CustomAvatarPath() + if len(avatarPath) <= 0 { + return "" + } + if !com.IsFile(avatarPath) { + return "" + } + return setting.AppSubURL + "/repo-avatars/" + repo.Avatar +} + +// AvatarLink returns user avatar absolute link. +func (repo *Repository) AvatarLink() string { + link := repo.RelAvatarLink() + // link may be empty! + if len(link) > 0 { + if link[0] == '/' && link[1] != '/' { + return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:] + } + } + return link +} + +// UploadAvatar saves custom avatar for repository. +// FIXME: split uploads to different subdirs in case we have massive number of repos. +func (repo *Repository) UploadAvatar(data []byte) error { + m, err := avatar.Prepare(data) + if err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + oldAvatarPath := repo.CustomAvatarPath() + + // Users can upload the same image to other repo - prefix it with ID + // Then repo will be removed - only it avatar file will be removed + repo.Avatar = fmt.Sprintf("%d-%x", repo.ID, md5.Sum(data)) + if _, err := sess.ID(repo.ID).Cols("avatar").Update(repo); err != nil { + return fmt.Errorf("UploadAvatar: Update repository avatar: %v", err) + } + + if err := os.MkdirAll(setting.RepositoryAvatarUploadPath, os.ModePerm); err != nil { + return fmt.Errorf("UploadAvatar: Failed to create dir %s: %v", setting.RepositoryAvatarUploadPath, err) + } + + fw, err := os.Create(repo.CustomAvatarPath()) + if err != nil { + return fmt.Errorf("UploadAvatar: Create file: %v", err) + } + defer fw.Close() + + if err = png.Encode(fw, *m); err != nil { + return fmt.Errorf("UploadAvatar: Encode png: %v", err) + } + + if len(oldAvatarPath) > 0 && oldAvatarPath != repo.CustomAvatarPath() { + if err := os.Remove(oldAvatarPath); err != nil { + return fmt.Errorf("UploadAvatar: Failed to remove old repo avatar %s: %v", oldAvatarPath, err) + } + } + + return sess.Commit() +} + +// DeleteAvatar deletes the repos's custom avatar. +func (repo *Repository) DeleteAvatar() error { + + // Avatar not exists + if len(repo.Avatar) == 0 { + return nil + } + + avatarPath := repo.CustomAvatarPath() + log.Trace("DeleteAvatar[%d]: %s", repo.ID, avatarPath) + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + repo.Avatar = "" + if _, err := sess.ID(repo.ID).Cols("avatar").Update(repo); err != nil { + return fmt.Errorf("DeleteAvatar: Update repository avatar: %v", err) + } + + if _, err := os.Stat(avatarPath); err == nil { + if err := os.Remove(avatarPath); err != nil { + return fmt.Errorf("DeleteAvatar: Failed to remove %s: %v", avatarPath, err) + } + } else { + // // Schrodinger: file may or may not exist. See err for details. + log.Trace("DeleteAvatar[%d]: %v", err) + } + return sess.Commit() +} diff --git a/models/repo_test.go b/models/repo_test.go index eee3997868022..8411536d70e23 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -5,6 +5,11 @@ package models import ( + "bytes" + "crypto/md5" + "fmt" + "image" + "image/png" "testing" "code.gitea.io/gitea/modules/markup" @@ -158,3 +163,51 @@ func TestTransferOwnership(t *testing.T) { CheckConsistencyFor(t, &Repository{}, &User{}, &Team{}) } + +func TestUploadAvatar(t *testing.T) { + + // Generate image + myImage := image.NewRGBA(image.Rect(0, 0, 1, 1)) + var buff bytes.Buffer + png.Encode(&buff, myImage) + + assert.NoError(t, PrepareTestDatabase()) + repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository) + + err := repo.UploadAvatar(buff.Bytes()) + assert.NoError(t, err) + assert.Equal(t, fmt.Sprintf("%d-%x", 10, md5.Sum(buff.Bytes())), repo.Avatar) +} + +func TestUploadBigAvatar(t *testing.T) { + + // Generate BIG image + myImage := image.NewRGBA(image.Rect(0, 0, 5000, 1)) + var buff bytes.Buffer + png.Encode(&buff, myImage) + + assert.NoError(t, PrepareTestDatabase()) + repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository) + + err := repo.UploadAvatar(buff.Bytes()) + assert.Error(t, err) +} + +func TestDeleteAvatar(t *testing.T) { + + // Generate image + myImage := image.NewRGBA(image.Rect(0, 0, 1, 1)) + var buff bytes.Buffer + png.Encode(&buff, myImage) + + assert.NoError(t, PrepareTestDatabase()) + repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository) + + err := repo.UploadAvatar(buff.Bytes()) + assert.NoError(t, err) + + err = repo.DeleteAvatar() + assert.NoError(t, err) + + assert.Equal(t, "", repo.Avatar) +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index de89c67d04dd9..9e9610578823b 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -250,14 +250,16 @@ var ( } // Picture settings - AvatarUploadPath string - AvatarMaxWidth int - AvatarMaxHeight int - GravatarSource string - GravatarSourceURL *url.URL - DisableGravatar bool - EnableFederatedAvatar bool - LibravatarService *libravatar.Libravatar + AvatarUploadPath string + AvatarMaxWidth int + AvatarMaxHeight int + GravatarSource string + GravatarSourceURL *url.URL + DisableGravatar bool + EnableFederatedAvatar bool + LibravatarService *libravatar.Libravatar + AvatarMaxFileSize int64 + RepositoryAvatarUploadPath string // Log settings LogLevel string @@ -835,8 +837,14 @@ func NewContext() { if !filepath.IsAbs(AvatarUploadPath) { AvatarUploadPath = path.Join(AppWorkPath, AvatarUploadPath) } + RepositoryAvatarUploadPath = sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").MustString(path.Join(AppDataPath, "repo-avatars")) + forcePathSeparator(RepositoryAvatarUploadPath) + if !filepath.IsAbs(RepositoryAvatarUploadPath) { + RepositoryAvatarUploadPath = path.Join(AppWorkPath, RepositoryAvatarUploadPath) + } AvatarMaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) AvatarMaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) + AvatarMaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576) switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source { case "duoshuo": GravatarSource = "http://gravatar.duoshuo.com/avatar/" diff --git a/modules/structs/repo.go b/modules/structs/repo.go index b5283beeaa41b..19f5ff8afe833 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -43,6 +43,7 @@ type Repository struct { // swagger:strfmt date-time Updated time.Time `json:"updated_at"` Permissions *Permission `json:"permissions,omitempty"` + AvatarURL string `json:"avatar_url"` } // CreateRepoOption options when creating repository diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a691232cfff0b..645c9770a4882 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -389,6 +389,7 @@ choose_new_avatar = Choose new avatar update_avatar = Update Avatar delete_current_avatar = Delete Current Avatar uploaded_avatar_not_a_image = The uploaded file is not an image. +uploaded_avatar_is_too_big = The uploaded file has exceeded the maximum size. update_avatar_success = Your avatar has been updated. change_password = Update Password @@ -1314,6 +1315,7 @@ settings.unarchive.header = Un-Archive This Repo settings.unarchive.text = Un-Archiving the repo will restore its ability to recieve commits and pushes, as well as new issues and pull-requests. settings.unarchive.success = The repo was successfully un-archived. settings.unarchive.error = An error occured while trying to un-archive the repo. See the log for more details. +settings.update_avatar_success = The repository avatar has been updated. diff.browse_source = Browse Source diff.parent = parent diff --git a/public/css/index.css b/public/css/index.css index 8cea4e2c1d31a..8950cc70386a2 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -956,6 +956,7 @@ tbody.commit-list{vertical-align:baseline} .ui.repository.list .item .ui.header .metas span:not(:last-child){margin-right:5px} .ui.repository.list .item .time{font-size:12px;color:grey} .ui.repository.list .item .ui.tags{margin-bottom:1em} +.ui.repository.list .item .ui.avatar.image{width:24px;height:24px} .ui.repository.branches .time{font-size:12px;color:grey} .ui.user.list .item{padding-bottom:25px} .ui.user.list .item:not(:first-child){border-top:1px solid #eee;padding-top:25px} diff --git a/public/less/_explore.less b/public/less/_explore.less index 809a138a6ccc8..c5065a35bc240 100644 --- a/public/less/_explore.less +++ b/public/less/_explore.less @@ -53,6 +53,11 @@ .ui.tags { margin-bottom: 1em; } + + .ui.avatar.image { + width: 24px; + height: 24px; + } } } diff --git a/routers/repo/setting.go b/routers/repo/setting.go index f58601633a222..07649982d28bd 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -7,11 +7,14 @@ package repo import ( "errors" + "fmt" + "io/ioutil" "net/url" "regexp" "strings" "time" + "github.com/Unknwon/com" "mvdan.cc/xurls/v2" "code.gitea.io/gitea/models" @@ -727,3 +730,59 @@ func init() { panic(err) } } + +// UpdateAvatarSetting update repo's avatar +func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm) error { + ctxRepo := ctx.Repo.Repository + + if form.Avatar == nil { + // No avatar is uploaded and we not removing it here. + // No random avatar generated here. + // Just exit, no action. + if !com.IsFile(ctxRepo.CustomAvatarPath()) { + log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID) + } + return nil + } + + r, err := form.Avatar.Open() + if err != nil { + return fmt.Errorf("Avatar.Open: %v", err) + } + defer r.Close() + + if form.Avatar.Size > setting.AvatarMaxFileSize { + return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big")) + } + + data, err := ioutil.ReadAll(r) + if err != nil { + return fmt.Errorf("ioutil.ReadAll: %v", err) + } + if !base.IsImageFile(data) { + return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) + } + if err = ctxRepo.UploadAvatar(data); err != nil { + return fmt.Errorf("UploadAvatar: %v", err) + } + return nil +} + +// SettingsAvatar save new POSTed repository avatar +func SettingsAvatar(ctx *context.Context, form auth.AvatarForm) { + form.Source = auth.AvatarLocal + if err := UpdateAvatarSetting(ctx, form); err != nil { + ctx.Flash.Error(err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success")) + } + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} + +// SettingsDeleteAvatar delete repository avatar +func SettingsDeleteAvatar(ctx *context.Context) { + if err := ctx.Repo.Repository.DeleteAvatar(); err != nil { + ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err)) + } + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index d19823714b06a..eb5f73768e38e 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -159,6 +159,14 @@ func NewMacaron() *macaron.Macaron { ExpiresAfter: time.Hour * 6, }, )) + m.Use(public.StaticHandler( + setting.RepositoryAvatarUploadPath, + &public.Options{ + Prefix: "repo-avatars", + SkipLogging: setting.DisableRouterLog, + ExpiresAfter: time.Hour * 6, + }, + )) m.Use(templates.HTMLRenderer()) models.InitMailRender(templates.Mailer()) @@ -613,6 +621,9 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/settings", func() { m.Combo("").Get(repo.Settings). Post(bindIgnErr(auth.RepoSettingForm{}), repo.SettingsPost) + m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), repo.SettingsAvatar) + m.Post("/avatar/delete", repo.SettingsDeleteAvatar) + m.Group("/collaboration", func() { m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost) m.Post("/access_mode", repo.ChangeCollaborationAccessMode) diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go index 85c9c83fd19af..ac5c4c97fb27a 100644 --- a/routers/user/setting/profile.go +++ b/routers/user/setting/profile.go @@ -127,6 +127,10 @@ func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *mo } defer fr.Close() + if form.Avatar.Size > setting.AvatarMaxFileSize { + return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big")) + } + data, err := ioutil.ReadAll(fr) if err != nil { return fmt.Errorf("ioutil.ReadAll: %v", err) diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index b1768170010d2..34aab6477a96a 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -2,6 +2,7 @@ {{range .Repos}}
- {{if .DescriptionHTML}}

{{.DescriptionHTML}}

{{end}} - {{if .Topics }} -
- {{range .Topics}} - {{if ne . "" }}
{{.}}
{{end}} +
+ {{if .DescriptionHTML}}

{{.DescriptionHTML}}

{{end}} + {{if .Topics }} +
+ {{range .Topics}} + {{if ne . "" }}
{{.}}
{{end}} + {{end}} +
{{end}} -
- {{end}} -

{{$.i18n.Tr "org.repo_updated"}} {{TimeSinceUnix .UpdatedUnix $.i18n.Lang}}

+

{{$.i18n.Tr "org.repo_updated"}} {{TimeSinceUnix .UpdatedUnix $.i18n.Lang}}

+
{{else}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index d340575353209..f4eefd3fde1d5 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -3,7 +3,11 @@
+ +
+ +
+ {{.CsrfTokenHtml}} +
+ + +
+ +
+ + {{$.i18n.Tr "settings.delete_current_avatar"}} +
+
+
{{if .Repository.IsMirror}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index c0790ac23edff..7307d1284b50b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -9066,6 +9066,10 @@ "type": "boolean", "x-go-name": "Archived" }, + "avatar_url": { + "type": "string", + "x-go-name": "AvatarURL" + }, "clone_url": { "type": "string", "x-go-name": "CloneURL" From cdd10f145be0b5e9b94c19f1303dc01c6e9c8c29 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Thu, 30 May 2019 02:25:01 +0000 Subject: [PATCH 33/44] [skip ci] Updated translations via Crowdin --- options/locale/locale_pt-BR.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 782d3a9bb95c7..26ff625df26ce 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -94,6 +94,10 @@ sqlite_helper=Caminho do arquivo do banco de dados SQLite3.
Informe um caminh err_empty_db_path=O caminho do banco de dados SQLite3 não pode ser em branco. no_admin_and_disable_registration=Você não pode desabilitar o auto-cadastro do usuário sem criar uma conta de administrador. err_empty_admin_password=A senha do administrador não pode ser em branco. +err_empty_admin_email=O e-mail do administrador não pode ser em branco. +err_admin_name_is_reserved=Nome de usuário do administrador é inválido, nome de usuário está reservado +err_admin_name_pattern_not_allowed=Nome de usuário do administrador é inválido, nome de usuário não é permitido +err_admin_name_is_invalid=Nome de usuário do administrador inválido general_title=Configurações gerais app_name=Nome do servidor From 1831b3b57144e87ccfc4f6322eefc88a49b2300e Mon Sep 17 00:00:00 2001 From: Richard Mahn Date: Thu, 30 May 2019 11:09:05 -0400 Subject: [PATCH 34/44] Fixes #5960 - Adds API Endpoint for Repo Edit (#7006) * Feature - #5960 - API Endpoint for Repo Editing * Revert from merge * Adds integration testing * Updates to integration tests * Revert changes * Update year in file header * Misspell fix * XORM = test * XORM = test * revert XORM = file * Makes RepoUnit.ID be pk and autoincr * Fix to units * revert header * Remove print statement * Adds other responses * Improves swagger for creating repo * Fixes import order * Better Unit Type does not exist error * Adds editable repo properties to the response repo structure * Fix to api_repo_edit_test.go * Fixes repo test * Changes per review * Fixes typo and standardizes comments in the EditRepoOption struct for swagger * Fixes typo and standardizes comments in the EditRepoOption struct for swagger * Actually can unarchive through the API * Unlike delete, user doesn't have to be the owner of the org, just admin to the repo * Fix to swagger comments for field name change * Update to swagger docs * Update swagger * Changes allow_pull_requests to has_pull_requests --- integrations/api_repo_edit_test.go | 225 +++++++++++++++++ integrations/api_repo_file_delete_test.go | 2 +- models/org.go | 4 +- models/repo.go | 111 ++++++--- models/unit.go | 8 +- modules/structs/repo.go | 58 +++-- routers/api/v1/api.go | 3 +- routers/api/v1/repo/repo.go | 278 ++++++++++++++++++++++ routers/api/v1/repo/repo_test.go | 82 +++++++ routers/api/v1/swagger/options.go | 2 + templates/swagger/v1_json.tmpl | 161 +++++++++++++ 11 files changed, 868 insertions(+), 66 deletions(-) create mode 100644 integrations/api_repo_edit_test.go create mode 100644 routers/api/v1/repo/repo_test.go diff --git a/integrations/api_repo_edit_test.go b/integrations/api_repo_edit_test.go new file mode 100644 index 0000000000000..3b2c916ab0c7b --- /dev/null +++ b/integrations/api_repo_edit_test.go @@ -0,0 +1,225 @@ +// Copyright 2019 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 ( + "fmt" + "net/http" + "net/url" + "testing" + + "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +// getRepoEditOptionFromRepo gets the options for an existing repo exactly as is +func getRepoEditOptionFromRepo(repo *models.Repository) *api.EditRepoOption { + name := repo.Name + description := repo.Description + website := repo.Website + private := repo.IsPrivate + hasIssues := false + if _, err := repo.GetUnit(models.UnitTypeIssues); err == nil { + hasIssues = true + } + hasWiki := false + if _, err := repo.GetUnit(models.UnitTypeWiki); err == nil { + hasWiki = true + } + defaultBranch := repo.DefaultBranch + hasPullRequests := false + ignoreWhitespaceConflicts := false + allowMerge := false + allowRebase := false + allowRebaseMerge := false + allowSquash := false + if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil { + config := unit.PullRequestsConfig() + hasPullRequests = true + ignoreWhitespaceConflicts = config.IgnoreWhitespaceConflicts + allowMerge = config.AllowMerge + allowRebase = config.AllowRebase + allowRebaseMerge = config.AllowRebaseMerge + allowSquash = config.AllowSquash + } + archived := repo.IsArchived + return &api.EditRepoOption{ + Name: &name, + Description: &description, + Website: &website, + Private: &private, + HasIssues: &hasIssues, + HasWiki: &hasWiki, + DefaultBranch: &defaultBranch, + HasPullRequests: &hasPullRequests, + IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts, + AllowMerge: &allowMerge, + AllowRebase: &allowRebase, + AllowRebaseMerge: &allowRebaseMerge, + AllowSquash: &allowSquash, + Archived: &archived, + } +} + +// getNewRepoEditOption Gets the options to change everything about an existing repo by adding to strings or changing +// the boolean +func getNewRepoEditOption(opts *api.EditRepoOption) *api.EditRepoOption { + // Gives a new property to everything + name := *opts.Name + "renamed" + description := "new description" + website := "http://wwww.newwebsite.com" + private := !*opts.Private + hasIssues := !*opts.HasIssues + hasWiki := !*opts.HasWiki + defaultBranch := "master" + hasPullRequests := !*opts.HasPullRequests + ignoreWhitespaceConflicts := !*opts.IgnoreWhitespaceConflicts + allowMerge := !*opts.AllowMerge + allowRebase := !*opts.AllowRebase + allowRebaseMerge := !*opts.AllowRebaseMerge + allowSquash := !*opts.AllowSquash + archived := !*opts.Archived + + return &api.EditRepoOption{ + Name: &name, + Description: &description, + Website: &website, + Private: &private, + DefaultBranch: &defaultBranch, + HasIssues: &hasIssues, + HasWiki: &hasWiki, + HasPullRequests: &hasPullRequests, + IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts, + AllowMerge: &allowMerge, + AllowRebase: &allowRebase, + AllowRebaseMerge: &allowRebaseMerge, + AllowSquash: &allowSquash, + Archived: &archived, + } +} + +func TestAPIRepoEdit(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16 + user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org + user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos + repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo + repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo + repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo + + // Get user2's token + session := loginUser(t, user2.Name) + token2 := getTokenForLoggedInUser(t, session) + session = emptyTestSession(t) + // Get user4's token + session = loginUser(t, user4.Name) + token4 := getTokenForLoggedInUser(t, session) + session = emptyTestSession(t) + + // Test editing a repo1 which user2 owns, changing name and many properties + origRepoEditOption := getRepoEditOptionFromRepo(repo1) + repoEditOption := getNewRepoEditOption(origRepoEditOption) + url := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo1.Name, token2) + req := NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + resp := session.MakeRequest(t, req, http.StatusOK) + var repo api.Repository + DecodeJSON(t, resp, &repo) + assert.NotNil(t, repo) + // check response + assert.Equal(t, *repoEditOption.Name, repo.Name) + assert.Equal(t, *repoEditOption.Description, repo.Description) + assert.Equal(t, *repoEditOption.Website, repo.Website) + assert.Equal(t, *repoEditOption.Archived, repo.Archived) + // check repo1 from database + repo1edited := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) + repo1editedOption := getRepoEditOptionFromRepo(repo1edited) + assert.Equal(t, *repoEditOption.Name, *repo1editedOption.Name) + assert.Equal(t, *repoEditOption.Description, *repo1editedOption.Description) + assert.Equal(t, *repoEditOption.Website, *repo1editedOption.Website) + assert.Equal(t, *repoEditOption.Archived, *repo1editedOption.Archived) + assert.Equal(t, *repoEditOption.Private, *repo1editedOption.Private) + assert.Equal(t, *repoEditOption.HasWiki, *repo1editedOption.HasWiki) + // reset repo in db + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, *repoEditOption.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + + // Test editing a non-existing repo + name := "repodoesnotexist" + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{Name: &name}) + resp = session.MakeRequest(t, req, http.StatusNotFound) + + // Test editing repo16 by user4 who does not have write access + origRepoEditOption = getRepoEditOptionFromRepo(repo16) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token4) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + session.MakeRequest(t, req, http.StatusNotFound) + + // Tests a repo with no token given so will fail + origRepoEditOption = getRepoEditOptionFromRepo(repo16) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo16.Name) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + resp = session.MakeRequest(t, req, http.StatusNotFound) + + // Test using access token for a private repo that the user of the token owns + origRepoEditOption = getRepoEditOptionFromRepo(repo16) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + // reset repo in db + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, *repoEditOption.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + + // Test making a repo public that is private + repo16 = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) + assert.True(t, repo16.IsPrivate) + private := false + repoEditOption = &api.EditRepoOption{ + Private: &private, + } + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + repo16 = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) + assert.False(t, repo16.IsPrivate) + // Make it private again + private = true + repoEditOption.Private = &private + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + + // Test using org repo "user3/repo3" where user2 is a collaborator + origRepoEditOption = getRepoEditOptionFromRepo(repo3) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user3.Name, repo3.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + session.MakeRequest(t, req, http.StatusOK) + // reset repo in db + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user3.Name, *repoEditOption.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + + // Test using org repo "user3/repo3" with no user token + origRepoEditOption = getRepoEditOptionFromRepo(repo3) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s", user3.Name, repo3.Name) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + session.MakeRequest(t, req, http.StatusNotFound) + + // Test using repo "user2/repo1" where user4 is a NOT collaborator + origRepoEditOption = getRepoEditOptionFromRepo(repo1) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo1.Name, token4) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + session.MakeRequest(t, req, http.StatusForbidden) + }) +} diff --git a/integrations/api_repo_file_delete_test.go b/integrations/api_repo_file_delete_test.go index 57e2539e19182..e9029a669b977 100644 --- a/integrations/api_repo_file_delete_test.go +++ b/integrations/api_repo_file_delete_test.go @@ -108,7 +108,7 @@ func TestAPIDeleteFile(t *testing.T) { DecodeJSON(t, resp, &apiError) assert.Equal(t, expectedAPIError, apiError) - // Test creating a file in repo1 by user4 who does not have write access + // Test creating a file in repo16 by user4 who does not have write access fileID++ treePath = fmt.Sprintf("delete/file%d.txt", fileID) createFile(user2, repo16, treePath) diff --git a/models/org.go b/models/org.go index b7db32ef1669a..6511072e2b7b0 100644 --- a/models/org.go +++ b/models/org.go @@ -162,8 +162,8 @@ func CreateOrganization(org, owner *User) (err error) { } // insert units for team - var units = make([]TeamUnit, 0, len(allRepUnitTypes)) - for _, tp := range allRepUnitTypes { + var units = make([]TeamUnit, 0, len(AllRepoUnitTypes)) + for _, tp := range AllRepoUnitTypes { units = append(units, TeamUnit{ OrgID: org.ID, TeamID: t.ID, diff --git a/models/repo.go b/models/repo.go index b8a3714abf415..16684bdeef6dd 100644 --- a/models/repo.go +++ b/models/repo.go @@ -274,32 +274,64 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool) parent = repo.BaseRepo.innerAPIFormat(e, mode, true) } } + hasIssues := false + if _, err := repo.getUnit(e, UnitTypeIssues); err == nil { + hasIssues = true + } + hasWiki := false + if _, err := repo.getUnit(e, UnitTypeWiki); err == nil { + hasWiki = true + } + hasPullRequests := false + ignoreWhitespaceConflicts := false + allowMerge := false + allowRebase := false + allowRebaseMerge := false + allowSquash := false + if unit, err := repo.getUnit(e, UnitTypePullRequests); err == nil { + config := unit.PullRequestsConfig() + hasPullRequests = true + ignoreWhitespaceConflicts = config.IgnoreWhitespaceConflicts + allowMerge = config.AllowMerge + allowRebase = config.AllowRebase + allowRebaseMerge = config.AllowRebaseMerge + allowSquash = config.AllowSquash + } + return &api.Repository{ - ID: repo.ID, - Owner: repo.Owner.APIFormat(), - Name: repo.Name, - FullName: repo.FullName(), - Description: repo.Description, - Private: repo.IsPrivate, - Empty: repo.IsEmpty, - Archived: repo.IsArchived, - Size: int(repo.Size / 1024), - Fork: repo.IsFork, - Parent: parent, - Mirror: repo.IsMirror, - HTMLURL: repo.HTMLURL(), - SSHURL: cloneLink.SSH, - CloneURL: cloneLink.HTTPS, - Website: repo.Website, - Stars: repo.NumStars, - Forks: repo.NumForks, - Watchers: repo.NumWatches, - OpenIssues: repo.NumOpenIssues, - DefaultBranch: repo.DefaultBranch, - Created: repo.CreatedUnix.AsTime(), - Updated: repo.UpdatedUnix.AsTime(), - Permissions: permission, - AvatarURL: repo.AvatarLink(), + ID: repo.ID, + Owner: repo.Owner.APIFormat(), + Name: repo.Name, + FullName: repo.FullName(), + Description: repo.Description, + Private: repo.IsPrivate, + Empty: repo.IsEmpty, + Archived: repo.IsArchived, + Size: int(repo.Size / 1024), + Fork: repo.IsFork, + Parent: parent, + Mirror: repo.IsMirror, + HTMLURL: repo.HTMLURL(), + SSHURL: cloneLink.SSH, + CloneURL: cloneLink.HTTPS, + Website: repo.Website, + Stars: repo.NumStars, + Forks: repo.NumForks, + Watchers: repo.NumWatches, + OpenIssues: repo.NumOpenIssues, + DefaultBranch: repo.DefaultBranch, + Created: repo.CreatedUnix.AsTime(), + Updated: repo.UpdatedUnix.AsTime(), + Permissions: permission, + HasIssues: hasIssues, + HasWiki: hasWiki, + HasPullRequests: hasPullRequests, + IgnoreWhitespaceConflicts: ignoreWhitespaceConflicts, + AllowMerge: allowMerge, + AllowRebase: allowRebase, + AllowRebaseMerge: allowRebaseMerge, + AllowSquash: allowSquash, + AvatarURL: repo.AvatarLink(), } } @@ -346,10 +378,20 @@ func (repo *Repository) UnitEnabled(tp UnitType) bool { return false } -var ( - // ErrUnitNotExist organization does not exist - ErrUnitNotExist = errors.New("Unit does not exist") -) +// ErrUnitTypeNotExist represents a "UnitTypeNotExist" kind of error. +type ErrUnitTypeNotExist struct { + UT UnitType +} + +// IsErrUnitTypeNotExist checks if an error is a ErrUnitNotExist. +func IsErrUnitTypeNotExist(err error) bool { + _, ok := err.(ErrUnitTypeNotExist) + return ok +} + +func (err ErrUnitTypeNotExist) Error() string { + return fmt.Sprintf("Unit type does not exist: %s", err.UT.String()) +} // MustGetUnit always returns a RepoUnit object func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit { @@ -373,6 +415,11 @@ func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit { Type: tp, Config: new(PullRequestsConfig), } + } else if tp == UnitTypeIssues { + return &RepoUnit{ + Type: tp, + Config: new(IssuesConfig), + } } return &RepoUnit{ Type: tp, @@ -394,7 +441,7 @@ func (repo *Repository) getUnit(e Engine, tp UnitType) (*RepoUnit, error) { return unit, nil } } - return nil, ErrUnitNotExist + return nil, ErrUnitTypeNotExist{tp} } func (repo *Repository) getOwner(e Engine) (err error) { @@ -1232,8 +1279,8 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err } // insert units for repo - var units = make([]RepoUnit, 0, len(defaultRepoUnits)) - for _, tp := range defaultRepoUnits { + var units = make([]RepoUnit, 0, len(DefaultRepoUnits)) + for _, tp := range DefaultRepoUnits { if tp == UnitTypeIssues { units = append(units, RepoUnit{ RepoID: repo.ID, diff --git a/models/unit.go b/models/unit.go index 697df696bc117..9f5c8d3cbbf60 100644 --- a/models/unit.go +++ b/models/unit.go @@ -58,8 +58,8 @@ func (u UnitType) ColorFormat(s fmt.State) { } var ( - // allRepUnitTypes contains all the unit types - allRepUnitTypes = []UnitType{ + // AllRepoUnitTypes contains all the unit types + AllRepoUnitTypes = []UnitType{ UnitTypeCode, UnitTypeIssues, UnitTypePullRequests, @@ -69,8 +69,8 @@ var ( UnitTypeExternalTracker, } - // defaultRepoUnits contains the default unit types - defaultRepoUnits = []UnitType{ + // DefaultRepoUnits contains the default unit types + DefaultRepoUnits = []UnitType{ UnitTypeCode, UnitTypeIssues, UnitTypePullRequests, diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 19f5ff8afe833..b4d162b776fa9 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -41,9 +41,17 @@ type Repository struct { // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time - Updated time.Time `json:"updated_at"` - Permissions *Permission `json:"permissions,omitempty"` - AvatarURL string `json:"avatar_url"` + Updated time.Time `json:"updated_at"` + Permissions *Permission `json:"permissions,omitempty"` + HasIssues bool `json:"has_issues"` + HasWiki bool `json:"has_wiki"` + HasPullRequests bool `json:"has_pull_requests"` + IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"` + AllowMerge bool `json:"allow_merge_commits"` + AllowRebase bool `json:"allow_rebase"` + AllowRebaseMerge bool `json:"allow_rebase_explicit"` + AllowSquash bool `json:"allow_squash_merge"` + AvatarURL string `json:"avatar_url"` } // CreateRepoOption options when creating repository @@ -71,38 +79,36 @@ type CreateRepoOption struct { // EditRepoOption options when editing a repository's properties // swagger:model type EditRepoOption struct { - // Name of the repository - // - // required: true + // name of the repository // unique: true - Name *string `json:"name" binding:"Required;AlphaDashDot;MaxSize(100)"` - // A short description of the repository. + Name *string `json:"name,omitempty" binding:"OmitEmpty;AlphaDashDot;MaxSize(100);"` + // a short description of the repository. Description *string `json:"description,omitempty" binding:"MaxSize(255)"` - // A URL with more information about the repository. + // a URL with more information about the repository. Website *string `json:"website,omitempty" binding:"MaxSize(255)"` - // Either `true` to make the repository private or `false` to make it public. - // Note: You will get a 422 error if the organization restricts changing repository visibility to organization + // either `true` to make the repository private or `false` to make it public. + // Note: you will get a 422 error if the organization restricts changing repository visibility to organization // owners and a non-owner tries to change the value of private. Private *bool `json:"private,omitempty"` - // Either `true` to enable issues for this repository or `false` to disable them. - EnableIssues *bool `json:"enable_issues,omitempty"` - // Either `true` to enable the wiki for this repository or `false` to disable it. - EnableWiki *bool `json:"enable_wiki,omitempty"` - // Updates the default branch for this repository. + // either `true` to enable issues for this repository or `false` to disable them. + HasIssues *bool `json:"has_issues,omitempty"` + // either `true` to enable the wiki for this repository or `false` to disable it. + HasWiki *bool `json:"has_wiki,omitempty"` + // sets the default branch for this repository. DefaultBranch *string `json:"default_branch,omitempty"` - // Either `true` to allow pull requests, or `false` to prevent pull request. - EnablePullRequests *bool `json:"enable_pull_requests,omitempty"` - // Either `true` to ignore whitepace for conflicts, or `false` to not ignore whitespace. `enabled_pull_requests` must be `true`. - IgnoreWhitespaceConflicts *bool `json:"ignore_whitespace,omitempty"` - // Either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. `enabled_pull_requests` must be `true`. + // either `true` to allow pull requests, or `false` to prevent pull request. + HasPullRequests *bool `json:"has_pull_requests,omitempty"` + // either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace. `has_pull_requests` must be `true`. + IgnoreWhitespaceConflicts *bool `json:"ignore_whitespace_conflicts,omitempty"` + // either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. `has_pull_requests` must be `true`. AllowMerge *bool `json:"allow_merge_commits,omitempty"` - // Either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging. `enabled_pull_requests` must be `true`. + // either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging. `has_pull_requests` must be `true`. AllowRebase *bool `json:"allow_rebase,omitempty"` - // Either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits. `enabled_pull_requests` must be `true`. + // either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits. `has_pull_requests` must be `true`. AllowRebaseMerge *bool `json:"allow_rebase_explicit,omitempty"` - // Either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `enabled_pull_requests` must be `true`. - AllowSquashMerge *bool `json:"allow_squash_merge,omitempty"` - // `true` to archive this repository. Note: You cannot unarchive repositories through the API. + // either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `has_pull_requests` must be `true`. + AllowSquash *bool `json:"allow_squash_merge,omitempty"` + // set to `true` to archive this repository. Archived *bool `json:"archived,omitempty"` } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index ae64e887caabd..c1561200cdbcc 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -608,7 +608,8 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/:username/:reponame", func() { m.Combo("").Get(reqAnyRepoReader(), repo.Get). - Delete(reqToken(), reqOwner(), repo.Delete) + Delete(reqToken(), reqOwner(), repo.Delete). + Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) m.Group("/hooks", func() { m.Combo("").Get(repo.ListHooks). Post(bind(api.CreateHookOption{}), repo.CreateHook) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 62153893a6f38..f8df3e9fa12fe 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -240,6 +240,10 @@ func Create(ctx *context.APIContext, opt api.CreateRepoOption) { // responses: // "201": // "$ref": "#/responses/Repository" + // "409": + // description: The repository with the same name already exists. + // "422": + // "$ref": "#/responses/validationError" if ctx.User.IsOrganization() { // Shouldn't reach this condition, but just in case. ctx.Error(422, "", "not allowed creating repository for organization") @@ -500,6 +504,280 @@ func GetByID(ctx *context.APIContext) { ctx.JSON(200, repo.APIFormat(perm.AccessMode)) } +// Edit edit repository properties +func Edit(ctx *context.APIContext, opts api.EditRepoOption) { + // swagger:operation PATCH /repos/{owner}/{repo} repository repoEdit + // --- + // summary: Edit a repository's properties. Only fields that are set will be changed. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo to edit + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo to edit + // type: string + // required: true + // required: true + // - name: body + // in: body + // description: "Properties of a repo that you can edit" + // schema: + // "$ref": "#/definitions/EditRepoOption" + // responses: + // "200": + // "$ref": "#/responses/Repository" + // "403": + // "$ref": "#/responses/forbidden" + // "422": + // "$ref": "#/responses/validationError" + if err := updateBasicProperties(ctx, opts); err != nil { + return + } + + if err := updateRepoUnits(ctx, opts); err != nil { + return + } + + if opts.Archived != nil { + if err := updateRepoArchivedState(ctx, opts); err != nil { + return + } + } + + ctx.JSON(http.StatusOK, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode)) +} + +// updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility +func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) error { + owner := ctx.Repo.Owner + repo := ctx.Repo.Repository + + oldRepoName := repo.Name + newRepoName := repo.Name + if opts.Name != nil { + newRepoName = *opts.Name + } + // Check if repository name has been changed and not just a case change + if repo.LowerName != strings.ToLower(newRepoName) { + if err := models.ChangeRepositoryName(ctx.Repo.Owner, repo.Name, newRepoName); err != nil { + switch { + case models.IsErrRepoAlreadyExist(err): + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err) + case models.IsErrNameReserved(err): + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is reserved [name: %s]", newRepoName), err) + case models.IsErrNamePatternNotAllowed(err): + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name's pattern is not allowed [name: %s, pattern: %s]", newRepoName, err.(models.ErrNamePatternNotAllowed).Pattern), err) + default: + ctx.Error(http.StatusUnprocessableEntity, "ChangeRepositoryName", err) + } + return err + } + + err := models.NewRepoRedirect(ctx.Repo.Owner.ID, repo.ID, repo.Name, newRepoName) + if err != nil { + ctx.Error(http.StatusUnprocessableEntity, "NewRepoRedirect", err) + return err + } + + if err := models.RenameRepoAction(ctx.User, oldRepoName, repo); err != nil { + log.Error("RenameRepoAction: %v", err) + ctx.Error(http.StatusInternalServerError, "RenameRepoActions", err) + return err + } + + log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName) + } + // Update the name in the repo object for the response + repo.Name = newRepoName + repo.LowerName = strings.ToLower(newRepoName) + + if opts.Description != nil { + repo.Description = *opts.Description + } + + if opts.Website != nil { + repo.Website = *opts.Website + } + + visibilityChanged := false + if opts.Private != nil { + // Visibility of forked repository is forced sync with base repository. + if repo.IsFork { + *opts.Private = repo.BaseRepo.IsPrivate + } + + visibilityChanged = repo.IsPrivate != *opts.Private + // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public + if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.User.IsAdmin { + err := fmt.Errorf("cannot change private repository to public") + ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err) + return err + } + + repo.IsPrivate = *opts.Private + } + + if err := models.UpdateRepository(repo, visibilityChanged); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateRepository", err) + return err + } + + log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name) + return nil +} + +func unitTypeInTypes(unitType models.UnitType, unitTypes []models.UnitType) bool { + for _, tp := range unitTypes { + if unitType == tp { + return true + } + } + return false +} + +// updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings +func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { + owner := ctx.Repo.Owner + repo := ctx.Repo.Repository + + var units []models.RepoUnit + + for _, tp := range models.MustRepoUnits { + units = append(units, models.RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: new(models.UnitConfig), + }) + } + + if opts.HasIssues != nil { + if *opts.HasIssues { + // We don't currently allow setting individual issue settings through the API, + // only can enable/disable issues, so when enabling issues, + // we either get the existing config which means it was already enabled, + // or create a new config since it doesn't exist. + unit, err := repo.GetUnit(models.UnitTypeIssues) + var config *models.IssuesConfig + if err != nil { + // Unit type doesn't exist so we make a new config file with default values + config = &models.IssuesConfig{ + EnableTimetracker: true, + AllowOnlyContributorsToTrackTime: true, + EnableDependencies: true, + } + } else { + config = unit.IssuesConfig() + } + units = append(units, models.RepoUnit{ + RepoID: repo.ID, + Type: models.UnitTypeIssues, + Config: config, + }) + } + } + + if opts.HasWiki != nil { + if *opts.HasWiki { + // We don't currently allow setting individual wiki settings through the API, + // only can enable/disable the wiki, so when enabling the wiki, + // we either get the existing config which means it was already enabled, + // or create a new config since it doesn't exist. + config := &models.UnitConfig{} + units = append(units, models.RepoUnit{ + RepoID: repo.ID, + Type: models.UnitTypeWiki, + Config: config, + }) + } + } + + if opts.HasPullRequests != nil { + if *opts.HasPullRequests { + // We do allow setting individual PR settings through the API, so + // we get the config settings and then set them + // if those settings were provided in the opts. + unit, err := repo.GetUnit(models.UnitTypePullRequests) + var config *models.PullRequestsConfig + if err != nil { + // Unit type doesn't exist so we make a new config file with default values + config = &models.PullRequestsConfig{ + IgnoreWhitespaceConflicts: false, + AllowMerge: true, + AllowRebase: true, + AllowRebaseMerge: true, + AllowSquash: true, + } + } else { + config = unit.PullRequestsConfig() + } + + if opts.IgnoreWhitespaceConflicts != nil { + config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts + } + if opts.AllowMerge != nil { + config.AllowMerge = *opts.AllowMerge + } + if opts.AllowRebase != nil { + config.AllowRebase = *opts.AllowRebase + } + if opts.AllowRebaseMerge != nil { + config.AllowRebaseMerge = *opts.AllowRebaseMerge + } + if opts.AllowSquash != nil { + config.AllowSquash = *opts.AllowSquash + } + + units = append(units, models.RepoUnit{ + RepoID: repo.ID, + Type: models.UnitTypePullRequests, + Config: config, + }) + } + } + + if err := models.UpdateRepositoryUnits(repo, units); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err) + return err + } + + log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name) + return nil +} + +// updateRepoArchivedState updates repo's archive state +func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) error { + repo := ctx.Repo.Repository + // archive / un-archive + if opts.Archived != nil { + if repo.IsMirror { + err := fmt.Errorf("repo is a mirror, cannot archive/un-archive") + ctx.Error(http.StatusUnprocessableEntity, err.Error(), err) + return err + } + if *opts.Archived { + if err := repo.SetArchiveRepoState(*opts.Archived); err != nil { + log.Error("Tried to archive a repo: %s", err) + ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err) + return err + } + log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) + } else { + if err := repo.SetArchiveRepoState(*opts.Archived); err != nil { + log.Error("Tried to un-archive a repo: %s", err) + ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err) + return err + } + log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) + } + } + return nil +} + // Delete one repository func Delete(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go new file mode 100644 index 0000000000000..053134ec6199b --- /dev/null +++ b/routers/api/v1/repo/repo_test.go @@ -0,0 +1,82 @@ +// Copyright 2019 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 ( + "net/http" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestRepoEdit(t *testing.T) { + models.PrepareTestEnv(t) + + ctx := test.MockContext(t, "user2/repo1") + test.LoadRepo(t, ctx, 1) + test.LoadUser(t, ctx, 2) + ctx.Repo.Owner = ctx.User + description := "new description" + website := "http://wwww.newwebsite.com" + private := true + hasIssues := false + hasWiki := false + defaultBranch := "master" + hasPullRequests := true + ignoreWhitespaceConflicts := true + allowMerge := false + allowRebase := false + allowRebaseMerge := false + allowSquashMerge := false + archived := true + opts := api.EditRepoOption{ + Name: &ctx.Repo.Repository.Name, + Description: &description, + Website: &website, + Private: &private, + HasIssues: &hasIssues, + HasWiki: &hasWiki, + DefaultBranch: &defaultBranch, + HasPullRequests: &hasPullRequests, + IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts, + AllowMerge: &allowMerge, + AllowRebase: &allowRebase, + AllowRebaseMerge: &allowRebaseMerge, + AllowSquash: &allowSquashMerge, + Archived: &archived, + } + + Edit(&context.APIContext{Context: ctx, Org: nil}, opts) + + assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + models.AssertExistsAndLoadBean(t, &models.Repository{ + ID: 1, + }, models.Cond("name = ? AND is_archived = 1", *opts.Name)) +} + +func TestRepoEditNameChange(t *testing.T) { + models.PrepareTestEnv(t) + + ctx := test.MockContext(t, "user2/repo1") + test.LoadRepo(t, ctx, 1) + test.LoadUser(t, ctx, 2) + ctx.Repo.Owner = ctx.User + name := "newname" + opts := api.EditRepoOption{ + Name: &name, + } + + Edit(&context.APIContext{Context: ctx, Org: nil}, opts) + assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + + models.AssertExistsAndLoadBean(t, &models.Repository{ + ID: 1, + }, models.Cond("name = ?", opts.Name)) +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 2df97304aa060..c1196eeb71581 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -82,6 +82,8 @@ type swaggerParameterBodies struct { // in:body CreateRepoOption api.CreateRepoOption // in:body + EditRepoOption api.EditRepoOption + // in:body CreateForkOption api.CreateForkOption // in:body diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 7307d1284b50b..a3090d1d52eb5 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1210,6 +1210,51 @@ "$ref": "#/responses/forbidden" } } + }, + "patch": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Edit a repository's properties. Only fields that are set will be changed.", + "operationId": "repoEdit", + "parameters": [ + { + "type": "string", + "description": "owner of the repo to edit", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo to edit", + "name": "repo", + "in": "path", + "required": true + }, + { + "description": "Properties of a repo that you can edit", + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/EditRepoOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/Repository" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "422": { + "$ref": "#/responses/validationError" + } + } } }, "/repos/{owner}/{repo}/archive/{archive}": { @@ -6037,6 +6082,12 @@ "responses": { "201": { "$ref": "#/responses/Repository" + }, + "409": { + "description": "The repository with the same name already exists." + }, + "422": { + "$ref": "#/responses/validationError" } } } @@ -7738,6 +7789,84 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "EditRepoOption": { + "description": "EditRepoOption options when editing a repository's properties", + "type": "object", + "properties": { + "allow_merge_commits": { + "description": "either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. `has_pull_requests` must be `true`.", + "type": "boolean", + "x-go-name": "AllowMerge" + }, + "allow_rebase": { + "description": "either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging. `has_pull_requests` must be `true`.", + "type": "boolean", + "x-go-name": "AllowRebase" + }, + "allow_rebase_explicit": { + "description": "either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits. `has_pull_requests` must be `true`.", + "type": "boolean", + "x-go-name": "AllowRebaseMerge" + }, + "allow_squash_merge": { + "description": "either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `has_pull_requests` must be `true`.", + "type": "boolean", + "x-go-name": "AllowSquash" + }, + "archived": { + "description": "set to `true` to archive this repository.", + "type": "boolean", + "x-go-name": "Archived" + }, + "default_branch": { + "description": "sets the default branch for this repository.", + "type": "string", + "x-go-name": "DefaultBranch" + }, + "description": { + "description": "a short description of the repository.", + "type": "string", + "x-go-name": "Description" + }, + "has_issues": { + "description": "either `true` to enable issues for this repository or `false` to disable them.", + "type": "boolean", + "x-go-name": "HasIssues" + }, + "has_pull_requests": { + "description": "either `true` to allow pull requests, or `false` to prevent pull request.", + "type": "boolean", + "x-go-name": "HasPullRequests" + }, + "has_wiki": { + "description": "either `true` to enable the wiki for this repository or `false` to disable it.", + "type": "boolean", + "x-go-name": "HasWiki" + }, + "ignore_whitespace_conflicts": { + "description": "either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace. `has_pull_requests` must be `true`.", + "type": "boolean", + "x-go-name": "IgnoreWhitespaceConflicts" + }, + "name": { + "description": "name of the repository", + "type": "string", + "uniqueItems": true, + "x-go-name": "Name" + }, + "private": { + "description": "either `true` to make the repository private or `false` to make it public.\nNote: you will get a 422 error if the organization restricts changing repository visibility to organization\nowners and a non-owner tries to change the value of private.", + "type": "boolean", + "x-go-name": "Private" + }, + "website": { + "description": "a URL with more information about the repository.", + "type": "string", + "x-go-name": "Website" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "EditTeamOption": { "description": "EditTeamOption options for editing a team", "type": "object", @@ -9062,6 +9191,22 @@ "description": "Repository represents a repository", "type": "object", "properties": { + "allow_merge_commits": { + "type": "boolean", + "x-go-name": "AllowMerge" + }, + "allow_rebase": { + "type": "boolean", + "x-go-name": "AllowRebase" + }, + "allow_rebase_explicit": { + "type": "boolean", + "x-go-name": "AllowRebaseMerge" + }, + "allow_squash_merge": { + "type": "boolean", + "x-go-name": "AllowSquash" + }, "archived": { "type": "boolean", "x-go-name": "Archived" @@ -9104,6 +9249,18 @@ "type": "string", "x-go-name": "FullName" }, + "has_issues": { + "type": "boolean", + "x-go-name": "HasIssues" + }, + "has_pull_requests": { + "type": "boolean", + "x-go-name": "HasPullRequests" + }, + "has_wiki": { + "type": "boolean", + "x-go-name": "HasWiki" + }, "html_url": { "type": "string", "x-go-name": "HTMLURL" @@ -9113,6 +9270,10 @@ "format": "int64", "x-go-name": "ID" }, + "ignore_whitespace_conflicts": { + "type": "boolean", + "x-go-name": "IgnoreWhitespaceConflicts" + }, "mirror": { "type": "boolean", "x-go-name": "Mirror" From 97b6368b7d1a5709d12495ba84636c3003c5d4c3 Mon Sep 17 00:00:00 2001 From: John Olheiser <42128690+jolheiser@users.noreply.github.com> Date: Thu, 30 May 2019 11:46:31 -0500 Subject: [PATCH 35/44] 1.8.2 changelog (#7079) (#7088) * 1.8.2 changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 361ec205460b6..3b7547e1ac848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ This changelog goes through all the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.io). +## [1.8.2](https://github.com/go-gitea/gitea/releases/tag/v1.8.2) - 2019-05-29 +* BUGFIXES + * Fix possbile mysql invalid connnection error (#7051) (#7071) + * Handle invalid administrator username on install page (#7060) (#7063) + * Disable arm7 builds (#7037) (#7042) + * Fix default for allowing new organization creation for new users (#7017) (#7034) + * SearchRepositoryByName improvements and unification (#6897) (#7002) + * Fix u2f registrationlist ToRegistrations() method (#6980) (#6982) + * Allow collaborators to view repo owned by private org (#6965) (#6968) + * Use AppURL for Oauth user link (#6894) (#6925) + * Escape the commit message on issues update (#6901) (#6902) + * Fix regression for API users search (#6882) (#6885) + * Handle early git version's lack of get-url (#7065) (#7076) + * Fix wrong init dependency on markup extensions (#7038) (#7074) + ## [1.8.1](https://github.com/go-gitea/gitea/releases/tag/v1.8.1) - 2019-05-08 * BUGFIXES * Fix 404 when sending pull requests in some situations (#6871) (#6873) From d95caf50ec9e34a652a0e51baf5037c8cf424a01 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Thu, 30 May 2019 16:49:44 +0000 Subject: [PATCH 36/44] [skip ci] Updated translations via Crowdin --- options/locale/locale_de-DE.ini | 5 +++++ options/locale/locale_fr-FR.ini | 22 ++++++++++++++++++++++ options/locale/locale_ja-JP.ini | 2 ++ options/locale/locale_pt-BR.ini | 2 ++ 4 files changed, 31 insertions(+) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 7082e9e8a2e7e..3a3e970d774a0 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -386,6 +386,7 @@ choose_new_avatar=Neues Profilbild auswählen update_avatar=Profilbild aktualisieren delete_current_avatar=Aktuelles Profilbild löschen uploaded_avatar_not_a_image=Die hochgeladene Datei ist kein Bild. +uploaded_avatar_is_too_big=Die hochgeladene Datei hat die maximale Größe überschritten. update_avatar_success=Dein Profilbild wurde geändert. change_password=Passwort aktualisieren @@ -615,6 +616,7 @@ migrate.permission_denied=Du hast keine Berechtigung zum Importieren lokaler Rep migrate.invalid_local_path=Der lokale Pfad ist ungültig, existiert nicht oder ist kein Ordner. migrate.failed=Fehler bei der Migration: %v migrate.lfs_mirror_unsupported=Spiegeln von LFS-Objekten wird nicht unterstützt - nutze stattdessen 'git lfs fetch --all' und 'git lfs push --all'. +migrate.migrate_items_options=Wenn du von GitHub migrierst und einen Benutzernamen eingegeben hast, werden die Migrationsoptionen angezeigt. mirror_from=Mirror von forked_from=geforkt von @@ -1269,7 +1271,9 @@ settings.protected_branch_can_push_yes=Du kannst pushen settings.protected_branch_can_push_no=Du kannst nicht pushen settings.branch_protection=Branch-Schutz für Branch „%s“ settings.protect_this_branch=Branch-Schutz aktivieren +settings.protect_this_branch_desc=Löschen verhindern und „force pushing” von Git auf dieser Branch deaktivieren. settings.protect_whitelist_committers=Push-Whitelist aktivieren +settings.protect_whitelist_committers_desc=Erlaube, dass Benutzer oder Teams, die auf der Whitelist stehen, auf diese Branch "pushen" dürfen (aber nicht "force pushen"). settings.protect_whitelist_users=Nutzer, die pushen dürfen: settings.protect_whitelist_search_users=Benutzer suchen… settings.protect_whitelist_teams=Teams, die pushen dürfen: @@ -1307,6 +1311,7 @@ settings.unarchive.header=Archivieren dieses Repos rückgängig machen settings.unarchive.text=Das Rückgängig machen dieses Repo-Archivs stellt seine Fähigkeit wieder her, Commits und Pushes anzunehmen, sowie neue Issues und Pull-Requests zu erstellen. settings.unarchive.success=Die Archivierung des Repos wurde erfolgreich wieder rückgängig gemacht. settings.unarchive.error=Beim Rückgängig machen dieses Repo-Archivs trat ein Fehler auf. In den Logs befinden sich mehr Details. +settings.update_avatar_success=Der Repository-Avatar wurde aktualisiert. diff.browse_source=Quellcode durchsuchen diff.parent=Ursprung diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index c8dbfd9839f9b..e6755f8cb5662 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -86,12 +86,18 @@ host=Hôte user=Nom d'utilisateur password=Mot de passe db_name=Nom de base de données +db_helper=Note aux utilisateurs de MySQL : utilisez le moteur de stockage InnoDB et si vous utilisez "utf8mb4", votre version InnoDB doit être supérieure à 5.6 . ssl_mode=SSL +charset=Jeu de caractères path=Emplacement sqlite_helper=Chemin d'accès pour la base de données SQLite3.
Entrer un chemin absolu si vous exécutez Gitea en tant que service. err_empty_db_path=Le chemin de la base de donnée SQLite3 ne peut être vide. no_admin_and_disable_registration=Vous ne pouvez pas désactiver la création de nouveaux utilisateurs avant d'avoir créé un compte administrateur. err_empty_admin_password=Le mot de passe administrateur ne peut pas être vide. +err_empty_admin_email=L'adresse e-mail de l'administrateur ne peut pas être vide. +err_admin_name_is_reserved=Le nom d'utilisateur de l'administrateur est invalide, le nom d'utilisateur est réservé +err_admin_name_pattern_not_allowed=Le nom d'utilisateur de l'administrateur est invalide, le nom d'utilisateur est interdit +err_admin_name_is_invalid=Le nom d'utilisateur de l'administrateur est invalide general_title=Configuration générale app_name=Titre du site @@ -299,6 +305,8 @@ password_not_match=Les mots de passe ne correspondent pas. username_been_taken=Le nom d'utilisateur est déjà pris. repo_name_been_taken=Ce nom de dépôt est déjà utilisé. +visit_rate_limit=Le taux d'appel à distance autorisé a été dépassé. +2fa_auth_required=L'accès à distance requiert une authentification à deux facteurs. org_name_been_taken=Ce nom d'organisation est déjà pris. team_name_been_taken=Le nom d'équipe est déjà pris. team_no_units_error=Autoriser l’accès à au moins une section du dépôt. @@ -380,6 +388,7 @@ choose_new_avatar=Sélectionner un nouvel avatar update_avatar=Mise à jour de l'avatar delete_current_avatar=Supprimer l'avatar actuel uploaded_avatar_not_a_image=Le fichier téléversé n'est pas une image. +uploaded_avatar_is_too_big=Le fichier téléversé dépasse la taille maximale. update_avatar_success=Votre avatar a été mis à jour. change_password=Modifier le mot de passe @@ -595,6 +604,13 @@ form.name_pattern_not_allowed=%s" n'est pas autorisé dans un nom de dépôt. need_auth=Autorisations de clonage migrate_type=Type de migration migrate_type_helper=Ce dépôt sera un miroir +migrate_items=Éléments à migrer +migrate_items_wiki=Wiki +migrate_items_milestones=Jalons +migrate_items_labels=Étiquettes +migrate_items_issues=Tickets +migrate_items_pullrequests=Demandes d'ajout +migrate_items_releases=Versions migrate_repo=Migrer le dépôt migrate.clone_address=Migrer/Cloner depuis une URL migrate.clone_address_desc=L'URL HTTP(S) ou Git "clone" d'un dépôt existant @@ -603,6 +619,7 @@ migrate.permission_denied=Vous n'êtes pas autorisé à importer des dépôts lo migrate.invalid_local_path=Chemin local non valide, non existant ou n'étant pas un dossier. migrate.failed=Echec de migration: %v migrate.lfs_mirror_unsupported=La synchronisation des objets LFS n'est pas supportée - veuillez utiliser 'git lfs fetch --all' et 'git lfs push --all' à la place. +migrate.migrate_items_options=Quand vous migrez depuis github, une fois le nom d'utilisateur saisi, des options de migration supplémentaires seront affichées. mirror_from=miroir de forked_from=bifurqué depuis @@ -1257,7 +1274,9 @@ settings.protected_branch_can_push_yes=Vous pouvez pousser settings.protected_branch_can_push_no=Vous ne pouvez pas pousser settings.branch_protection=Protection de la branche "%s settings.protect_this_branch=Protection de la branche +settings.protect_this_branch_desc=Interdire de supprimer et de pousser sur la branche. settings.protect_whitelist_committers=Activer la liste blanche pour les mises à jour +settings.protect_whitelist_committers_desc=Autoriser les utilisateurs et les équipes sur liste blanche à supprimer et pousser sur la branche (mais pas de poussée forcée). settings.protect_whitelist_users=Utilisateurs en liste blanche : settings.protect_whitelist_search_users=Rechercher des utilisateurs… settings.protect_whitelist_teams=Équipes en liste blanche : @@ -1295,10 +1314,12 @@ settings.unarchive.header=Désarchiver ce dépôt settings.unarchive.text=Désarchiver le dépôt lui permettra de recevoir des révisions, ainsi que des nouveaux tickets ou demandes d'ajout. settings.unarchive.success=Ce dépôt a été désarchivé avec succès. settings.unarchive.error=Une erreur s'est produite durant le désarchivage. Referez-vous au journal pour plus de détails. +settings.update_avatar_success=L'avatar du dépôt a été mis à jour. diff.browse_source=Parcourir la source diff.parent=Parent diff.commit=révision +diff.git-notes=Notes diff.data_not_available=Contenu de la comparaison indisponible diff.show_diff_stats=Afficher les stats Diff diff.show_split_view=Vue séparée @@ -1753,6 +1774,7 @@ config.cache_config=Configuration du cache config.cache_adapter=Adaptateur du Cache config.cache_interval=Intervales du Cache config.cache_conn=Liaison du Cache +config.cache_item_ttl=Durée de vie des éléments dans le cache config.session_config=Configuration de session config.session_provider=Fournisseur de session diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index beeeb848a2169..e65162dfe2b71 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -388,6 +388,7 @@ choose_new_avatar=新しいアバターを選択 update_avatar=アバターを更新 delete_current_avatar=現在のアバターを削除 uploaded_avatar_not_a_image=アップロードしたファイルは画像ファイルではありません。 +uploaded_avatar_is_too_big=アップロードしたファイルは最大サイズを超えています。 update_avatar_success=アバターを更新しました。 change_password=パスワードを更新 @@ -1313,6 +1314,7 @@ settings.unarchive.header=このリポジトリをアーカイブ解除 settings.unarchive.text=リポジトリのアーカイブを解除すると、コミット、プッシュ、新規の課題やプルリクエストを受け付けるよう元に戻されます。 settings.unarchive.success=リポジトリのアーカイブを解除しました。 settings.unarchive.error=リポジトリのアーカイブ解除でエラーが発生しました。 詳細はログを確認してください。 +settings.update_avatar_success=リポジトリのアバターを更新しました。 diff.browse_source=ソースを参照 diff.parent=親 diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 26ff625df26ce..65b26c6ee9101 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -388,6 +388,7 @@ choose_new_avatar=Escolha um novo avatar update_avatar=Atualizar o avatar delete_current_avatar=Excluir o avatar atual uploaded_avatar_not_a_image=O arquivo enviado não é uma imagem. +uploaded_avatar_is_too_big=O arquivo enviado excedeu o tamanho máximo. update_avatar_success=Seu avatar foi atualizado. change_password=Atualizar senha @@ -1313,6 +1314,7 @@ settings.unarchive.header=Desarquivar este repositório settings.unarchive.text=Desarquivando um repositório irá restaurar a capacidade do mesmo receber commits, pushs, assim como novas issues e pull requests. settings.unarchive.success=O repositório foi desarquivado com sucesso. settings.unarchive.error=Um erro ocorreu enquanto estava sendo desarquivado o repositório. Veja o log para mais detalhes. +settings.update_avatar_success=O avatar do repositório foi atualizado. diff.browse_source=Ver código fonte diff.parent=pai From 43cf2f3b55de4a69183966da2a6e0167592c733c Mon Sep 17 00:00:00 2001 From: Richard Mahn Date: Thu, 30 May 2019 13:57:55 -0400 Subject: [PATCH 37/44] Fixes #7023 - API Org Visibility (#7028) --- integrations/api_admin_org_test.go | 86 ++++++++++++++++++++++++++++++ integrations/api_org_test.go | 48 ++++++++++++++++- integrations/api_user_orgs_test.go | 2 + modules/structs/org.go | 33 +++++++----- modules/structs/org_type.go | 10 ++++ modules/structs/repo_file.go | 26 ++++++--- routers/api/v1/admin/org.go | 7 +++ routers/api/v1/convert/convert.go | 1 + routers/api/v1/org/org.go | 14 ++++- routers/api/v1/repo/file.go | 6 +-- templates/swagger/v1_json.tmpl | 65 +++++++++++++++++----- 11 files changed, 258 insertions(+), 40 deletions(-) create mode 100644 integrations/api_admin_org_test.go diff --git a/integrations/api_admin_org_test.go b/integrations/api_admin_org_test.go new file mode 100644 index 0000000000000..546ed861c255a --- /dev/null +++ b/integrations/api_admin_org_test.go @@ -0,0 +1,86 @@ +// Copyright 2019 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 ( + "net/http" + "net/url" + "strings" + "testing" + + "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestAPIAdminOrgCreate(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session) + + var org = api.CreateOrgOption{ + UserName: "user2_org", + FullName: "User2's organization", + Description: "This organization created by admin for user2", + Website: "https://try.gitea.io", + Location: "Shanghai", + Visibility: "private", + } + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs?token="+token, &org) + resp := session.MakeRequest(t, req, http.StatusCreated) + + var apiOrg api.Organization + DecodeJSON(t, resp, &apiOrg) + + assert.Equal(t, org.UserName, apiOrg.UserName) + assert.Equal(t, org.FullName, apiOrg.FullName) + assert.Equal(t, org.Description, apiOrg.Description) + assert.Equal(t, org.Website, apiOrg.Website) + assert.Equal(t, org.Location, apiOrg.Location) + assert.Equal(t, org.Visibility, apiOrg.Visibility) + + models.AssertExistsAndLoadBean(t, &models.User{ + Name: org.UserName, + LowerName: strings.ToLower(org.UserName), + FullName: org.FullName, + }) + }) +} + +func TestAPIAdminOrgCreateBadVisibility(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session) + + var org = api.CreateOrgOption{ + UserName: "user2_org", + FullName: "User2's organization", + Description: "This organization created by admin for user2", + Website: "https://try.gitea.io", + Location: "Shanghai", + Visibility: "notvalid", + } + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs?token="+token, &org) + session.MakeRequest(t, req, http.StatusUnprocessableEntity) + }) +} + +func TestAPIAdminOrgCreateNotAdmin(t *testing.T) { + prepareTestEnv(t) + nonAdminUsername := "user2" + session := loginUser(t, nonAdminUsername) + token := getTokenForLoggedInUser(t, session) + var org = api.CreateOrgOption{ + UserName: "user2_org", + FullName: "User2's organization", + Description: "This organization created by admin for user2", + Website: "https://try.gitea.io", + Location: "Shanghai", + Visibility: "public", + } + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs?token="+token, &org) + session.MakeRequest(t, req, http.StatusForbidden) +} diff --git a/integrations/api_org_test.go b/integrations/api_org_test.go index b36650f2e8b5f..34579aa1ea9c1 100644 --- a/integrations/api_org_test.go +++ b/integrations/api_org_test.go @@ -17,7 +17,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAPIOrg(t *testing.T) { +func TestAPIOrgCreate(t *testing.T) { onGiteaRun(t, func(*testing.T, *url.URL) { session := loginUser(t, "user1") @@ -28,6 +28,7 @@ func TestAPIOrg(t *testing.T) { Description: "This organization created by user1", Website: "https://try.gitea.io", Location: "Shanghai", + Visibility: "limited", } req := NewRequestWithJSON(t, "POST", "/api/v1/orgs?token="+token, &org) resp := session.MakeRequest(t, req, http.StatusCreated) @@ -40,6 +41,7 @@ func TestAPIOrg(t *testing.T) { assert.Equal(t, org.Description, apiOrg.Description) assert.Equal(t, org.Website, apiOrg.Website) assert.Equal(t, org.Location, apiOrg.Location) + assert.Equal(t, org.Visibility, apiOrg.Visibility) models.AssertExistsAndLoadBean(t, &models.User{ Name: org.UserName, @@ -72,6 +74,50 @@ func TestAPIOrg(t *testing.T) { }) } +func TestAPIOrgEdit(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + session := loginUser(t, "user1") + + token := getTokenForLoggedInUser(t, session) + var org = api.EditOrgOption{ + FullName: "User3 organization new full name", + Description: "A new description", + Website: "https://try.gitea.io/new", + Location: "Beijing", + Visibility: "private", + } + req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/user3?token="+token, &org) + resp := session.MakeRequest(t, req, http.StatusOK) + + var apiOrg api.Organization + DecodeJSON(t, resp, &apiOrg) + + assert.Equal(t, "user3", apiOrg.UserName) + assert.Equal(t, org.FullName, apiOrg.FullName) + assert.Equal(t, org.Description, apiOrg.Description) + assert.Equal(t, org.Website, apiOrg.Website) + assert.Equal(t, org.Location, apiOrg.Location) + assert.Equal(t, org.Visibility, apiOrg.Visibility) + }) +} + +func TestAPIOrgEditBadVisibility(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + session := loginUser(t, "user1") + + token := getTokenForLoggedInUser(t, session) + var org = api.EditOrgOption{ + FullName: "User3 organization new full name", + Description: "A new description", + Website: "https://try.gitea.io/new", + Location: "Beijing", + Visibility: "badvisibility", + } + req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/user3?token="+token, &org) + session.MakeRequest(t, req, http.StatusUnprocessableEntity) + }) +} + func TestAPIOrgDeny(t *testing.T) { onGiteaRun(t, func(*testing.T, *url.URL) { setting.Service.RequireSignInView = true diff --git a/integrations/api_user_orgs_test.go b/integrations/api_user_orgs_test.go index 63e67f4356a30..6611a429d1f68 100644 --- a/integrations/api_user_orgs_test.go +++ b/integrations/api_user_orgs_test.go @@ -38,6 +38,7 @@ func TestUserOrgs(t *testing.T) { Description: "", Website: "", Location: "", + Visibility: "public", }, }, orgs) } @@ -63,6 +64,7 @@ func TestMyOrgs(t *testing.T) { Description: "", Website: "", Location: "", + Visibility: "public", }, }, orgs) } diff --git a/modules/structs/org.go b/modules/structs/org.go index fd15da1ce946c..08ab139975283 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -6,25 +6,27 @@ package structs // Organization represents an organization type Organization struct { - ID int64 `json:"id"` - UserName string `json:"username"` - FullName string `json:"full_name"` - AvatarURL string `json:"avatar_url"` - Description string `json:"description"` - Website string `json:"website"` - Location string `json:"location"` - Visibility VisibleType `json:"visibility"` + ID int64 `json:"id"` + UserName string `json:"username"` + FullName string `json:"full_name"` + AvatarURL string `json:"avatar_url"` + Description string `json:"description"` + Website string `json:"website"` + Location string `json:"location"` + Visibility string `json:"visibility"` } // CreateOrgOption options for creating an organization type CreateOrgOption struct { // required: true - UserName string `json:"username" binding:"Required"` - FullName string `json:"full_name"` - Description string `json:"description"` - Website string `json:"website"` - Location string `json:"location"` - Visibility VisibleType `json:"visibility"` + UserName string `json:"username" binding:"Required"` + FullName string `json:"full_name"` + Description string `json:"description"` + Website string `json:"website"` + Location string `json:"location"` + // possible values are `public` (default), `limited` or `private` + // enum: public,limited,private + Visibility string `json:"visibility" binding:"In(,public,limited,private)"` } // EditOrgOption options for editing an organization @@ -33,4 +35,7 @@ type EditOrgOption struct { Description string `json:"description"` Website string `json:"website"` Location string `json:"location"` + // possible values are `public`, `limited` or `private` + // enum: public,limited,private + Visibility string `json:"visibility" binding:"In(,public,limited,private)"` } diff --git a/modules/structs/org_type.go b/modules/structs/org_type.go index 86dc5c81cd6ab..4fb9b6fc0fdc8 100644 --- a/modules/structs/org_type.go +++ b/modules/structs/org_type.go @@ -40,6 +40,16 @@ func (vt VisibleType) IsPrivate() bool { return vt == VisibleTypePrivate } +// VisibilityString provides the mode string of the visibility type (public, limited, private) +func (vt VisibleType) String() string { + for k, v := range VisibilityModes { + if vt == v { + return k + } + } + return "" +} + // ExtractKeysFromMapString provides a slice of keys from map func ExtractKeysFromMapString(in map[string]VisibleType) (keys []string) { for k := range in { diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index ac8b9333fe57c..c447d267244c9 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -7,29 +7,43 @@ package structs // FileOptions options for all file APIs type FileOptions struct { - Message string `json:"message" binding:"Required"` - BranchName string `json:"branch"` - NewBranchName string `json:"new_branch"` - Author Identity `json:"author"` - Committer Identity `json:"committer"` + // message (optional) for the commit of this file. if not supplied, a default message will be used + Message string `json:"message" binding:"Required"` + // branch (optional) to base this file from. if not given, the default branch is used + BranchName string `json:"branch"` + // new_branch (optional) will make a new branch from `branch` before creating the file + NewBranchName string `json:"new_branch"` + // `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) + Author Identity `json:"author"` + Committer Identity `json:"committer"` } // CreateFileOptions options for creating files +// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type CreateFileOptions struct { FileOptions + // content must be base64 encoded + // required: true Content string `json:"content"` } // DeleteFileOptions options for deleting files (used for other File structs below) +// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type DeleteFileOptions struct { FileOptions + // sha is the SHA for the file that already exists + // required: true SHA string `json:"sha" binding:"Required"` } // UpdateFileOptions options for updating files +// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type UpdateFileOptions struct { DeleteFileOptions - Content string `json:"content"` + // content must be base64 encoded + // required: true + Content string `json:"content"` + // from_path (optional) is the path of the original file which will be moved/renamed to the path in the URL FromPath string `json:"from_path" binding:"MaxSize(500)"` } diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index fba41a8cfece8..d740647cd4f20 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -45,6 +45,11 @@ func CreateOrg(ctx *context.APIContext, form api.CreateOrgOption) { return } + visibility := api.VisibleTypePublic + if form.Visibility != "" { + visibility = api.VisibilityModes[form.Visibility] + } + org := &models.User{ Name: form.UserName, FullName: form.FullName, @@ -53,7 +58,9 @@ func CreateOrg(ctx *context.APIContext, form api.CreateOrgOption) { Location: form.Location, IsActive: true, Type: models.UserTypeOrganization, + Visibility: visibility, } + if err := models.CreateOrganization(org, u); err != nil { if models.IsErrUserAlreadyExist(err) || models.IsErrNameReserved(err) || diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go index 74fd9b3afd354..ba61c7e46c938 100644 --- a/routers/api/v1/convert/convert.go +++ b/routers/api/v1/convert/convert.go @@ -213,6 +213,7 @@ func ToOrganization(org *models.User) *api.Organization { Description: org.Description, Website: org.Website, Location: org.Location, + Visibility: org.Visibility.String(), } } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index e1d0663f05aa4..2893887a4bf14 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -90,6 +90,11 @@ func Create(ctx *context.APIContext, form api.CreateOrgOption) { return } + visibility := api.VisibleTypePublic + if form.Visibility != "" { + visibility = api.VisibilityModes[form.Visibility] + } + org := &models.User{ Name: form.UserName, FullName: form.FullName, @@ -98,6 +103,7 @@ func Create(ctx *context.APIContext, form api.CreateOrgOption) { Location: form.Location, IsActive: true, Type: models.UserTypeOrganization, + Visibility: visibility, } if err := models.CreateOrganization(org, ctx.User); err != nil { if models.IsErrUserAlreadyExist(err) || @@ -153,6 +159,7 @@ func Edit(ctx *context.APIContext, form api.EditOrgOption) { // required: true // - name: body // in: body + // required: true // schema: // "$ref": "#/definitions/EditOrgOption" // responses: @@ -163,8 +170,11 @@ func Edit(ctx *context.APIContext, form api.EditOrgOption) { org.Description = form.Description org.Website = form.Website org.Location = form.Location - if err := models.UpdateUserCols(org, "full_name", "description", "website", "location"); err != nil { - ctx.Error(500, "UpdateUser", err) + if form.Visibility != "" { + org.Visibility = api.VisibilityModes[form.Visibility] + } + if err := models.UpdateUserCols(org, "full_name", "description", "website", "location", "visibility"); err != nil { + ctx.Error(500, "EditOrganization", err) return } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index db952263e2f23..20f80f37f4c67 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -181,7 +181,7 @@ func CreateFile(ctx *context.APIContext, apiOpts api.CreateFileOptions) { // required: true // - name: body // in: body - // description: "'content' must be base64 encoded\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'sha' is the SHA for the file that already exists\n\n 'new_branch' (optional) will make a new branch from 'branch' before creating the file" + // required: true // schema: // "$ref": "#/definitions/CreateFileOptions" // responses: @@ -238,7 +238,7 @@ func UpdateFile(ctx *context.APIContext, apiOpts api.UpdateFileOptions) { // required: true // - name: body // in: body - // description: "'content' must be base64 encoded\n\n 'sha' is the SHA for the file that already exists\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before updating the file" + // required: true // schema: // "$ref": "#/definitions/UpdateFileOptions" // responses: @@ -316,7 +316,7 @@ func DeleteFile(ctx *context.APIContext, apiOpts api.DeleteFileOptions) { // required: true // - name: body // in: body - // description: "'sha' is the SHA for the file to be deleted\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before deleting the file" + // required: true // schema: // "$ref": "#/definitions/DeleteFileOptions" // responses: diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index a3090d1d52eb5..77515bb139e61 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -552,6 +552,7 @@ { "name": "body", "in": "body", + "required": true, "schema": { "$ref": "#/definitions/EditOrgOption" } @@ -1649,9 +1650,9 @@ "required": true }, { - "description": "'content' must be base64 encoded\n\n 'sha' is the SHA for the file that already exists\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before updating the file", "name": "body", "in": "body", + "required": true, "schema": { "$ref": "#/definitions/UpdateFileOptions" } @@ -1698,9 +1699,9 @@ "required": true }, { - "description": "'content' must be base64 encoded\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'sha' is the SHA for the file that already exists\n\n 'new_branch' (optional) will make a new branch from 'branch' before creating the file", "name": "body", "in": "body", + "required": true, "schema": { "$ref": "#/definitions/CreateFileOptions" } @@ -1747,9 +1748,9 @@ "required": true }, { - "description": "'sha' is the SHA for the file to be deleted\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before deleting the file", "name": "body", "in": "body", + "required": true, "schema": { "$ref": "#/definitions/DeleteFileOptions" } @@ -6935,13 +6936,17 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "CreateFileOptions": { - "description": "CreateFileOptions options for creating files", + "description": "CreateFileOptions options for creating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "type": "object", + "required": [ + "content" + ], "properties": { "author": { "$ref": "#/definitions/Identity" }, "branch": { + "description": "branch (optional) to base this file from. if not given, the default branch is used", "type": "string", "x-go-name": "BranchName" }, @@ -6949,14 +6954,17 @@ "$ref": "#/definitions/Identity" }, "content": { + "description": "content must be base64 encoded", "type": "string", "x-go-name": "Content" }, "message": { + "description": "message (optional) for the commit of this file. if not supplied, a default message will be used", "type": "string", "x-go-name": "Message" }, "new_branch": { + "description": "new_branch (optional) will make a new branch from `branch` before creating the file", "type": "string", "x-go-name": "NewBranchName" } @@ -7191,7 +7199,14 @@ "x-go-name": "UserName" }, "visibility": { - "$ref": "#/definitions/VisibleType" + "description": "possible values are `public` (default), `limited` or `private`", + "type": "string", + "enum": [ + "public", + "limited", + "private" + ], + "x-go-name": "Visibility" }, "website": { "type": "string", @@ -7459,13 +7474,17 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "DeleteFileOptions": { - "description": "DeleteFileOptions options for deleting files (used for other File structs below)", + "description": "DeleteFileOptions options for deleting files (used for other File structs below)\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "type": "object", + "required": [ + "sha" + ], "properties": { "author": { "$ref": "#/definitions/Identity" }, "branch": { + "description": "branch (optional) to base this file from. if not given, the default branch is used", "type": "string", "x-go-name": "BranchName" }, @@ -7473,14 +7492,17 @@ "$ref": "#/definitions/Identity" }, "message": { + "description": "message (optional) for the commit of this file. if not supplied, a default message will be used", "type": "string", "x-go-name": "Message" }, "new_branch": { + "description": "new_branch (optional) will make a new branch from `branch` before creating the file", "type": "string", "x-go-name": "NewBranchName" }, "sha": { + "description": "sha is the SHA for the file that already exists", "type": "string", "x-go-name": "SHA" } @@ -7703,6 +7725,16 @@ "type": "string", "x-go-name": "Location" }, + "visibility": { + "description": "possible values are `public`, `limited` or `private`", + "type": "string", + "enum": [ + "public", + "limited", + "private" + ], + "x-go-name": "Visibility" + }, "website": { "type": "string", "x-go-name": "Website" @@ -8741,7 +8773,8 @@ "x-go-name": "UserName" }, "visibility": { - "$ref": "#/definitions/VisibleType" + "type": "string", + "x-go-name": "Visibility" }, "website": { "type": "string", @@ -9537,13 +9570,18 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "UpdateFileOptions": { - "description": "UpdateFileOptions options for updating files", + "description": "UpdateFileOptions options for updating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "type": "object", + "required": [ + "sha", + "content" + ], "properties": { "author": { "$ref": "#/definitions/Identity" }, "branch": { + "description": "branch (optional) to base this file from. if not given, the default branch is used", "type": "string", "x-go-name": "BranchName" }, @@ -9551,22 +9589,27 @@ "$ref": "#/definitions/Identity" }, "content": { + "description": "content must be base64 encoded", "type": "string", "x-go-name": "Content" }, "from_path": { + "description": "from_path (optional) is the path of the original file which will be moved/renamed to the path in the URL", "type": "string", "x-go-name": "FromPath" }, "message": { + "description": "message (optional) for the commit of this file. if not supplied, a default message will be used", "type": "string", "x-go-name": "Message" }, "new_branch": { + "description": "new_branch (optional) will make a new branch from `branch` before creating the file", "type": "string", "x-go-name": "NewBranchName" }, "sha": { + "description": "sha is the SHA for the file that already exists", "type": "string", "x-go-name": "SHA" } @@ -9631,12 +9674,6 @@ }, "x-go-package": "code.gitea.io/gitea/models" }, - "VisibleType": { - "description": "VisibleType defines the visibility (Organization only)", - "type": "integer", - "format": "int64", - "x-go-package": "code.gitea.io/gitea/modules/structs" - }, "WatchInfo": { "description": "WatchInfo represents an API watch status of one repository", "type": "object", From 7d12ec2abd452c6a8a5981537ce2c50440979e25 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 31 May 2019 04:26:57 +0800 Subject: [PATCH 38/44] improve github downloader on migrations (#7049) * improve github downloader on migrations * fix tests * fix uppercase function parameters --- modules/migrations/base/downloader.go | 4 +- modules/migrations/git.go | 8 +- modules/migrations/github.go | 250 ++++++++++++-------------- modules/migrations/github_test.go | 6 +- modules/migrations/migrate.go | 10 +- 5 files changed, 134 insertions(+), 144 deletions(-) diff --git a/modules/migrations/base/downloader.go b/modules/migrations/base/downloader.go index 9a09fdac0ac10..f28d0b61e73be 100644 --- a/modules/migrations/base/downloader.go +++ b/modules/migrations/base/downloader.go @@ -11,9 +11,9 @@ type Downloader interface { GetMilestones() ([]*Milestone, error) GetReleases() ([]*Release, error) GetLabels() ([]*Label, error) - GetIssues(start, limit int) ([]*Issue, error) + GetIssues(page, perPage int) ([]*Issue, bool, error) GetComments(issueNumber int64) ([]*Comment, error) - GetPullRequests(start, limit int) ([]*PullRequest, error) + GetPullRequests(page, perPage int) ([]*PullRequest, error) } // DownloaderFactory defines an interface to match a downloader implementation and create a downloader diff --git a/modules/migrations/git.go b/modules/migrations/git.go index cbaa37282170a..335d44ec9b63a 100644 --- a/modules/migrations/git.go +++ b/modules/migrations/git.go @@ -53,9 +53,9 @@ func (g *PlainGitDownloader) GetReleases() ([]*base.Release, error) { return nil, ErrNotSupported } -// GetIssues returns issues according start and limit -func (g *PlainGitDownloader) GetIssues(start, limit int) ([]*base.Issue, error) { - return nil, ErrNotSupported +// GetIssues returns issues according page and perPage +func (g *PlainGitDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { + return nil, false, ErrNotSupported } // GetComments returns comments according issueNumber @@ -63,7 +63,7 @@ func (g *PlainGitDownloader) GetComments(issueNumber int64) ([]*base.Comment, er return nil, ErrNotSupported } -// GetPullRequests returns pull requests according start and limit +// GetPullRequests returns pull requests according page and perPage func (g *PlainGitDownloader) GetPullRequests(start, limit int) ([]*base.PullRequest, error) { return nil, ErrNotSupported } diff --git a/modules/migrations/github.go b/modules/migrations/github.go index 8e1cd67df8321..21c1becedfb7d 100644 --- a/modules/migrations/github.go +++ b/modules/migrations/github.go @@ -272,71 +272,65 @@ func convertGithubReactions(reactions *github.Reactions) *base.Reactions { } // GetIssues returns issues according start and limit -func (g *GithubDownloaderV3) GetIssues(start, limit int) ([]*base.Issue, error) { - var perPage = 100 +func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { opt := &github.IssueListByRepoOptions{ Sort: "created", Direction: "asc", State: "all", ListOptions: github.ListOptions{ PerPage: perPage, + Page: page, }, } - var allIssues = make([]*base.Issue, 0, limit) - for { - issues, resp, err := g.client.Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt) - if err != nil { - return nil, fmt.Errorf("error while listing repos: %v", err) - } - for _, issue := range issues { - if issue.IsPullRequest() { - continue - } - var body string - if issue.Body != nil { - body = *issue.Body - } - var milestone string - if issue.Milestone != nil { - milestone = *issue.Milestone.Title - } - var labels = make([]*base.Label, 0, len(issue.Labels)) - for _, l := range issue.Labels { - labels = append(labels, convertGithubLabel(&l)) - } - var reactions *base.Reactions - if issue.Reactions != nil { - reactions = convertGithubReactions(issue.Reactions) - } - var email string - if issue.User.Email != nil { - email = *issue.User.Email - } - allIssues = append(allIssues, &base.Issue{ - Title: *issue.Title, - Number: int64(*issue.Number), - PosterName: *issue.User.Login, - PosterEmail: email, - Content: body, - Milestone: milestone, - State: *issue.State, - Created: *issue.CreatedAt, - Labels: labels, - Reactions: reactions, - Closed: issue.ClosedAt, - IsLocked: *issue.Locked, - }) - if len(allIssues) >= limit { - return allIssues, nil - } + var allIssues = make([]*base.Issue, 0, perPage) + + issues, _, err := g.client.Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt) + if err != nil { + return nil, false, fmt.Errorf("error while listing repos: %v", err) + } + for _, issue := range issues { + if issue.IsPullRequest() { + continue } - if resp.NextPage == 0 { - break + var body string + if issue.Body != nil { + body = *issue.Body } - opt.Page = resp.NextPage + var milestone string + if issue.Milestone != nil { + milestone = *issue.Milestone.Title + } + var labels = make([]*base.Label, 0, len(issue.Labels)) + for _, l := range issue.Labels { + labels = append(labels, convertGithubLabel(&l)) + } + var reactions *base.Reactions + if issue.Reactions != nil { + reactions = convertGithubReactions(issue.Reactions) + } + + var email string + if issue.User.Email != nil { + email = *issue.User.Email + } + allIssues = append(allIssues, &base.Issue{ + Title: *issue.Title, + Number: int64(*issue.Number), + PosterName: *issue.User.Login, + PosterEmail: email, + Content: body, + Milestone: milestone, + State: *issue.State, + Created: *issue.CreatedAt, + Labels: labels, + Reactions: reactions, + Closed: issue.ClosedAt, + IsLocked: *issue.Locked, + }) } - return allIssues, nil + + return allIssues, len(issues) < perPage, nil } // GetComments returns comments according issueNumber @@ -379,97 +373,91 @@ func (g *GithubDownloaderV3) GetComments(issueNumber int64) ([]*base.Comment, er return allComments, nil } -// GetPullRequests returns pull requests according start and limit -func (g *GithubDownloaderV3) GetPullRequests(start, limit int) ([]*base.PullRequest, error) { +// GetPullRequests returns pull requests according page and perPage +func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, error) { opt := &github.PullRequestListOptions{ Sort: "created", Direction: "asc", State: "all", ListOptions: github.ListOptions{ - PerPage: 100, + PerPage: perPage, + Page: page, }, } - var allPRs = make([]*base.PullRequest, 0, 100) - for { - prs, resp, err := g.client.PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt) - if err != nil { - return nil, fmt.Errorf("error while listing repos: %v", err) - } - for _, pr := range prs { - var body string - if pr.Body != nil { - body = *pr.Body - } - var milestone string - if pr.Milestone != nil { - milestone = *pr.Milestone.Title - } - var labels = make([]*base.Label, 0, len(pr.Labels)) - for _, l := range pr.Labels { - labels = append(labels, convertGithubLabel(l)) - } + var allPRs = make([]*base.PullRequest, 0, perPage) - // FIXME: This API missing reactions, we may need another extra request to get reactions + prs, _, err := g.client.PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt) + if err != nil { + return nil, fmt.Errorf("error while listing repos: %v", err) + } + for _, pr := range prs { + var body string + if pr.Body != nil { + body = *pr.Body + } + var milestone string + if pr.Milestone != nil { + milestone = *pr.Milestone.Title + } + var labels = make([]*base.Label, 0, len(pr.Labels)) + for _, l := range pr.Labels { + labels = append(labels, convertGithubLabel(l)) + } - var email string - if pr.User.Email != nil { - email = *pr.User.Email - } - var merged bool - // pr.Merged is not valid, so use MergedAt to test if it's merged - if pr.MergedAt != nil { - merged = true - } + // FIXME: This API missing reactions, we may need another extra request to get reactions - var headRepoName string - var cloneURL string - if pr.Head.Repo != nil { - headRepoName = *pr.Head.Repo.Name - cloneURL = *pr.Head.Repo.CloneURL - } - var mergeCommitSHA string - if pr.MergeCommitSHA != nil { - mergeCommitSHA = *pr.MergeCommitSHA - } + var email string + if pr.User.Email != nil { + email = *pr.User.Email + } + var merged bool + // pr.Merged is not valid, so use MergedAt to test if it's merged + if pr.MergedAt != nil { + merged = true + } - allPRs = append(allPRs, &base.PullRequest{ - Title: *pr.Title, - Number: int64(*pr.Number), - PosterName: *pr.User.Login, - PosterEmail: email, - Content: body, - Milestone: milestone, - State: *pr.State, - Created: *pr.CreatedAt, - Closed: pr.ClosedAt, - Labels: labels, - Merged: merged, - MergeCommitSHA: mergeCommitSHA, - MergedTime: pr.MergedAt, - IsLocked: pr.ActiveLockReason != nil, - Head: base.PullRequestBranch{ - Ref: *pr.Head.Ref, - SHA: *pr.Head.SHA, - RepoName: headRepoName, - OwnerName: *pr.Head.User.Login, - CloneURL: cloneURL, - }, - Base: base.PullRequestBranch{ - Ref: *pr.Base.Ref, - SHA: *pr.Base.SHA, - RepoName: *pr.Base.Repo.Name, - OwnerName: *pr.Base.User.Login, - }, - PatchURL: *pr.PatchURL, - }) - if len(allPRs) >= limit { - return allPRs, nil - } + var headRepoName string + var cloneURL string + if pr.Head.Repo != nil { + headRepoName = *pr.Head.Repo.Name + cloneURL = *pr.Head.Repo.CloneURL } - if resp.NextPage == 0 { - break + var mergeCommitSHA string + if pr.MergeCommitSHA != nil { + mergeCommitSHA = *pr.MergeCommitSHA } - opt.Page = resp.NextPage + + allPRs = append(allPRs, &base.PullRequest{ + Title: *pr.Title, + Number: int64(*pr.Number), + PosterName: *pr.User.Login, + PosterEmail: email, + Content: body, + Milestone: milestone, + State: *pr.State, + Created: *pr.CreatedAt, + Closed: pr.ClosedAt, + Labels: labels, + Merged: merged, + MergeCommitSHA: mergeCommitSHA, + MergedTime: pr.MergedAt, + IsLocked: pr.ActiveLockReason != nil, + Head: base.PullRequestBranch{ + Ref: *pr.Head.Ref, + SHA: *pr.Head.SHA, + RepoName: headRepoName, + OwnerName: *pr.Head.User.Login, + CloneURL: cloneURL, + }, + Base: base.PullRequestBranch{ + Ref: *pr.Base.Ref, + SHA: *pr.Base.SHA, + RepoName: *pr.Base.Repo.Name, + OwnerName: *pr.Base.User.Login, + }, + PatchURL: *pr.PatchURL, + }) } + return allPRs, nil } diff --git a/modules/migrations/github_test.go b/modules/migrations/github_test.go index e1d3efad588b8..c14292ecc6da0 100644 --- a/modules/migrations/github_test.go +++ b/modules/migrations/github_test.go @@ -166,9 +166,11 @@ func TestGitHubDownloadRepo(t *testing.T) { }, releases[len(releases)-1:]) // downloader.GetIssues() - issues, err := downloader.GetIssues(0, 3) + issues, isEnd, err := downloader.GetIssues(1, 8) assert.NoError(t, err) assert.EqualValues(t, 3, len(issues)) + assert.False(t, isEnd) + var ( closed1 = time.Date(2018, 10, 23, 02, 57, 43, 0, time.UTC) ) @@ -319,7 +321,7 @@ something like in the latest 15days could be enough don't you think ? }, comments[:3]) // downloader.GetPullRequests() - prs, err := downloader.GetPullRequests(0, 3) + prs, err := downloader.GetPullRequests(1, 3) assert.NoError(t, err) assert.EqualValues(t, 3, len(prs)) diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index ac55a2e7278a5..4b1229f9495c0 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -128,8 +128,8 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts if opts.Issues { log.Trace("migrating issues and comments") - for { - issues, err := downloader.GetIssues(0, 100) + for i := 1; ; i++ { + issues, isEnd, err := downloader.GetIssues(i, 100) if err != nil { return err } @@ -160,7 +160,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts } } - if len(issues) < 100 { + if isEnd { break } } @@ -168,8 +168,8 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts if opts.PullRequests { log.Trace("migrating pull requests and comments") - for { - prs, err := downloader.GetPullRequests(0, 100) + for i := 1; ; i++ { + prs, err := downloader.GetPullRequests(i, 100) if err != nil { return err } From 592924a34b8e671c93416c01a468b9aab0ab39aa Mon Sep 17 00:00:00 2001 From: "Robert A. Nowak" Date: Thu, 30 May 2019 23:23:16 +0200 Subject: [PATCH 39/44] Fix Erlang and Elixir highlight mappings (#7044) --- modules/highlight/highlight.go | 72 +++++++++++++++++----------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index fc9a4ad103d02..6d5e1a97ba769 100644 --- a/modules/highlight/highlight.go +++ b/modules/highlight/highlight.go @@ -26,45 +26,47 @@ var ( // Extensions that are same as highlight classes. highlightExts = map[string]struct{}{ - ".arm": {}, - ".as": {}, - ".sh": {}, - ".cs": {}, - ".cpp": {}, - ".c": {}, - ".css": {}, - ".cmake": {}, - ".bat": {}, - ".dart": {}, - ".patch": {}, - ".elixir": {}, - ".erlang": {}, - ".go": {}, - ".html": {}, - ".xml": {}, - ".hs": {}, - ".ini": {}, - ".json": {}, - ".java": {}, - ".js": {}, - ".less": {}, - ".lua": {}, - ".php": {}, - ".py": {}, - ".rb": {}, - ".scss": {}, - ".sql": {}, - ".scala": {}, - ".swift": {}, - ".ts": {}, - ".vb": {}, - ".yml": {}, - ".yaml": {}, + ".arm": {}, + ".as": {}, + ".sh": {}, + ".cs": {}, + ".cpp": {}, + ".c": {}, + ".css": {}, + ".cmake": {}, + ".bat": {}, + ".dart": {}, + ".patch": {}, + ".erl": {}, + ".go": {}, + ".html": {}, + ".xml": {}, + ".hs": {}, + ".ini": {}, + ".json": {}, + ".java": {}, + ".js": {}, + ".less": {}, + ".lua": {}, + ".php": {}, + ".py": {}, + ".rb": {}, + ".scss": {}, + ".sql": {}, + ".scala": {}, + ".swift": {}, + ".ts": {}, + ".vb": {}, + ".yml": {}, + ".yaml": {}, } // Extensions that are not same as highlight classes. highlightMapping = map[string]string{ - ".txt": "nohighlight", + ".txt": "nohighlight", + ".escript": "erlang", + ".ex": "elixir", + ".exs": "elixir", } ) From de6ef14d04c36272143ad822bf5903f84c7f238b Mon Sep 17 00:00:00 2001 From: mrsdizzie Date: Fri, 31 May 2019 05:21:15 -0400 Subject: [PATCH 40/44] Validate External Tracker URL Format (#7089) * Validate External Tracker URL Format Add some validation checks for external tracker URL format. Fixes #7068 * Don't make {index} a hard requirement * Fix Description * make fmt * move regex to package level * fix copyright date --- modules/validation/helpers.go | 19 +++++++++ modules/validation/helpers_test.go | 67 ++++++++++++++++++++++++++++++ routers/repo/setting.go | 2 +- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index 9a4dfab7a42d8..c22e667a2ebf9 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -7,6 +7,7 @@ package validation import ( "net" "net/url" + "regexp" "strings" "code.gitea.io/gitea/modules/setting" @@ -14,6 +15,8 @@ import ( var loopbackIPBlocks []*net.IPNet +var externalTrackerRegex = regexp.MustCompile(`({?)(?:user|repo|index)+?(}?)`) + func init() { for _, cidr := range []string{ "127.0.0.0/8", // IPv4 loopback @@ -75,3 +78,19 @@ func IsValidExternalURL(uri string) bool { return true } + +// IsValidExternalTrackerURLFormat checks if URL matches required syntax for external trackers +func IsValidExternalTrackerURLFormat(uri string) bool { + if !IsValidExternalURL(uri) { + return false + } + + // check for typoed variables like /{index/ or /[repo} + for _, match := range externalTrackerRegex.FindAllStringSubmatch(uri, -1) { + if (match[1] == "{" || match[2] == "}") && (match[1] != "{" || match[2] != "}") { + return false + } + } + + return true +} diff --git a/modules/validation/helpers_test.go b/modules/validation/helpers_test.go index 875625a02cdc0..9051ee1a0d19f 100644 --- a/modules/validation/helpers_test.go +++ b/modules/validation/helpers_test.go @@ -88,3 +88,70 @@ func Test_IsValidExternalURL(t *testing.T) { }) } } + +func Test_IsValidExternalTrackerURLFormat(t *testing.T) { + setting.AppURL = "https://try.gitea.io/" + + cases := []struct { + description string + url string + valid bool + }{ + { + description: "Correct external tracker URL with all placeholders", + url: "https://github.com/{user}/{repo}/issues/{index}", + valid: true, + }, + { + description: "Local external tracker URL with all placeholders", + url: "https://127.0.0.1/{user}/{repo}/issues/{index}", + valid: false, + }, + { + description: "External tracker URL with typo placeholder", + url: "https://github.com/{user}/{repo/issues/{index}", + valid: false, + }, + { + description: "External tracker URL with typo placeholder", + url: "https://github.com/[user}/{repo/issues/{index}", + valid: false, + }, + { + description: "External tracker URL with typo placeholder", + url: "https://github.com/{user}/repo}/issues/{index}", + valid: false, + }, + { + description: "External tracker URL missing optional placeholder", + url: "https://github.com/{user}/issues/{index}", + valid: true, + }, + { + description: "External tracker URL missing optional placeholder", + url: "https://github.com/{repo}/issues/{index}", + valid: true, + }, + { + description: "External tracker URL missing optional placeholder", + url: "https://github.com/issues/{index}", + valid: true, + }, + { + description: "External tracker URL missing optional placeholder", + url: "https://github.com/issues/{user}", + valid: true, + }, + { + description: "External tracker URL with similar placeholder names test", + url: "https://github.com/user/repo/issues/{index}", + valid: true, + }, + } + + for _, testCase := range cases { + t.Run(testCase.description, func(t *testing.T) { + assert.Equal(t, testCase.valid, IsValidExternalTrackerURLFormat(testCase.url)) + }) + } +} diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 07649982d28bd..767cdacde0195 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -249,7 +249,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { ctx.Redirect(repo.Link() + "/settings") return } - if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalURL(form.TrackerURLFormat) { + if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) { ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error")) ctx.Redirect(repo.Link() + "/settings") return From fb4438a815b013895f3171f8f2f1ed22f79596de Mon Sep 17 00:00:00 2001 From: zeripath Date: Fri, 31 May 2019 11:12:15 +0100 Subject: [PATCH 41/44] Improve git test (#7086) * Ensure that the lfs files are created with a different prefix * Reduce the replication in git_test.go --- .../api_helper_for_declarative_test.go | 42 ++ .../git_helper_for_declarative_test.go | 36 +- integrations/git_test.go | 426 +++++++++--------- 3 files changed, 280 insertions(+), 224 deletions(-) diff --git a/integrations/api_helper_for_declarative_test.go b/integrations/api_helper_for_declarative_test.go index 943981ead2f04..85f0ab621f846 100644 --- a/integrations/api_helper_for_declarative_test.go +++ b/integrations/api_helper_for_declarative_test.go @@ -5,11 +5,14 @@ package integrations import ( + "encoding/json" "fmt" "io/ioutil" "net/http" "testing" + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) @@ -150,3 +153,42 @@ func doAPICreateDeployKey(ctx APITestContext, keyname, keyFile string, readOnly ctx.Session.MakeRequest(t, req, http.StatusCreated) } } + +func doAPICreatePullRequest(ctx APITestContext, owner, repo, baseBranch, headBranch string) func(*testing.T) (api.PullRequest, error) { + return func(t *testing.T) (api.PullRequest, error) { + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", + owner, repo, ctx.Token) + req := NewRequestWithJSON(t, http.MethodPost, urlStr, &api.CreatePullRequestOption{ + Head: headBranch, + Base: baseBranch, + Title: fmt.Sprintf("create a pr from %s to %s", headBranch, baseBranch), + }) + + expected := 201 + if ctx.ExpectedCode != 0 { + expected = ctx.ExpectedCode + } + resp := ctx.Session.MakeRequest(t, req, expected) + decoder := json.NewDecoder(resp.Body) + pr := api.PullRequest{} + err := decoder.Decode(&pr) + return pr, err + } +} + +func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) { + return func(t *testing.T) { + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s", + owner, repo, index, ctx.Token) + req := NewRequestWithJSON(t, http.MethodPost, urlStr, &auth.MergePullRequestForm{ + MergeMessageField: "doAPIMergePullRequest Merge", + Do: string(models.MergeStyleMerge), + }) + + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + ctx.Session.MakeRequest(t, req, 200) + } +} diff --git a/integrations/git_helper_for_declarative_test.go b/integrations/git_helper_for_declarative_test.go index b4fead66253b0..235f4b4a9b74f 100644 --- a/integrations/git_helper_for_declarative_test.go +++ b/integrations/git_helper_for_declarative_test.go @@ -112,16 +112,44 @@ func doGitAddRemote(dstPath, remoteName string, u *url.URL) func(*testing.T) { } } -func doGitPushTestRepository(dstPath, remoteName, branch string) func(*testing.T) { +func doGitPushTestRepository(dstPath string, args ...string) func(*testing.T) { return func(t *testing.T) { - _, err := git.NewCommand("push", "-u", remoteName, branch).RunInDir(dstPath) + _, err := git.NewCommand(append([]string{"push", "-u"}, args...)...).RunInDir(dstPath) assert.NoError(t, err) } } -func doGitPushTestRepositoryFail(dstPath, remoteName, branch string) func(*testing.T) { +func doGitPushTestRepositoryFail(dstPath string, args ...string) func(*testing.T) { return func(t *testing.T) { - _, err := git.NewCommand("push", "-u", remoteName, branch).RunInDir(dstPath) + _, err := git.NewCommand(append([]string{"push"}, args...)...).RunInDir(dstPath) assert.Error(t, err) } } + +func doGitCreateBranch(dstPath, branch string) func(*testing.T) { + return func(t *testing.T) { + _, err := git.NewCommand("checkout", "-b", branch).RunInDir(dstPath) + assert.NoError(t, err) + } +} + +func doGitCheckoutBranch(dstPath string, args ...string) func(*testing.T) { + return func(t *testing.T) { + _, err := git.NewCommand(append([]string{"checkout"}, args...)...).RunInDir(dstPath) + assert.NoError(t, err) + } +} + +func doGitMerge(dstPath string, args ...string) func(*testing.T) { + return func(t *testing.T) { + _, err := git.NewCommand(append([]string{"merge"}, args...)...).RunInDir(dstPath) + assert.NoError(t, err) + } +} + +func doGitPull(dstPath string, args ...string) func(*testing.T) { + return func(t *testing.T) { + _, err := git.NewCommand(append([]string{"pull"}, args...)...).RunInDir(dstPath) + assert.NoError(t, err) + } +} diff --git a/integrations/git_test.go b/integrations/git_test.go index 0554f9a5aead0..ce5aee493d835 100644 --- a/integrations/git_test.go +++ b/integrations/git_test.go @@ -13,11 +13,13 @@ import ( "os" "path" "path/filepath" + "strconv" "testing" "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) @@ -43,119 +45,23 @@ func testGit(t *testing.T, u *url.URL) { httpContext.Reponame = "repo-tmp-17" dstPath, err := ioutil.TempDir("", httpContext.Reponame) - var little, big, littleLFS, bigLFS string - assert.NoError(t, err) defer os.RemoveAll(dstPath) - t.Run("Standard", func(t *testing.T) { - PrintCurrentTest(t) - ensureAnonymousClone(t, u) - - t.Run("CreateRepo", doAPICreateRepository(httpContext, false)) - - u.Path = httpContext.GitPath() - u.User = url.UserPassword(username, userPassword) - - t.Run("Clone", doGitClone(dstPath, u)) - - t.Run("PushCommit", func(t *testing.T) { - PrintCurrentTest(t) - t.Run("Little", func(t *testing.T) { - PrintCurrentTest(t) - little = commitAndPush(t, littleSize, dstPath) - }) - t.Run("Big", func(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode.") - return - } - PrintCurrentTest(t) - big = commitAndPush(t, bigSize, dstPath) - }) - }) - }) - t.Run("LFS", func(t *testing.T) { - PrintCurrentTest(t) - t.Run("PushCommit", func(t *testing.T) { - PrintCurrentTest(t) - //Setup git LFS - _, err = git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath) - assert.NoError(t, err) - _, err = git.NewCommand("lfs").AddArguments("track", "data-file-*").RunInDir(dstPath) - assert.NoError(t, err) - err = git.AddChanges(dstPath, false, ".gitattributes") - assert.NoError(t, err) - - t.Run("Little", func(t *testing.T) { - PrintCurrentTest(t) - littleLFS = commitAndPush(t, littleSize, dstPath) - lockFileTest(t, littleLFS, dstPath) - }) - t.Run("Big", func(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode.") - return - } - PrintCurrentTest(t) - bigLFS = commitAndPush(t, bigSize, dstPath) - lockFileTest(t, bigLFS, dstPath) - }) - }) - t.Run("Locks", func(t *testing.T) { - PrintCurrentTest(t) - lockTest(t, u.String(), dstPath) - }) - }) - t.Run("Raw", func(t *testing.T) { - PrintCurrentTest(t) - session := loginUser(t, "user2") - - // Request raw paths - req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", little)) - resp := session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Body.Len()) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", littleLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, littleSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) - - if !testing.Short() { - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", big)) - nilResp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, nilResp.Length) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", bigLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, bigSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) - } - - }) - t.Run("Media", func(t *testing.T) { - PrintCurrentTest(t) - session := loginUser(t, "user2") - // Request media paths - req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", little)) - resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Length) + t.Run("CreateRepo", doAPICreateRepository(httpContext, false)) + ensureAnonymousClone(t, u) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", littleLFS)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Length) + u.Path = httpContext.GitPath() + u.User = url.UserPassword(username, userPassword) - if !testing.Short() { - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", big)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Length) + t.Run("Clone", doGitClone(dstPath, u)) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", bigLFS)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Length) - } - }) + little, big := standardCommitAndPushTest(t, dstPath) + littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath) + rawTest(t, &httpContext, little, big, littleLFS, bigLFS) + mediaTest(t, &httpContext, little, big, littleLFS, bigLFS) + t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath)) }) t.Run("SSH", func(t *testing.T) { PrintCurrentTest(t) @@ -165,123 +71,26 @@ func testGit(t *testing.T, u *url.URL) { //Setup key the user ssh key withKeyFile(t, keyname, func(keyFile string) { t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile)) - PrintCurrentTest(t) //Setup remote link + //TODO: get url from api sshURL := createSSHUrl(sshContext.GitPath(), u) //Setup clone folder dstPath, err := ioutil.TempDir("", sshContext.Reponame) assert.NoError(t, err) defer os.RemoveAll(dstPath) - var little, big, littleLFS, bigLFS string - - t.Run("Standard", func(t *testing.T) { - PrintCurrentTest(t) - t.Run("CreateRepo", doAPICreateRepository(sshContext, false)) - - //TODO get url from api - t.Run("Clone", doGitClone(dstPath, sshURL)) - - //time.Sleep(5 * time.Minute) - t.Run("PushCommit", func(t *testing.T) { - PrintCurrentTest(t) - t.Run("Little", func(t *testing.T) { - PrintCurrentTest(t) - little = commitAndPush(t, littleSize, dstPath) - }) - t.Run("Big", func(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode.") - return - } - PrintCurrentTest(t) - big = commitAndPush(t, bigSize, dstPath) - }) - }) - }) - t.Run("LFS", func(t *testing.T) { - PrintCurrentTest(t) - t.Run("PushCommit", func(t *testing.T) { - PrintCurrentTest(t) - //Setup git LFS - _, err = git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath) - assert.NoError(t, err) - _, err = git.NewCommand("lfs").AddArguments("track", "data-file-*").RunInDir(dstPath) - assert.NoError(t, err) - err = git.AddChanges(dstPath, false, ".gitattributes") - assert.NoError(t, err) - - t.Run("Little", func(t *testing.T) { - PrintCurrentTest(t) - littleLFS = commitAndPush(t, littleSize, dstPath) - lockFileTest(t, littleLFS, dstPath) - - }) - t.Run("Big", func(t *testing.T) { - if testing.Short() { - return - } - PrintCurrentTest(t) - bigLFS = commitAndPush(t, bigSize, dstPath) - lockFileTest(t, bigLFS, dstPath) - - }) - }) - t.Run("Locks", func(t *testing.T) { - PrintCurrentTest(t) - lockTest(t, u.String(), dstPath) - }) - }) - t.Run("Raw", func(t *testing.T) { - PrintCurrentTest(t) - session := loginUser(t, "user2") - - // Request raw paths - req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", little)) - resp := session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Body.Len()) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", littleLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, littleSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) - - if !testing.Short() { - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", big)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Body.Len()) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", bigLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, bigSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) - } - }) - t.Run("Media", func(t *testing.T) { - PrintCurrentTest(t) - session := loginUser(t, "user2") - - // Request media paths - req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", little)) - resp := session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Body.Len()) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", littleLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Body.Len()) - - if !testing.Short() { - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", big)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Body.Len()) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", bigLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Body.Len()) - } - }) + t.Run("CreateRepo", doAPICreateRepository(sshContext, false)) + + t.Run("Clone", doGitClone(dstPath, sshURL)) + + little, big := standardCommitAndPushTest(t, dstPath) + littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath) + rawTest(t, &sshContext, little, big, littleLFS, bigLFS) + mediaTest(t, &sshContext, little, big, littleLFS, bigLFS) + + t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath)) }) }) @@ -295,7 +104,116 @@ func ensureAnonymousClone(t *testing.T, u *url.URL) { } -func lockTest(t *testing.T, remote, repoPath string) { +func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string) { + t.Run("Standard", func(t *testing.T) { + PrintCurrentTest(t) + little, big = commitAndPushTest(t, dstPath, "data-file-") + }) + return +} + +func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) { + t.Run("LFS", func(t *testing.T) { + PrintCurrentTest(t) + prefix := "lfs-data-file-" + _, err := git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath) + assert.NoError(t, err) + _, err = git.NewCommand("lfs").AddArguments("track", prefix+"*").RunInDir(dstPath) + assert.NoError(t, err) + err = git.AddChanges(dstPath, false, ".gitattributes") + assert.NoError(t, err) + + littleLFS, bigLFS = commitAndPushTest(t, dstPath, prefix) + + t.Run("Locks", func(t *testing.T) { + PrintCurrentTest(t) + lockTest(t, dstPath) + }) + }) + return +} + +func commitAndPushTest(t *testing.T, dstPath, prefix string) (little, big string) { + t.Run("PushCommit", func(t *testing.T) { + PrintCurrentTest(t) + t.Run("Little", func(t *testing.T) { + PrintCurrentTest(t) + little = doCommitAndPush(t, littleSize, dstPath, prefix) + }) + t.Run("Big", func(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test in short mode.") + return + } + PrintCurrentTest(t) + big = doCommitAndPush(t, bigSize, dstPath, prefix) + }) + }) + return +} + +func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) { + t.Run("Raw", func(t *testing.T) { + PrintCurrentTest(t) + username := ctx.Username + reponame := ctx.Reponame + + session := loginUser(t, username) + + // Request raw paths + req := NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", little)) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, littleSize, resp.Body.Len()) + + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEqual(t, littleSize, resp.Body.Len()) + assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + + if !testing.Short() { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Body.Len()) + + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEqual(t, bigSize, resp.Body.Len()) + assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + } + }) +} + +func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) { + t.Run("Media", func(t *testing.T) { + PrintCurrentTest(t) + + username := ctx.Username + reponame := ctx.Reponame + + session := loginUser(t, username) + + // Request media paths + req := NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", little)) + resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, littleSize, resp.Length) + + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, littleSize, resp.Length) + + if !testing.Short() { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Length) + + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Length) + } + }) +} + +func lockTest(t *testing.T, repoPath string) { lockFileTest(t, "README.md", repoPath) } @@ -310,22 +228,22 @@ func lockFileTest(t *testing.T, filename, repoPath string) { assert.NoError(t, err) } -func commitAndPush(t *testing.T, size int, repoPath string) string { - name, err := generateCommitWithNewData(size, repoPath, "user2@example.com", "User Two") +func doCommitAndPush(t *testing.T, size int, repoPath, prefix string) string { + name, err := generateCommitWithNewData(size, repoPath, "user2@example.com", "User Two", prefix) assert.NoError(t, err) - _, err = git.NewCommand("push").RunInDir(repoPath) //Push + _, err = git.NewCommand("push", "origin", "master").RunInDir(repoPath) //Push assert.NoError(t, err) return name } -func generateCommitWithNewData(size int, repoPath, email, fullName string) (string, error) { +func generateCommitWithNewData(size int, repoPath, email, fullName, prefix string) (string, error) { //Generate random file data := make([]byte, size) _, err := rand.Read(data) if err != nil { return "", err } - tmpFile, err := ioutil.TempFile(repoPath, "data-file-") + tmpFile, err := ioutil.TempFile(repoPath, prefix) if err != nil { return "", err } @@ -355,3 +273,71 @@ func generateCommitWithNewData(size int, repoPath, email, fullName string) (stri }) return filepath.Base(tmpFile.Name()), err } + +func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { + return func(t *testing.T) { + PrintCurrentTest(t) + t.Run("CreateBranchProtected", doGitCreateBranch(dstPath, "protected")) + t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected")) + + ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame) + t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + }) + t.Run("FailToPushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "origin", "protected")) + t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected")) + var pr api.PullRequest + var err error + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t) + assert.NoError(t, err) + }) + t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + t.Run("PullProtected", doGitPull(dstPath, "origin", "protected")) + t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username)) + + t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master")) + t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + }) + t.Run("FailToForcePushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected")) + t.Run("MergeProtectedToToforce", doGitMerge(dstPath, "protected")) + t.Run("PushToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "toforce:protected")) + t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master")) + } +} + +func doProtectBranch(ctx APITestContext, branch string, userToWhitelist string) func(t *testing.T) { + // We are going to just use the owner to set the protection. + return func(t *testing.T) { + csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings/branches", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame))) + + if userToWhitelist == "" { + // Change branch to protected + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{ + "_csrf": csrf, + "protected": "on", + }) + ctx.Session.MakeRequest(t, req, http.StatusFound) + } else { + user, err := models.GetUserByName(userToWhitelist) + assert.NoError(t, err) + // Change branch to protected + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{ + "_csrf": csrf, + "protected": "on", + "enable_whitelist": "on", + "whitelist_users": strconv.FormatInt(user.ID, 10), + }) + ctx.Session.MakeRequest(t, req, http.StatusFound) + } + // Check if master branch has been locked successfully + flashCookie := ctx.Session.GetCookie("macaron_flash") + assert.NotNil(t, flashCookie) + assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Bbranch%2B%2527"+url.QueryEscape(branch)+"%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) + } +} From 8a343dda39b187627db6ffb4c24a6e0ae615867b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 1 Jun 2019 03:34:46 +0800 Subject: [PATCH 42/44] update go git from v4.10.0 to v4.11.0 (#7096) --- go.mod | 2 +- go.sum | 4 +- vendor/gopkg.in/src-d/go-git.v4/.travis.yml | 2 +- vendor/gopkg.in/src-d/go-git.v4/options.go | 4 +- .../go-git.v4/plumbing/cache/object_lru.go | 5 +++ .../src-d/go-git.v4/plumbing/object/commit.go | 38 +++++++++++-------- .../plumbing/object/commit_walker_bfs.go | 2 +- .../src-d/go-git.v4/plumbing/object/patch.go | 11 +++++- .../src-d/go-git.v4/plumbing/object/tree.go | 2 +- vendor/gopkg.in/src-d/go-git.v4/remote.go | 9 ++++- vendor/gopkg.in/src-d/go-git.v4/repository.go | 17 +++++++++ .../src-d/go-git.v4/utils/diff/diff.go | 18 ++++++++- vendor/gopkg.in/src-d/go-git.v4/worktree.go | 11 ------ .../src-d/go-git.v4/worktree_status.go | 6 ++- vendor/modules.txt | 2 +- 15 files changed, 93 insertions(+), 40 deletions(-) diff --git a/go.mod b/go.mod index 299a4b29f9494..c6ebe16039338 100644 --- a/go.mod +++ b/go.mod @@ -130,7 +130,7 @@ require ( gopkg.in/macaron.v1 v1.3.2 gopkg.in/redis.v2 v2.3.2 // indirect gopkg.in/src-d/go-billy.v4 v4.3.0 - gopkg.in/src-d/go-git.v4 v4.10.0 + gopkg.in/src-d/go-git.v4 v4.11.0 gopkg.in/testfixtures.v2 v2.5.0 mvdan.cc/xurls/v2 v2.0.0 strk.kbt.io/projects/go/libravatar v0.0.0-20160628055650-5eed7bff870a diff --git a/go.sum b/go.sum index 94d332cbc955d..81b3aae5d456e 100644 --- a/go.sum +++ b/go.sum @@ -380,8 +380,8 @@ gopkg.in/src-d/go-billy.v4 v4.3.0 h1:KtlZ4c1OWbIs4jCv5ZXrTqG8EQocr0g/d4DjNg70aek gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= gopkg.in/src-d/go-git-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs= gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.10.0 h1:NWjTJTQnk8UpIGlssuefyDZ6JruEjo5s88vm88uASbw= -gopkg.in/src-d/go-git.v4 v4.10.0/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk= +gopkg.in/src-d/go-git.v4 v4.11.0 h1:cJwWgJ0DXifrNrXM6RGN1Y2yR60Rr1zQ9Q5DX5S9qgU= +gopkg.in/src-d/go-git.v4 v4.11.0/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk= gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= gopkg.in/testfixtures.v2 v2.5.0 h1:N08B7l2GzFQenyYbzqthDnKAA+cmb17iAZhhFxr7JHw= diff --git a/vendor/gopkg.in/src-d/go-git.v4/.travis.yml b/vendor/gopkg.in/src-d/go-git.v4/.travis.yml index c68b5f473e0aa..3a65f3e08212e 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/.travis.yml +++ b/vendor/gopkg.in/src-d/go-git.v4/.travis.yml @@ -1,8 +1,8 @@ language: go go: - - "1.10" - "1.11" + - "1.12" go_import_path: gopkg.in/src-d/go-git.v4 diff --git a/vendor/gopkg.in/src-d/go-git.v4/options.go b/vendor/gopkg.in/src-d/go-git.v4/options.go index ed7689ab3f894..7c9e68728827d 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/options.go +++ b/vendor/gopkg.in/src-d/go-git.v4/options.go @@ -229,7 +229,7 @@ var ( ErrCreateRequiresBranch = errors.New("Branch is mandatory when Create is used") ) -// CheckoutOptions describes how a checkout 31operation should be performed. +// CheckoutOptions describes how a checkout operation should be performed. type CheckoutOptions struct { // Hash is the hash of the commit to be checked out. If used, HEAD will be // in detached mode. If Create is not used, Branch and Hash are mutually @@ -288,7 +288,7 @@ const ( // ResetOptions describes how a reset operation should be performed. type ResetOptions struct { - // Commit, if commit is pressent set the current branch head (HEAD) to it. + // Commit, if commit is present set the current branch head (HEAD) to it. Commit plumbing.Hash // Mode, form resets the current branch head to Commit and possibly updates // the index (resetting it to the tree of Commit) and the working tree diff --git a/vendor/gopkg.in/src-d/go-git.v4/plumbing/cache/object_lru.go b/vendor/gopkg.in/src-d/go-git.v4/plumbing/cache/object_lru.go index 53d8b02d961c7..cd3712b7d7b39 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/plumbing/cache/object_lru.go +++ b/vendor/gopkg.in/src-d/go-git.v4/plumbing/cache/object_lru.go @@ -61,6 +61,11 @@ func (c *ObjectLRU) Put(obj plumbing.EncodedObject) { c.actualSize += objSize for c.actualSize > c.MaxSize { last := c.ll.Back() + if last == nil { + c.actualSize = 0 + break + } + lastObj := last.Value.(plumbing.EncodedObject) lastSize := FileSize(lastObj.Size()) diff --git a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit.go b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit.go index e2543426ac5a4..b569d3ce2db1d 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit.go +++ b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit.go @@ -76,8 +76,8 @@ func (c *Commit) Tree() (*Tree, error) { return GetTree(c.s, c.TreeHash) } -// Patch returns the Patch between the actual commit and the provided one. -// Error will be return if context expires. Provided context must be non-nil +// PatchContext returns the Patch between the actual commit and the provided one. +// Error will be return if context expires. Provided context must be non-nil. func (c *Commit) PatchContext(ctx context.Context, to *Commit) (*Patch, error) { fromTree, err := c.Tree() if err != nil { @@ -291,25 +291,33 @@ func (b *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) { return err } -// Stats shows the status of commit. +// Stats returns the stats of a commit. func (c *Commit) Stats() (FileStats, error) { - // Get the previous commit. - ci := c.Parents() - parentCommit, err := ci.Next() + return c.StatsContext(context.Background()) +} + +// StatsContext returns the stats of a commit. Error will be return if context +// expires. Provided context must be non-nil. +func (c *Commit) StatsContext(ctx context.Context) (FileStats, error) { + fromTree, err := c.Tree() if err != nil { - if err == io.EOF { - emptyNoder := treeNoder{} - parentCommit = &Commit{ - Hash: emptyNoder.hash, - // TreeHash: emptyNoder.parent.Hash, - s: c.s, - } - } else { + return nil, err + } + + toTree := &Tree{} + if c.NumParents() != 0 { + firstParent, err := c.Parents().Next() + if err != nil { + return nil, err + } + + toTree, err = firstParent.Tree() + if err != nil { return nil, err } } - patch, err := parentCommit.Patch(c) + patch, err := toTree.PatchContext(ctx, fromTree) if err != nil { return nil, err } diff --git a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit_walker_bfs.go b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit_walker_bfs.go index aef1cf24c6990..dabfe75c27b8e 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit_walker_bfs.go +++ b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit_walker_bfs.go @@ -67,7 +67,7 @@ func (w *bfsCommitIterator) Next() (*Commit, error) { for _, h := range c.ParentHashes { err := w.appendHash(c.s, h) if err != nil { - return nil, nil + return nil, err } } diff --git a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go index adeaccb0a86a6..068589eff8e1e 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go +++ b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go @@ -320,11 +320,18 @@ func getFileStatsFromFilePatches(filePatches []fdiff.FilePatch) FileStats { } for _, chunk := range fp.Chunks() { + s := chunk.Content() switch chunk.Type() { case fdiff.Add: - cs.Addition += strings.Count(chunk.Content(), "\n") + cs.Addition += strings.Count(s, "\n") + if s[len(s)-1] != '\n' { + cs.Addition++ + } case fdiff.Delete: - cs.Deletion += strings.Count(chunk.Content(), "\n") + cs.Deletion += strings.Count(s, "\n") + if s[len(s)-1] != '\n' { + cs.Deletion++ + } } } diff --git a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/tree.go b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/tree.go index 78d61a1fba546..1f9ea2651c517 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/tree.go +++ b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/tree.go @@ -135,7 +135,7 @@ func (t *Tree) FindEntry(path string) (*TreeEntry, error) { pathCurrent := "" // search for the longest path in the tree path cache - for i := len(pathParts); i > 1; i-- { + for i := len(pathParts) - 1; i > 1; i-- { path := filepath.Join(pathParts[:i]...) tree, ok := t.t[path] diff --git a/vendor/gopkg.in/src-d/go-git.v4/remote.go b/vendor/gopkg.in/src-d/go-git.v4/remote.go index de537ce8e8bcd..80604092ab17e 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/remote.go +++ b/vendor/gopkg.in/src-d/go-git.v4/remote.go @@ -1020,7 +1020,12 @@ func pushHashes( if err != nil { return nil, err } - done := make(chan error) + + // Set buffer size to 1 so the error message can be written when + // ReceivePack fails. Otherwise the goroutine will be blocked writing + // to the channel. + done := make(chan error, 1) + go func() { e := packfile.NewEncoder(wr, s, useRefDeltas) if _, err := e.Encode(hs, config.Pack.Window); err != nil { @@ -1033,6 +1038,8 @@ func pushHashes( rs, err := sess.ReceivePack(ctx, req) if err != nil { + // close the pipe to unlock encode write + _ = rd.Close() return nil, err } diff --git a/vendor/gopkg.in/src-d/go-git.v4/repository.go b/vendor/gopkg.in/src-d/go-git.v4/repository.go index de92d647099df..e5b12b0c52f17 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/repository.go +++ b/vendor/gopkg.in/src-d/go-git.v4/repository.go @@ -49,6 +49,7 @@ var ( ErrRepositoryAlreadyExists = errors.New("repository already exists") ErrRemoteNotFound = errors.New("remote not found") ErrRemoteExists = errors.New("remote already exists") + ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'") ErrWorktreeNotProvided = errors.New("worktree should be provided") ErrIsBareRepository = errors.New("worktree not available in a bare repository") ErrUnableToResolveCommit = errors.New("unable to resolve commit") @@ -492,6 +493,22 @@ func (r *Repository) CreateRemote(c *config.RemoteConfig) (*Remote, error) { return remote, r.Storer.SetConfig(cfg) } +// CreateRemoteAnonymous creates a new anonymous remote. c.Name must be "anonymous". +// It's used like 'git fetch git@github.com:src-d/go-git.git master:master'. +func (r *Repository) CreateRemoteAnonymous(c *config.RemoteConfig) (*Remote, error) { + if err := c.Validate(); err != nil { + return nil, err + } + + if c.Name != "anonymous" { + return nil, ErrAnonymousRemoteName + } + + remote := newRemote(r.Storer, c) + + return remote, nil +} + // DeleteRemote delete a remote from the repository and delete the config func (r *Repository) DeleteRemote(name string) error { cfg, err := r.Storer.Config() diff --git a/vendor/gopkg.in/src-d/go-git.v4/utils/diff/diff.go b/vendor/gopkg.in/src-d/go-git.v4/utils/diff/diff.go index f49ae55baeb4a..6142ed051550a 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/utils/diff/diff.go +++ b/vendor/gopkg.in/src-d/go-git.v4/utils/diff/diff.go @@ -8,14 +8,30 @@ package diff import ( "bytes" + "time" "github.com/sergi/go-diff/diffmatchpatch" ) // Do computes the (line oriented) modifications needed to turn the src -// string into the dst string. +// string into the dst string. The underlying algorithm is Meyers, +// its complexity is O(N*d) where N is min(lines(src), lines(dst)) and d +// is the size of the diff. func Do(src, dst string) (diffs []diffmatchpatch.Diff) { + // the default timeout is time.Second which may be too small under heavy load + return DoWithTimeout(src, dst, time.Hour) +} + +// DoWithTimeout computes the (line oriented) modifications needed to turn the src +// string into the dst string. The `timeout` argument specifies the maximum +// amount of time it is allowed to spend in this function. If the timeout +// is exceeded, the parts of the strings which were not considered are turned into +// a bulk delete+insert and the half-baked suboptimal result is returned at once. +// The underlying algorithm is Meyers, its complexity is O(N*d) where N is +// min(lines(src), lines(dst)) and d is the size of the diff. +func DoWithTimeout (src, dst string, timeout time.Duration) (diffs []diffmatchpatch.Diff) { dmp := diffmatchpatch.New() + dmp.DiffTimeout = timeout wSrc, wDst, warray := dmp.DiffLinesToRunes(src, dst) diffs = dmp.DiffMainRunes(wSrc, wDst, false) diffs = dmp.DiffCharsToLines(diffs, warray) diff --git a/vendor/gopkg.in/src-d/go-git.v4/worktree.go b/vendor/gopkg.in/src-d/go-git.v4/worktree.go index a14fd8d6c34e5..dae40a38a7163 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/worktree.go +++ b/vendor/gopkg.in/src-d/go-git.v4/worktree.go @@ -152,17 +152,6 @@ func (w *Worktree) Checkout(opts *CheckoutOptions) error { } } - if !opts.Force { - unstaged, err := w.containsUnstagedChanges() - if err != nil { - return err - } - - if unstaged { - return ErrUnstagedChanges - } - } - c, err := w.getCommitFromCheckoutOptions(opts) if err != nil { return err diff --git a/vendor/gopkg.in/src-d/go-git.v4/worktree_status.go b/vendor/gopkg.in/src-d/go-git.v4/worktree_status.go index 0e113d0937632..16ce937077c82 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/worktree_status.go +++ b/vendor/gopkg.in/src-d/go-git.v4/worktree_status.go @@ -142,12 +142,16 @@ func (w *Worktree) diffStagingWithWorktree(reverse bool) (merkletrie.Changes, er func (w *Worktree) excludeIgnoredChanges(changes merkletrie.Changes) merkletrie.Changes { patterns, err := gitignore.ReadPatterns(w.Filesystem, nil) - if err != nil || len(patterns) == 0 { + if err != nil { return changes } patterns = append(patterns, w.Excludes...) + if len(patterns) == 0 { + return changes + } + m := gitignore.NewMatcher(patterns) var res merkletrie.Changes diff --git a/vendor/modules.txt b/vendor/modules.txt index 0085f7bbdadd9..9f9ae9b4fbe66 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -422,7 +422,7 @@ gopkg.in/src-d/go-billy.v4 gopkg.in/src-d/go-billy.v4/util gopkg.in/src-d/go-billy.v4/helper/chroot gopkg.in/src-d/go-billy.v4/helper/polyfill -# gopkg.in/src-d/go-git.v4 v4.10.0 +# gopkg.in/src-d/go-git.v4 v4.11.0 gopkg.in/src-d/go-git.v4 gopkg.in/src-d/go-git.v4/config gopkg.in/src-d/go-git.v4/plumbing From 356854fc5f8d7d1a7e4d68c9e00929e9ce8aa867 Mon Sep 17 00:00:00 2001 From: zeripath Date: Sat, 1 Jun 2019 16:00:21 +0100 Subject: [PATCH 43/44] Move serv hook functionality & drop GitLogger (#6993) * Move hook functionality internally * Internalise serv logic * Remove old internal paths * finally remove the gitlogger * Disallow push on archived repositories * fix lint error * Update modules/private/key.go * Update routers/private/hook.go * Update routers/private/hook.go * Update routers/private/hook.go * Updated routers/private/serv.go * Fix LFS Locks over SSH * rev-list needs to be run by the hook process * fixup * Improve git test * Ensure that the lfs files are created with a different prefix * Reduce the replication in git_test.go * slight refactor * Remove unnecessary "/" * Restore ensureAnonymousClone * Restore ensureAnonymousClone * Run rev-list on server side * Try passing in the alternative directories instead * Mark test as skipped * Improve git test * Ensure that the lfs files are created with a different prefix * Reduce the replication in git_test.go * Remove unnecessary "/" --- cmd/hook.go | 125 +++----- cmd/serv.go | 173 +++-------- .../advanced/logging-documentation.en-us.md | 16 - integrations/internal_test.go | 44 --- models/helper_environment.go | 2 +- modules/log/log.go | 19 +- modules/pprof/pprof.go | 16 +- modules/private/branch.go | 67 ---- modules/private/hook.go | 84 +++++ modules/private/internal.go | 49 --- modules/private/key.go | 118 +------- modules/private/push_update.go | 40 --- modules/private/repository.go | 68 ----- modules/private/serv.go | 106 +++++++ modules/private/wiki.go | 33 -- routers/init.go | 2 - routers/private/branch.go | 52 ---- routers/private/hook.go | 209 +++++++++++++ routers/private/internal.go | 19 +- routers/private/key.go | 78 +---- routers/private/push_update.go | 47 --- routers/private/repository.go | 83 ----- routers/private/serv.go | 286 ++++++++++++++++++ routers/private/wiki.go | 34 --- routers/repo/http.go | 8 +- 25 files changed, 801 insertions(+), 977 deletions(-) delete mode 100644 integrations/internal_test.go delete mode 100644 modules/private/branch.go create mode 100644 modules/private/hook.go delete mode 100644 modules/private/push_update.go delete mode 100644 modules/private/repository.go create mode 100644 modules/private/serv.go delete mode 100644 modules/private/wiki.go delete mode 100644 routers/private/branch.go create mode 100644 routers/private/hook.go delete mode 100644 routers/private/push_update.go delete mode 100644 routers/private/repository.go create mode 100644 routers/private/serv.go delete mode 100644 routers/private/wiki.go diff --git a/cmd/hook.go b/cmd/hook.go index f8bd34c4e995e..b3e900afee4d5 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -8,15 +8,14 @@ import ( "bufio" "bytes" "fmt" + "net/http" "os" "strconv" "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" - "code.gitea.io/gitea/modules/util" "github.com/urfave/cli" ) @@ -62,12 +61,10 @@ func runHookPreReceive(c *cli.Context) error { setup("hooks/pre-receive.log") // the environment setted on serv command - repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64) isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true") username := os.Getenv(models.EnvRepoUsername) reponame := os.Getenv(models.EnvRepoName) - userIDStr := os.Getenv(models.EnvPusherID) - repoPath := models.RepoPath(username, reponame) + userID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64) buf := bytes.NewBuffer(nil) scanner := bufio.NewScanner(os.Stdin) @@ -91,35 +88,19 @@ func runHookPreReceive(c *cli.Context) error { // If the ref is a branch, check if it's protected if strings.HasPrefix(refFullName, git.BranchPrefix) { - branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) - protectBranch, err := private.GetProtectedBranchBy(repoID, branchName) - if err != nil { - fail("Internal error", fmt.Sprintf("retrieve protected branches information failed: %v", err)) - } - - if protectBranch != nil && protectBranch.IsProtected() { - // check and deletion - if newCommitID == git.EmptySHA { - fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "") - } - - // detect force push - if git.EmptySHA != oldCommitID { - output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDir(repoPath) - if err != nil { - fail("Internal error", "Fail to detect force push: %v", err) - } else if len(output) > 0 { - fail(fmt.Sprintf("branch %s is protected from force push", branchName), "") - } - } - - userID, _ := strconv.ParseInt(userIDStr, 10, 64) - canPush, err := private.CanUserPush(protectBranch.ID, userID) - if err != nil { - fail("Internal error", "Fail to detect user can push: %v", err) - } else if !canPush { - fail(fmt.Sprintf("protected branch %s can not be pushed to", branchName), "") - } + statusCode, msg := private.HookPreReceive(username, reponame, private.HookOptions{ + OldCommitID: oldCommitID, + NewCommitID: newCommitID, + RefFullName: refFullName, + UserID: userID, + GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories), + GitObjectDirectory: os.Getenv(private.GitObjectDirectory), + }) + switch statusCode { + case http.StatusInternalServerError: + fail("Internal Server Error", msg) + case http.StatusForbidden: + fail(msg, "") } } } @@ -145,7 +126,6 @@ func runHookPostReceive(c *cli.Context) error { setup("hooks/post-receive.log") // the environment setted on serv command - repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64) repoUser := os.Getenv(models.EnvRepoUsername) isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true") repoName := os.Getenv(models.EnvRepoName) @@ -172,64 +152,31 @@ func runHookPostReceive(c *cli.Context) error { newCommitID := string(fields[1]) refFullName := string(fields[2]) - // Only trigger activity updates for changes to branches or - // tags. Updates to other refs (eg, refs/notes, refs/changes, - // or other less-standard refs spaces are ignored since there - // may be a very large number of them). - if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { - if err := private.PushUpdate(models.PushUpdateOptions{ - RefFullName: refFullName, - OldCommitID: oldCommitID, - NewCommitID: newCommitID, - PusherID: pusherID, - PusherName: pusherName, - RepoUserName: repoUser, - RepoName: repoName, - }); err != nil { - log.GitLogger.Error("Update: %v", err) - } - } - - if newCommitID != git.EmptySHA && strings.HasPrefix(refFullName, git.BranchPrefix) { - branch := strings.TrimPrefix(refFullName, git.BranchPrefix) - repo, pullRequestAllowed, err := private.GetRepository(repoID) - if err != nil { - log.GitLogger.Error("get repo: %v", err) - break - } - if !pullRequestAllowed { - break - } - - baseRepo := repo - if repo.IsFork { - baseRepo = repo.BaseRepo - } - - if !repo.IsFork && branch == baseRepo.DefaultBranch { - break - } + res, err := private.HookPostReceive(repoUser, repoName, private.HookOptions{ + OldCommitID: oldCommitID, + NewCommitID: newCommitID, + RefFullName: refFullName, + UserID: pusherID, + UserName: pusherName, + }) - pr, err := private.ActivePullRequest(baseRepo.ID, repo.ID, baseRepo.DefaultBranch, branch) - if err != nil { - log.GitLogger.Error("get active pr: %v", err) - break - } + if res == nil { + fail("Internal Server Error", err) + } - fmt.Fprintln(os.Stderr, "") - if pr == nil { - if repo.IsFork { - branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch) - } - fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", branch) - fmt.Fprintf(os.Stderr, " %s/compare/%s...%s\n", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)) - } else { - fmt.Fprint(os.Stderr, "Visit the existing pull request:\n") - fmt.Fprintf(os.Stderr, " %s/pulls/%d\n", baseRepo.HTMLURL(), pr.Index) - } - fmt.Fprintln(os.Stderr, "") + if res["message"] == false { + continue } + fmt.Fprintln(os.Stderr, "") + if res["create"] == true { + fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", res["branch"]) + fmt.Fprintf(os.Stderr, " %s\n", res["url"]) + } else { + fmt.Fprint(os.Stderr, "Visit the existing pull request:\n") + fmt.Fprintf(os.Stderr, " %s\n", res["url"]) + } + fmt.Fprintln(os.Stderr, "") } return nil diff --git a/cmd/serv.go b/cmd/serv.go index a30e02e7a264f..aa068d4cf693f 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -8,9 +8,11 @@ package cmd import ( "encoding/json" "fmt" + "net/http" + "net/url" "os" "os/exec" - "path/filepath" + "strconv" "strings" "time" @@ -68,7 +70,6 @@ func setup(logPath string) { log.DelLogger("console") setting.NewContext() checkLFSVersion() - log.NewGitLogger(filepath.Join(setting.LogRootPath, logPath)) } func parseCmd(cmd string) (string, string) { @@ -95,15 +96,14 @@ func fail(userMessage, logMessage string, args ...interface{}) { if !setting.ProdMode { fmt.Fprintf(os.Stderr, logMessage+"\n", args...) } - log.GitLogger.Fatal(logMessage, args...) return } - log.GitLogger.Close() os.Exit(1) } func runServ(c *cli.Context) error { + // FIXME: This needs to internationalised setup("serv.log") if setting.SSH.Disabled { @@ -116,9 +116,23 @@ func runServ(c *cli.Context) error { return nil } + keys := strings.Split(c.Args()[0], "-") + if len(keys) != 2 || keys[0] != "key" { + fail("Key ID format error", "Invalid key argument: %s", c.Args()[0]) + } + keyID := com.StrTo(keys[1]).MustInt64() + cmd := os.Getenv("SSH_ORIGINAL_COMMAND") if len(cmd) == 0 { - println("Hi there, You've successfully authenticated, but Gitea does not provide shell access.") + key, user, err := private.ServNoCommand(keyID) + if err != nil { + fail("Internal error", "Failed to check provided key: %v", err) + } + if key.Type == models.KeyTypeDeploy { + println("Hi there! You've successfully authenticated with the deploy key named " + key.Name + ", but Gitea does not provide shell access.") + } else { + println("Hi there: " + user.Name + "! You've successfully authenticated with the key named " + key.Name + ", but Gitea does not provide shell access.") + } println("If this is unexpected, please log in with password and setup Gitea under another user.") return nil } @@ -152,41 +166,19 @@ func runServ(c *cli.Context) error { fail("Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err) } - stopCPUProfiler := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username) + stopCPUProfiler, err := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username) + if err != nil { + fail("Internal Server Error", "Unable to start CPU profile: %v", err) + } defer func() { stopCPUProfiler() - pprof.DumpMemProfileForUsername(setting.PprofDataPath, username) + err := pprof.DumpMemProfileForUsername(setting.PprofDataPath, username) + if err != nil { + fail("Internal Server Error", "Unable to dump Mem Profile: %v", err) + } }() } - var ( - isWiki bool - unitType = models.UnitTypeCode - unitName = "code" - ) - if strings.HasSuffix(reponame, ".wiki") { - isWiki = true - unitType = models.UnitTypeWiki - unitName = "wiki" - reponame = reponame[:len(reponame)-5] - } - - os.Setenv(models.EnvRepoUsername, username) - if isWiki { - os.Setenv(models.EnvRepoIsWiki, "true") - } else { - os.Setenv(models.EnvRepoIsWiki, "false") - } - os.Setenv(models.EnvRepoName, reponame) - - repo, err := private.GetRepositoryByOwnerAndName(username, reponame) - if err != nil { - if strings.Contains(err.Error(), "Failed to get repository: repository does not exist") { - fail(accessDenied, "Repository does not exist: %s/%s", username, reponame) - } - fail("Internal error", "Failed to get repository: %v", err) - } - requestedMode, has := allowedCommands[verb] if !has { fail("Unknown git command", "Unknown git command %s", verb) @@ -202,97 +194,36 @@ func runServ(c *cli.Context) error { } } - // Prohibit push to mirror repositories. - if requestedMode > models.AccessModeRead && repo.IsMirror { - fail("mirror repository is read-only", "") - } - - // Allow anonymous clone for public repositories. - var ( - keyID int64 - user *models.User - ) - if requestedMode == models.AccessModeWrite || repo.IsPrivate || setting.Service.RequireSignInView { - keys := strings.Split(c.Args()[0], "-") - if len(keys) != 2 { - fail("Key ID format error", "Invalid key argument: %s", c.Args()[0]) - } - - key, err := private.GetPublicKeyByID(com.StrTo(keys[1]).MustInt64()) - if err != nil { - fail("Invalid key ID", "Invalid key ID[%s]: %v", c.Args()[0], err) - } - keyID = key.ID - - // Check deploy key or user key. - if key.Type == models.KeyTypeDeploy { - // Now we have to get the deploy key for this repo - deployKey, err := private.GetDeployKey(key.ID, repo.ID) - if err != nil { - fail("Key access denied", "Failed to access internal api: [key_id: %d, repo_id: %d]", key.ID, repo.ID) - } - - if deployKey == nil { - fail("Key access denied", "Deploy key access denied: [key_id: %d, repo_id: %d]", key.ID, repo.ID) - } - - if deployKey.Mode < requestedMode { - fail("Key permission denied", "Cannot push with read-only deployment key: %d to repo_id: %d", key.ID, repo.ID) - } - - // Update deploy key activity. - if err = private.UpdateDeployKeyUpdated(key.ID, repo.ID); err != nil { - fail("Internal error", "UpdateDeployKey: %v", err) - } - - // FIXME: Deploy keys aren't really the owner of the repo pushing changes - // however we don't have good way of representing deploy keys in hook.go - // so for now use the owner - os.Setenv(models.EnvPusherName, username) - os.Setenv(models.EnvPusherID, fmt.Sprintf("%d", repo.OwnerID)) - } else { - user, err = private.GetUserByKeyID(key.ID) - if err != nil { - fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err) - } - - if !user.IsActive || user.ProhibitLogin { - fail("Your account is not active or has been disabled by Administrator", - "User %s is disabled and have no access to repository %s", - user.Name, repoPath) - } - - mode, err := private.CheckUnitUser(user.ID, repo.ID, user.IsAdmin, unitType) - if err != nil { - fail("Internal error", "Failed to check access: %v", err) - } else if *mode < requestedMode { - clientMessage := accessDenied - if *mode >= models.AccessModeRead { - clientMessage = "You do not have sufficient authorization for this action" - } - fail(clientMessage, - "User %s does not have level %v access to repository %s's "+unitName, - user.Name, requestedMode, repoPath) + results, err := private.ServCommand(keyID, username, reponame, requestedMode, verb, lfsVerb) + if err != nil { + if private.IsErrServCommand(err) { + errServCommand := err.(private.ErrServCommand) + if errServCommand.StatusCode != http.StatusInternalServerError { + fail("Unauthorized", errServCommand.Error()) + } else { + fail("Internal Server Error", errServCommand.Error()) } - - os.Setenv(models.EnvPusherName, user.Name) - os.Setenv(models.EnvPusherID, fmt.Sprintf("%d", user.ID)) } + fail("Internal Server Error", err.Error()) } + os.Setenv(models.EnvRepoIsWiki, strconv.FormatBool(results.IsWiki)) + os.Setenv(models.EnvRepoName, results.RepoName) + os.Setenv(models.EnvRepoUsername, results.OwnerName) + os.Setenv(models.EnvPusherName, username) + os.Setenv(models.EnvPusherID, strconv.FormatInt(results.UserID, 10)) + os.Setenv(models.ProtectedBranchRepoID, strconv.FormatInt(results.RepoID, 10)) //LFS token authentication if verb == lfsAuthenticateVerb { - url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, username, repo.Name) + url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName)) now := time.Now() claims := jwt.MapClaims{ - "repo": repo.ID, + "repo": results.RepoID, "op": lfsVerb, "exp": now.Add(setting.LFS.HTTPAuthExpiry).Unix(), "nbf": now.Unix(), - } - if user != nil { - claims["user"] = user.ID + "user": results.UserID, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) @@ -313,7 +244,6 @@ func runServ(c *cli.Context) error { if err != nil { fail("Internal error", "Failed to encode LFS json response: %v", err) } - return nil } @@ -329,13 +259,8 @@ func runServ(c *cli.Context) error { } else { gitcmd = exec.Command(verb, repoPath) } - if isWiki { - if err = private.InitWiki(repo.ID); err != nil { - fail("Internal error", "Failed to init wiki repo: %v", err) - } - } - os.Setenv(models.ProtectedBranchRepoID, fmt.Sprintf("%d", repo.ID)) + os.Setenv(models.ProtectedBranchRepoID, fmt.Sprintf("%d", results.RepoID)) gitcmd.Dir = setting.RepoRootPath gitcmd.Stdout = os.Stdout @@ -346,9 +271,9 @@ func runServ(c *cli.Context) error { } // Update user key activity. - if keyID > 0 { - if err = private.UpdatePublicKeyUpdated(keyID); err != nil { - fail("Internal error", "UpdatePublicKey: %v", err) + if results.KeyID > 0 { + if err = private.UpdatePublicKeyInRepo(results.KeyID, results.RepoID); err != nil { + fail("Internal error", "UpdatePublicKeyInRepo: %v", err) } } diff --git a/docs/content/doc/advanced/logging-documentation.en-us.md b/docs/content/doc/advanced/logging-documentation.en-us.md index df3578694346f..790e750084efd 100644 --- a/docs/content/doc/advanced/logging-documentation.en-us.md +++ b/docs/content/doc/advanced/logging-documentation.en-us.md @@ -27,7 +27,6 @@ log groups: * The Router logger * The Access logger * The XORM logger -* A logger called the `GitLogger` which is used during hooks. There is also the go log logger. @@ -180,21 +179,6 @@ which will not be inherited from the `[log]` or relevant * `EXPRESSION` will default to `""` * `PREFIX` will default to `""` -### The Hook and Serv "GitLoggers" - -These are less well defined loggers. Essentially these should only be -used within Gitea's subsystems and cannot be configured at present. - -They will write log files in: - -* `%(ROOT_PATH)/hooks/pre-receive.log` -* `%(ROOT_PATH)/hooks/update.log` -* `%(ROOT_PATH)/hooks/post-receive.log` -* `%(ROOT_PATH)/serv.log` -* `%(ROOT_PATH)/http.log` - -In the future these logs may be rationalised. - ## Log outputs Gitea provides 4 possible log outputs: diff --git a/integrations/internal_test.go b/integrations/internal_test.go deleted file mode 100644 index ee0c0d18f157a..0000000000000 --- a/integrations/internal_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2017 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 ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" - - "github.com/stretchr/testify/assert" -) - -func assertProtectedBranch(t *testing.T, repoID int64, branchName string, isErr, canPush bool) { - reqURL := fmt.Sprintf("/api/internal/branch/%d/%s", repoID, util.PathEscapeSegments(branchName)) - req := NewRequest(t, "GET", reqURL) - t.Log(reqURL) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)) - - resp := MakeRequest(t, req, NoExpectedStatus) - if isErr { - assert.EqualValues(t, http.StatusInternalServerError, resp.Code) - } else { - assert.EqualValues(t, http.StatusOK, resp.Code) - var branch models.ProtectedBranch - t.Log(resp.Body.String()) - assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &branch)) - assert.Equal(t, canPush, !branch.IsProtected()) - } -} - -func TestInternal_GetProtectedBranch(t *testing.T) { - prepareTestEnv(t) - - assertProtectedBranch(t, 1, "master", false, true) - assertProtectedBranch(t, 1, "dev", false, true) - assertProtectedBranch(t, 1, "lunny/dev", false, true) -} diff --git a/models/helper_environment.go b/models/helper_environment.go index 737a9a68c3a36..199eb6062d528 100644 --- a/models/helper_environment.go +++ b/models/helper_environment.go @@ -27,7 +27,7 @@ func PushingEnvironment(doer *User, repo *Repository) []string { "GIT_COMMITTER_NAME="+sig.Name, "GIT_COMMITTER_EMAIL="+sig.Email, EnvRepoName+"="+repo.Name, - EnvRepoUsername+"="+repo.OwnerName, + EnvRepoUsername+"="+repo.MustOwnerName(), EnvRepoIsWiki+"="+isWiki, EnvPusherName+"="+doer.Name, EnvPusherID+"="+fmt.Sprintf("%d", doer.ID), diff --git a/modules/log/log.go b/modules/log/log.go index d18996d48d31d..8698e9eed3ae3 100644 --- a/modules/log/log.go +++ b/modules/log/log.go @@ -5,9 +5,7 @@ package log import ( - "fmt" "os" - "path" "runtime" "strings" ) @@ -17,9 +15,7 @@ var ( DEFAULT = "default" // NamedLoggers map of named loggers NamedLoggers = make(map[string]*Logger) - // GitLogger logger for git - GitLogger *Logger - prefix string + prefix string ) // NewLogger create a logger for the default logger @@ -72,19 +68,6 @@ func GetLogger(name string) *Logger { return NamedLoggers[DEFAULT] } -// NewGitLogger create a logger for git -// FIXME: use same log level as other loggers. -func NewGitLogger(logPath string) { - path := path.Dir(logPath) - - if err := os.MkdirAll(path, os.ModePerm); err != nil { - Fatal("Failed to create dir %s: %v", path, err) - } - - GitLogger = newLogger("git", 0) - GitLogger.SetLogger("file", "file", fmt.Sprintf(`{"level":"TRACE","filename":"%s","rotate":true,"maxsize":%d,"daily":true,"maxdays":7,"compress":true,"compressionLevel":-1, "stacktraceLevel":"NONE"}`, logPath, 1<<28)) -} - // GetLevel returns the minimum logger level func GetLevel() Level { return NamedLoggers[DEFAULT].GetLevel() diff --git a/modules/pprof/pprof.go b/modules/pprof/pprof.go index e02c2d0f2aba9..b63904e713626 100644 --- a/modules/pprof/pprof.go +++ b/modules/pprof/pprof.go @@ -9,34 +9,30 @@ import ( "io/ioutil" "runtime" "runtime/pprof" - - "code.gitea.io/gitea/modules/log" ) // DumpMemProfileForUsername dumps a memory profile at pprofDataPath as memprofile__ -func DumpMemProfileForUsername(pprofDataPath, username string) { +func DumpMemProfileForUsername(pprofDataPath, username string) error { f, err := ioutil.TempFile(pprofDataPath, fmt.Sprintf("memprofile_%s_", username)) if err != nil { - log.GitLogger.Fatal("Could not create memory profile: %v", err) + return err } defer f.Close() runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - log.GitLogger.Fatal("Could not write memory profile: %v", err) - } + return pprof.WriteHeapProfile(f) } // DumpCPUProfileForUsername dumps a CPU profile at pprofDataPath as cpuprofile__ // it returns the stop function which stops, writes and closes the CPU profile file -func DumpCPUProfileForUsername(pprofDataPath, username string) func() { +func DumpCPUProfileForUsername(pprofDataPath, username string) (func(), error) { f, err := ioutil.TempFile(pprofDataPath, fmt.Sprintf("cpuprofile_%s_", username)) if err != nil { - log.GitLogger.Fatal("Could not create cpu profile: %v", err) + return nil, err } pprof.StartCPUProfile(f) return func() { pprof.StopCPUProfile() f.Close() - } + }, nil } diff --git a/modules/private/branch.go b/modules/private/branch.go deleted file mode 100644 index bbd0d4b697382..0000000000000 --- a/modules/private/branch.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2017 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 private - -import ( - "encoding/json" - "fmt" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" -) - -// GetProtectedBranchBy get protected branch information -func GetProtectedBranchBy(repoID int64, branchName string) (*models.ProtectedBranch, error) { - // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/branch/%d/%s", repoID, util.PathEscapeSegments(branchName)) - log.GitLogger.Trace("GetProtectedBranchBy: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - - var branch models.ProtectedBranch - if err := json.NewDecoder(resp.Body).Decode(&branch); err != nil { - return nil, err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return nil, fmt.Errorf("Failed to get protected branch: %s", decodeJSONError(resp).Err) - } - - return &branch, nil -} - -// CanUserPush returns if user can push -func CanUserPush(protectedBranchID, userID int64) (bool, error) { - // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/protectedbranch/%d/%d", protectedBranchID, userID) - log.GitLogger.Trace("CanUserPush: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return false, err - } - - var canPush = make(map[string]interface{}) - if err := json.NewDecoder(resp.Body).Decode(&canPush); err != nil { - return false, err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return false, fmt.Errorf("Failed to retrieve push user: %s", decodeJSONError(resp).Err) - } - - return canPush["can_push"].(bool), nil -} diff --git a/modules/private/hook.go b/modules/private/hook.go new file mode 100644 index 0000000000000..7e2a475d4b8a1 --- /dev/null +++ b/modules/private/hook.go @@ -0,0 +1,84 @@ +// Copyright 2019 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 private + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "code.gitea.io/gitea/modules/setting" +) + +// Git environment variables +const ( + GitAlternativeObjectDirectories = "GIT_ALTERNATE_OBJECT_DIRECTORIES" + GitObjectDirectory = "GIT_OBJECT_DIRECTORY" + GitQuarantinePath = "GIT_QUARANTINE_PATH" +) + +// HookOptions represents the options for the Hook calls +type HookOptions struct { + OldCommitID string + NewCommitID string + RefFullName string + UserID int64 + UserName string + GitObjectDirectory string + GitAlternativeObjectDirectories string +} + +// HookPreReceive check whether the provided commits are allowed +func HookPreReceive(ownerName, repoName string, opts HookOptions) (int, string) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s?old=%s&new=%s&ref=%s&userID=%d&gitObjectDirectory=%s&gitAlternativeObjectDirectories=%s", + url.PathEscape(ownerName), + url.PathEscape(repoName), + url.QueryEscape(opts.OldCommitID), + url.QueryEscape(opts.NewCommitID), + url.QueryEscape(opts.RefFullName), + opts.UserID, + url.QueryEscape(opts.GitObjectDirectory), + url.QueryEscape(opts.GitAlternativeObjectDirectories), + ) + + resp, err := newInternalRequest(reqURL, "GET").Response() + if err != nil { + return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return resp.StatusCode, decodeJSONError(resp).Err + } + + return http.StatusOK, "" +} + +// HookPostReceive updates services and users +func HookPostReceive(ownerName, repoName string, opts HookOptions) (map[string]interface{}, string) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s?old=%s&new=%s&ref=%s&userID=%d&username=%s", + url.PathEscape(ownerName), + url.PathEscape(repoName), + url.QueryEscape(opts.OldCommitID), + url.QueryEscape(opts.NewCommitID), + url.QueryEscape(opts.RefFullName), + opts.UserID, + url.QueryEscape(opts.UserName)) + + resp, err := newInternalRequest(reqURL, "GET").Response() + if err != nil { + return nil, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, decodeJSONError(resp).Err + } + res := map[string]interface{}{} + _ = json.NewDecoder(resp.Body).Decode(&res) + + return res, "" +} diff --git a/modules/private/internal.go b/modules/private/internal.go index 56852ce63c115..b4fee2680fbac 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -10,11 +10,8 @@ import ( "fmt" "net" "net/http" - "net/url" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -51,49 +48,3 @@ func newInternalRequest(url, method string) *httplib.Request { } return req } - -// CheckUnitUser check whether user could visit the unit of this repository -func CheckUnitUser(userID, repoID int64, isAdmin bool, unitType models.UnitType) (*models.AccessMode, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/user/%d/checkunituser?isAdmin=%t&unitType=%d", repoID, userID, isAdmin, unitType) - log.GitLogger.Trace("CheckUnitUser: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Failed to CheckUnitUser: %s", decodeJSONError(resp).Err) - } - - var a models.AccessMode - if err := json.NewDecoder(resp.Body).Decode(&a); err != nil { - return nil, err - } - - return &a, nil -} - -// GetRepositoryByOwnerAndName returns the repository by given ownername and reponame. -func GetRepositoryByOwnerAndName(ownerName, repoName string) (*models.Repository, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repo/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - log.GitLogger.Trace("GetRepositoryByOwnerAndName: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Failed to get repository: %s", decodeJSONError(resp).Err) - } - - var repo models.Repository - if err := json.NewDecoder(resp.Body).Decode(&repo); err != nil { - return nil, err - } - - return &repo, nil -} diff --git a/modules/private/key.go b/modules/private/key.go index 1c6511846b771..ebc28eb871399 100644 --- a/modules/private/key.go +++ b/modules/private/key.go @@ -5,127 +5,15 @@ package private import ( - "encoding/json" "fmt" - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) -// UpdateDeployKeyUpdated update deploy key updates -func UpdateDeployKeyUpdated(keyID int64, repoID int64) error { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/keys/%d/update", repoID, keyID) - log.GitLogger.Trace("UpdateDeployKeyUpdated: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "POST").Response() - if err != nil { - return err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return fmt.Errorf("Failed to update deploy key: %s", decodeJSONError(resp).Err) - } - return nil -} - -// GetDeployKey check if repo has deploy key -func GetDeployKey(keyID, repoID int64) (*models.DeployKey, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/keys/%d", repoID, keyID) - log.GitLogger.Trace("GetDeployKey: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - defer resp.Body.Close() - - switch resp.StatusCode { - case 404: - return nil, nil - case 200: - var dKey models.DeployKey - if err := json.NewDecoder(resp.Body).Decode(&dKey); err != nil { - return nil, err - } - return &dKey, nil - default: - return nil, fmt.Errorf("Failed to get deploy key: %s", decodeJSONError(resp).Err) - } -} - -// HasDeployKey check if repo has deploy key -func HasDeployKey(keyID, repoID int64) (bool, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/has-keys/%d", repoID, keyID) - log.GitLogger.Trace("HasDeployKey: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return false, err - } - defer resp.Body.Close() - - if resp.StatusCode == 200 { - return true, nil - } - return false, nil -} - -// GetPublicKeyByID get public ssh key by his ID -func GetPublicKeyByID(keyID int64) (*models.PublicKey, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d", keyID) - log.GitLogger.Trace("GetPublicKeyByID: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Failed to get repository: %s", decodeJSONError(resp).Err) - } - - var pKey models.PublicKey - if err := json.NewDecoder(resp.Body).Decode(&pKey); err != nil { - return nil, err - } - return &pKey, nil -} - -// GetUserByKeyID get user attached to key -func GetUserByKeyID(keyID int64) (*models.User, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/user", keyID) - log.GitLogger.Trace("GetUserByKeyID: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Failed to get user: %s", decodeJSONError(resp).Err) - } - - var user models.User - if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { - return nil, err - } - - return &user, nil -} - -// UpdatePublicKeyUpdated update public key updates -func UpdatePublicKeyUpdated(keyID int64) error { +// UpdatePublicKeyInRepo update public key and if necessary deploy key updates +func UpdatePublicKeyInRepo(keyID, repoID int64) error { // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update", keyID) - log.GitLogger.Trace("UpdatePublicKeyUpdated: %s", reqURL) - + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update/%d", keyID, repoID) resp, err := newInternalRequest(reqURL, "POST").Response() if err != nil { return err diff --git a/modules/private/push_update.go b/modules/private/push_update.go deleted file mode 100644 index f3071b63ade1c..0000000000000 --- a/modules/private/push_update.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2017 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 private - -import ( - "encoding/json" - "fmt" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" -) - -// PushUpdate update publick key updates -func PushUpdate(opt models.PushUpdateOptions) error { - // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + "api/internal/push/update" - log.GitLogger.Trace("PushUpdate: %s", reqURL) - - body, err := json.Marshal(&opt) - if err != nil { - return err - } - - resp, err := newInternalRequest(reqURL, "POST").Body(body).Response() - if err != nil { - return err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return fmt.Errorf("Failed to update public key: %s", decodeJSONError(resp).Err) - } - - return nil -} diff --git a/modules/private/repository.go b/modules/private/repository.go deleted file mode 100644 index cf8ae68409057..0000000000000 --- a/modules/private/repository.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2018 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 private - -import ( - "encoding/json" - "fmt" - "net/url" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" -) - -// GetRepository return the repository by its ID and a bool about if it's allowed to have PR -func GetRepository(repoID int64) (*models.Repository, bool, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repository/%d", repoID) - log.GitLogger.Trace("GetRepository: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, false, err - } - - var repoInfo struct { - Repository *models.Repository - AllowPullRequest bool - } - if err := json.NewDecoder(resp.Body).Decode(&repoInfo); err != nil { - return nil, false, err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return nil, false, fmt.Errorf("failed to retrieve repository: %s", decodeJSONError(resp).Err) - } - - return repoInfo.Repository, repoInfo.AllowPullRequest, nil -} - -// ActivePullRequest returns an active pull request if it exists -func ActivePullRequest(baseRepoID int64, headRepoID int64, baseBranch, headBranch string) (*models.PullRequest, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/active-pull-request?baseRepoID=%d&headRepoID=%d&baseBranch=%s&headBranch=%s", baseRepoID, headRepoID, url.QueryEscape(baseBranch), url.QueryEscape(headBranch)) - log.GitLogger.Trace("ActivePullRequest: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - - var pr *models.PullRequest - if err := json.NewDecoder(resp.Body).Decode(&pr); err != nil { - return nil, err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return nil, fmt.Errorf("failed to retrieve pull request: %s", decodeJSONError(resp).Err) - } - - return pr, nil -} diff --git a/modules/private/serv.go b/modules/private/serv.go new file mode 100644 index 0000000000000..5b4a27f11621a --- /dev/null +++ b/modules/private/serv.go @@ -0,0 +1,106 @@ +// Copyright 2019 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 private + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/setting" +) + +// KeyAndOwner is the response from ServNoCommand +type KeyAndOwner struct { + Key *models.PublicKey `json:"key"` + Owner *models.User `json:"user"` +} + +// ServNoCommand returns information about the provided key +func ServNoCommand(keyID int64) (*models.PublicKey, *models.User, error) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", + keyID) + resp, err := newInternalRequest(reqURL, "GET").Response() + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("%s", decodeJSONError(resp).Err) + } + + var keyAndOwner KeyAndOwner + if err := json.NewDecoder(resp.Body).Decode(&keyAndOwner); err != nil { + return nil, nil, err + } + return keyAndOwner.Key, keyAndOwner.Owner, nil +} + +// ServCommandResults are the results of a call to the private route serv +type ServCommandResults struct { + IsWiki bool + IsDeployKey bool + KeyID int64 + KeyName string + UserName string + UserID int64 + OwnerName string + RepoName string + RepoID int64 +} + +// ErrServCommand is an error returned from ServCommmand. +type ErrServCommand struct { + Results ServCommandResults + Type string + Err string + StatusCode int +} + +func (err ErrServCommand) Error() string { + return err.Err +} + +// IsErrServCommand checks if an error is a ErrServCommand. +func IsErrServCommand(err error) bool { + _, ok := err.(ErrServCommand) + return ok +} + +// ServCommand preps for a serv call +func ServCommand(keyID int64, ownerName, repoName string, mode models.AccessMode, verbs ...string) (*ServCommandResults, error) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d", + keyID, + url.PathEscape(ownerName), + url.PathEscape(repoName), + mode) + for _, verb := range verbs { + if verb != "" { + reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb)) + } + } + + resp, err := newInternalRequest(reqURL, "GET").Response() + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errServCommand ErrServCommand + if err := json.NewDecoder(resp.Body).Decode(&errServCommand); err != nil { + return nil, err + } + errServCommand.StatusCode = resp.StatusCode + return nil, errServCommand + } + var results ServCommandResults + if err := json.NewDecoder(resp.Body).Decode(&results); err != nil { + return nil, err + } + return &results, nil + +} diff --git a/modules/private/wiki.go b/modules/private/wiki.go deleted file mode 100644 index 4ad0cc7c4ef5c..0000000000000 --- a/modules/private/wiki.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2018 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 private - -import ( - "fmt" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" -) - -// InitWiki initwiki via repo id -func InitWiki(repoID int64) error { - // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/wiki/init", repoID) - log.GitLogger.Trace("InitWiki: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return fmt.Errorf("Failed to init wiki: %s", decodeJSONError(resp).Err) - } - - return nil -} diff --git a/routers/init.go b/routers/init.go index cfeb928819688..b3078b478aff0 100644 --- a/routers/init.go +++ b/routers/init.go @@ -5,7 +5,6 @@ package routers import ( - "path" "strings" "time" @@ -99,7 +98,6 @@ func GlobalInit() { models.InitSyncMirrors() models.InitDeliverHooks() models.InitTestPullRequests() - log.NewGitLogger(path.Join(setting.LogRootPath, "http.log")) } if models.EnableSQLite3 { log.Info("SQLite3 Supported") diff --git a/routers/private/branch.go b/routers/private/branch.go deleted file mode 100644 index 448c61f1dbba4..0000000000000 --- a/routers/private/branch.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2017 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 private - -import ( - "code.gitea.io/gitea/models" - - macaron "gopkg.in/macaron.v1" -) - -// GetProtectedBranchBy get protected branch information -func GetProtectedBranchBy(ctx *macaron.Context) { - repoID := ctx.ParamsInt64(":id") - branchName := ctx.Params("*") - protectBranch, err := models.GetProtectedBranchBy(repoID, branchName) - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } else if protectBranch != nil { - ctx.JSON(200, protectBranch) - } else { - ctx.JSON(200, &models.ProtectedBranch{ - ID: 0, - }) - } -} - -// CanUserPush returns if user push -func CanUserPush(ctx *macaron.Context) { - pbID := ctx.ParamsInt64(":pbid") - userID := ctx.ParamsInt64(":userid") - - protectBranch, err := models.GetProtectedBranchByID(pbID) - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } else if protectBranch != nil { - ctx.JSON(200, map[string]interface{}{ - "can_push": protectBranch.CanUserPush(userID), - }) - } else { - ctx.JSON(200, map[string]interface{}{ - "can_push": false, - }) - } -} diff --git a/routers/private/hook.go b/routers/private/hook.go new file mode 100644 index 0000000000000..700c8bf332790 --- /dev/null +++ b/routers/private/hook.go @@ -0,0 +1,209 @@ +// Copyright 2019 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 private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. +package private + +import ( + "fmt" + "net/http" + "os" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/util" + + macaron "gopkg.in/macaron.v1" +) + +// HookPreReceive checks whether a individual commit is acceptable +func HookPreReceive(ctx *macaron.Context) { + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + oldCommitID := ctx.QueryTrim("old") + newCommitID := ctx.QueryTrim("new") + refFullName := ctx.QueryTrim("ref") + userID := ctx.QueryInt64("userID") + gitObjectDirectory := ctx.QueryTrim("gitObjectDirectory") + gitAlternativeObjectDirectories := ctx.QueryTrim("gitAlternativeObjectDirectories") + + branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) + repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) + if err != nil { + log.Error("Unable to get repository: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": err.Error(), + }) + return + } + repo.OwnerName = ownerName + 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(500, map[string]interface{}{ + "err": err.Error(), + }) + return + } + if protectBranch != nil && protectBranch.IsProtected() { + // check and deletion + if newCommitID == git.EmptySHA { + log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("branch %s is protected from deletion", branchName), + }) + return + } + + // detect force push + if git.EmptySHA != oldCommitID { + env := append(os.Environ(), + private.GitAlternativeObjectDirectories+"="+gitAlternativeObjectDirectories, + private.GitObjectDirectory+"="+gitObjectDirectory, + private.GitQuarantinePath+"="+gitObjectDirectory, + ) + + 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, map[string]interface{}{ + "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, map[string]interface{}{ + "err": fmt.Sprintf("branch %s is protected from force push", branchName), + }) + return + + } + } + + if !protectBranch.CanUserPush(userID) { + log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v", userID, branchName, repo) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("protected branch %s can not be pushed to", branchName), + }) + return + } + } + ctx.PlainText(http.StatusOK, []byte("ok")) +} + +// HookPostReceive updates services and users +func HookPostReceive(ctx *macaron.Context) { + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + oldCommitID := ctx.Query("old") + newCommitID := ctx.Query("new") + refFullName := ctx.Query("ref") + userID := ctx.QueryInt64("userID") + userName := ctx.Query("username") + + branch := refFullName + if strings.HasPrefix(refFullName, git.BranchPrefix) { + branch = strings.TrimPrefix(refFullName, git.BranchPrefix) + } else if strings.HasPrefix(refFullName, git.TagPrefix) { + branch = strings.TrimPrefix(refFullName, git.TagPrefix) + } + + // Only trigger activity updates for changes to branches or + // tags. Updates to other refs (eg, refs/notes, refs/changes, + // or other less-standard refs spaces are ignored since there + // may be a very large number of them). + if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { + if err := models.PushUpdate(branch, models.PushUpdateOptions{ + RefFullName: refFullName, + OldCommitID: oldCommitID, + NewCommitID: newCommitID, + PusherID: userID, + PusherName: userName, + RepoUserName: ownerName, + RepoName: repoName, + }); err != nil { + log.Error("Failed to Update: %s/%s Branch: %s Error: %v", ownerName, repoName, branch, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Failed to Update: %s/%s Branch: %s Error: %v", ownerName, repoName, branch, err), + }) + return + } + } + + if newCommitID != git.EmptySHA && strings.HasPrefix(refFullName, git.BranchPrefix) { + repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) + if err != nil { + log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + repo.OwnerName = ownerName + + pullRequestAllowed := repo.AllowsPulls() + if !pullRequestAllowed { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": false, + }) + return + } + + baseRepo := repo + if repo.IsFork { + if err := repo.GetBaseRepo(); err != nil { + log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err), + }) + return + } + baseRepo = repo.BaseRepo + } + + if !repo.IsFork && branch == baseRepo.DefaultBranch { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": false, + }) + return + } + + pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch) + if err != nil && !models.IsErrPullRequestNotExist(err) { + log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf( + "Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err), + }) + return + } + + if pr == nil { + if repo.IsFork { + branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch) + } + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": true, + "create": true, + "branch": branch, + "url": fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)), + }) + } else { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": true, + "create": false, + "branch": branch, + "url": fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index), + }) + } + return + } + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": false, + }) + return +} diff --git a/routers/private/internal.go b/routers/private/internal.go index ee6e1274c3e64..11cea8b4b9f3b 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -76,19 +76,10 @@ func CheckUnitUser(ctx *macaron.Context) { // These APIs will be invoked by internal commands for example `gitea serv` and etc. func RegisterRoutes(m *macaron.Macaron) { m.Group("/", func() { - m.Get("/ssh/:id", GetPublicKeyByID) - m.Get("/ssh/:id/user", GetUserByKeyID) - m.Post("/ssh/:id/update", UpdatePublicKey) - m.Post("/repositories/:repoid/keys/:keyid/update", UpdateDeployKey) - m.Get("/repositories/:repoid/user/:userid/checkunituser", CheckUnitUser) - m.Get("/repositories/:repoid/has-keys/:keyid", HasDeployKey) - m.Get("/repositories/:repoid/keys/:keyid", GetDeployKey) - m.Get("/repositories/:repoid/wiki/init", InitWiki) - m.Post("/push/update", PushUpdate) - m.Get("/protectedbranch/:pbid/:userid", CanUserPush) - m.Get("/repo/:owner/:repo", GetRepositoryByOwnerAndName) - m.Get("/branch/:id/*", GetProtectedBranchBy) - m.Get("/repository/:rid", GetRepository) - m.Get("/active-pull-request", GetActivePullRequest) + m.Post("/ssh/:id/update/:repoid", UpdatePublicKeyInRepo) + m.Get("/hook/pre-receive/:owner/:repo", HookPreReceive) + m.Get("/hook/post-receive/:owner/:repo", HookPostReceive) + m.Get("/serv/none/:keyid", ServNoCommand) + m.Get("/serv/command/:keyid/:owner/:repo", ServCommand) }, CheckInternalToken) } diff --git a/routers/private/key.go b/routers/private/key.go index ee22f6ac4881c..f7212ec8929f1 100644 --- a/routers/private/key.go +++ b/routers/private/key.go @@ -12,30 +12,10 @@ import ( macaron "gopkg.in/macaron.v1" ) -// UpdateDeployKey update deploy key updates -func UpdateDeployKey(ctx *macaron.Context) { - repoID := ctx.ParamsInt64(":repoid") - keyID := ctx.ParamsInt64(":keyid") - deployKey, err := models.GetDeployKeyByRepo(keyID, repoID) - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - deployKey.UpdatedUnix = util.TimeStampNow() - if err = models.UpdateDeployKeyCols(deployKey, "updated_unix"); err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - ctx.PlainText(200, []byte("success")) -} - -// UpdatePublicKey update publick key updates -func UpdatePublicKey(ctx *macaron.Context) { +// UpdatePublicKeyInRepo update public key and deploy key updates +func UpdatePublicKeyInRepo(ctx *macaron.Context) { keyID := ctx.ParamsInt64(":id") + repoID := ctx.ParamsInt64(":repoid") if err := models.UpdatePublicKeyUpdated(keyID); err != nil { ctx.JSON(500, map[string]interface{}{ "err": err.Error(), @@ -43,60 +23,24 @@ func UpdatePublicKey(ctx *macaron.Context) { return } - ctx.PlainText(200, []byte("success")) -} - -//GetPublicKeyByID chainload to models.GetPublicKeyByID -func GetPublicKeyByID(ctx *macaron.Context) { - keyID := ctx.ParamsInt64(":id") - key, err := models.GetPublicKeyByID(keyID) - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - ctx.JSON(200, key) -} - -//GetUserByKeyID chainload to models.GetUserByKeyID -func GetUserByKeyID(ctx *macaron.Context) { - keyID := ctx.ParamsInt64(":id") - user, err := models.GetUserByKeyID(keyID) + deployKey, err := models.GetDeployKeyByRepo(keyID, repoID) if err != nil { + if models.IsErrDeployKeyNotExist(err) { + ctx.PlainText(200, []byte("success")) + return + } ctx.JSON(500, map[string]interface{}{ "err": err.Error(), }) return } - ctx.JSON(200, user) -} - -//GetDeployKey chainload to models.GetDeployKey -func GetDeployKey(ctx *macaron.Context) { - repoID := ctx.ParamsInt64(":repoid") - keyID := ctx.ParamsInt64(":keyid") - dKey, err := models.GetDeployKeyByRepo(keyID, repoID) - if err != nil { - if models.IsErrDeployKeyNotExist(err) { - ctx.JSON(404, []byte("not found")) - return - } + deployKey.UpdatedUnix = util.TimeStampNow() + if err = models.UpdateDeployKeyCols(deployKey, "updated_unix"); err != nil { ctx.JSON(500, map[string]interface{}{ "err": err.Error(), }) return } - ctx.JSON(200, dKey) -} -//HasDeployKey chainload to models.HasDeployKey -func HasDeployKey(ctx *macaron.Context) { - repoID := ctx.ParamsInt64(":repoid") - keyID := ctx.ParamsInt64(":keyid") - if models.HasDeployKey(keyID, repoID) { - ctx.PlainText(200, []byte("success")) - return - } - ctx.PlainText(404, []byte("not found")) + ctx.PlainText(200, []byte("success")) } diff --git a/routers/private/push_update.go b/routers/private/push_update.go deleted file mode 100644 index 5c42f066ee7d5..0000000000000 --- a/routers/private/push_update.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2017 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 private - -import ( - "encoding/json" - "strings" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" - - macaron "gopkg.in/macaron.v1" -) - -// PushUpdate update public key updates -func PushUpdate(ctx *macaron.Context) { - var opt models.PushUpdateOptions - if err := json.NewDecoder(ctx.Req.Request.Body).Decode(&opt); err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - branch := strings.TrimPrefix(opt.RefFullName, git.BranchPrefix) - if len(branch) == 0 || opt.PusherID <= 0 { - ctx.Error(404) - log.Trace("PushUpdate: branch or secret is empty, or pusher ID is not valid") - return - } - - err := models.PushUpdate(branch, opt) - if err != nil { - if models.IsErrUserNotExist(err) { - ctx.Error(404) - } else { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - } - return - } - ctx.Status(202) -} diff --git a/routers/private/repository.go b/routers/private/repository.go deleted file mode 100644 index 9f451bcf1dbb0..0000000000000 --- a/routers/private/repository.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2018 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 private - -import ( - "net/http" - - "code.gitea.io/gitea/models" - - macaron "gopkg.in/macaron.v1" -) - -// GetRepository return the default branch of a repository -func GetRepository(ctx *macaron.Context) { - repoID := ctx.ParamsInt64(":rid") - repository, err := models.GetRepositoryByID(repoID) - repository.MustOwnerName() - allowPulls := repository.AllowsPulls() - // put it back to nil because json unmarshal can't unmarshal it - repository.Units = nil - - if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - if repository.IsFork { - repository.GetBaseRepo() - if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), - }) - return - } - repository.BaseRepo.MustOwnerName() - allowPulls = repository.BaseRepo.AllowsPulls() - // put it back to nil because json unmarshal can't unmarshal it - repository.BaseRepo.Units = nil - } - - ctx.JSON(http.StatusOK, struct { - Repository *models.Repository - AllowPullRequest bool - }{ - Repository: repository, - AllowPullRequest: allowPulls, - }) -} - -// GetActivePullRequest return an active pull request when it exists or an empty object -func GetActivePullRequest(ctx *macaron.Context) { - baseRepoID := ctx.QueryInt64("baseRepoID") - headRepoID := ctx.QueryInt64("headRepoID") - baseBranch := ctx.QueryTrim("baseBranch") - if len(baseBranch) == 0 { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": "QueryTrim failed", - }) - return - } - - headBranch := ctx.QueryTrim("headBranch") - if len(headBranch) == 0 { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": "QueryTrim failed", - }) - return - } - - pr, err := models.GetUnmergedPullRequest(headRepoID, baseRepoID, headBranch, baseBranch) - if err != nil && !models.IsErrPullRequestNotExist(err) { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - ctx.JSON(http.StatusOK, pr) -} diff --git a/routers/private/serv.go b/routers/private/serv.go new file mode 100644 index 0000000000000..68e4361e56d84 --- /dev/null +++ b/routers/private/serv.go @@ -0,0 +1,286 @@ +// Copyright 2019 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 private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. +package private + +import ( + "fmt" + "net/http" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/setting" + + macaron "gopkg.in/macaron.v1" +) + +// ServNoCommand returns information about the provided keyid +func ServNoCommand(ctx *macaron.Context) { + keyID := ctx.ParamsInt64(":keyid") + if keyID <= 0 { + ctx.JSON(http.StatusBadRequest, map[string]interface{}{ + "err": fmt.Sprintf("Bad key id: %d", keyID), + }) + } + results := private.KeyAndOwner{} + + key, err := models.GetPublicKeyByID(keyID) + if err != nil { + if models.IsErrKeyNotExist(err) { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "err": fmt.Sprintf("Cannot find key: %d", keyID), + }) + return + } + log.Error("Unable to get public key: %d Error: %v", keyID, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": err.Error(), + }) + return + } + results.Key = key + + if key.Type == models.KeyTypeUser { + user, err := models.GetUserByID(key.OwnerID) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "err": fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), + }) + return + } + log.Error("Unable to get owner with id: %d for public key: %d Error: %v", key.OwnerID, keyID, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": err.Error(), + }) + return + } + results.Owner = user + } + ctx.JSON(http.StatusOK, &results) + return +} + +// ServCommand returns information about the provided keyid +func ServCommand(ctx *macaron.Context) { + // Although we provide the verbs we don't need them at present they're just for logging purposes + keyID := ctx.ParamsInt64(":keyid") + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + mode := models.AccessMode(ctx.QueryInt("mode")) + + // Set the basic parts of the results to return + results := private.ServCommandResults{ + RepoName: repoName, + OwnerName: ownerName, + KeyID: keyID, + } + + // Now because we're not translating things properly let's just default some Engish strings here + modeString := "read" + if mode > models.AccessModeRead { + modeString = "write to" + } + + // The default unit we're trying to look at is code + unitType := models.UnitTypeCode + + // Unless we're a wiki... + if strings.HasSuffix(repoName, ".wiki") { + // in which case we need to look at the wiki + unitType = models.UnitTypeWiki + // And we'd better munge the reponame and tell downstream we're looking at a wiki + results.IsWiki = true + results.RepoName = repoName[:len(repoName)-5] + } + + // Now get the Repository and set the results section + repo, err := models.GetRepositoryByOwnerAndName(results.OwnerName, results.RepoName) + if err != nil { + if models.IsErrRepoNotExist(err) { + ctx.JSON(http.StatusNotFound, map[string]interface{}{ + "results": results, + "type": "ErrRepoNotExist", + "err": fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), + }) + return + } + log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), + }) + return + } + repo.OwnerName = ownerName + results.RepoID = repo.ID + + // We can shortcut at this point if the repo is a mirror + if mode > models.AccessModeRead && repo.IsMirror { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrMirrorReadOnly", + "err": fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), + }) + return + } + + // Get the Public Key represented by the keyID + key, err := models.GetPublicKeyByID(keyID) + if err != nil { + if models.IsErrKeyNotExist(err) { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrKeyNotExist", + "err": fmt.Sprintf("Cannot find key: %d", keyID), + }) + return + } + log.Error("Unable to get public key: %d Error: %v", keyID, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), + }) + return + } + results.KeyName = key.Name + results.KeyID = key.ID + results.UserID = key.OwnerID + + // Deploy Keys have ownerID set to 0 therefore we can't use the owner + // So now we need to check if the key is a deploy key + // We'll keep hold of the deploy key here for permissions checking + var deployKey *models.DeployKey + var user *models.User + if key.Type == models.KeyTypeDeploy { + results.IsDeployKey = true + + var err error + deployKey, err = models.GetDeployKeyByRepo(key.ID, repo.ID) + if err != nil { + if models.IsErrDeployKeyNotExist(err) { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrDeployKeyNotExist", + "err": fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + }) + return + } + log.Error("Unable to get deploy for public (deploy) key: %d in %-v Error: %v", key.ID, repo, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), + }) + return + } + results.KeyName = deployKey.Name + + // FIXME: Deploy keys aren't really the owner of the repo pushing changes + // however we don't have good way of representing deploy keys in hook.go + // so for now use the owner of the repository + results.UserName = results.OwnerName + results.UserID = repo.OwnerID + } else { + // Get the user represented by the Key + var err error + user, err = models.GetUserByID(key.OwnerID) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrUserNotExist", + "err": fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), + }) + return + } + log.Error("Unable to get owner: %d for public key: %d:%s Error: %v", key.OwnerID, key.ID, key.Name, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), + }) + return + } + results.UserName = user.Name + } + + // Don't allow pushing if the repo is archived + if mode > models.AccessModeRead && repo.IsArchived { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrRepoIsArchived", + "err": fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), + }) + return + } + + // Permissions checking: + if mode > models.AccessModeRead || repo.IsPrivate || setting.Service.RequireSignInView { + if key.Type == models.KeyTypeDeploy { + if deployKey.Mode < mode { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrUnauthorized", + "err": fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + }) + return + } + } else { + perm, err := models.GetUserRepoPermission(repo, user) + if err != nil { + log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), + }) + return + } + + userMode := perm.UnitAccessMode(unitType) + + if userMode < mode { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrUnauthorized", + "err": fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), + }) + return + } + } + } + + // Finally if we're trying to touch the wiki we should init it + if results.IsWiki { + if err = repo.InitWiki(); err != nil { + log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + } + log.Debug("Serv Results:\nIsWiki: %t\nIsDeployKey: %t\nKeyID: %d\tKeyName: %s\nUserName: %s\nUserID: %d\nOwnerName: %s\nRepoName: %s\nRepoID: %d", + results.IsWiki, + results.IsDeployKey, + results.KeyID, + results.KeyName, + results.UserName, + results.UserID, + results.OwnerName, + results.RepoName, + results.RepoID) + + ctx.JSON(http.StatusOK, results) + // We will update the keys in a different call. + return +} diff --git a/routers/private/wiki.go b/routers/private/wiki.go deleted file mode 100644 index 33bcbaf17ea6e..0000000000000 --- a/routers/private/wiki.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2017 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 private - -import ( - "code.gitea.io/gitea/models" - - macaron "gopkg.in/macaron.v1" -) - -// InitWiki initilizes wiki via repo id -func InitWiki(ctx *macaron.Context) { - repoID := ctx.ParamsInt64("repoid") - - repo, err := models.GetRepositoryByID(repoID) - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - err = repo.InitWiki() - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - ctx.Status(202) -} diff --git a/routers/repo/http.go b/routers/repo/http.go index fccecfb71d2b2..214e2f3411330 100644 --- a/routers/repo/http.go +++ b/routers/repo/http.go @@ -351,7 +351,7 @@ func gitCommand(dir string, args ...string) []byte { cmd.Dir = dir out, err := cmd.Output() if err != nil { - log.GitLogger.Error(fmt.Sprintf("%v - %s", err, out)) + log.Error("%v - %s", err, out) } return out } @@ -409,7 +409,7 @@ func serviceRPC(h serviceHandler, service string) { if h.r.Header.Get("Content-Encoding") == "gzip" { reqBody, err = gzip.NewReader(reqBody) if err != nil { - log.GitLogger.Error("Fail to create gzip reader: %v", err) + log.Error("Fail to create gzip reader: %v", err) h.w.WriteHeader(http.StatusInternalServerError) return } @@ -428,7 +428,7 @@ func serviceRPC(h serviceHandler, service string) { cmd.Stdin = reqBody cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - log.GitLogger.Error("Fail to serve RPC(%s): %v - %v", service, err, stderr) + log.Error("Fail to serve RPC(%s): %v - %v", service, err, stderr) return } } @@ -541,7 +541,7 @@ func HTTPBackend(ctx *context.Context, cfg *serviceConfig) http.HandlerFunc { file := strings.Replace(r.URL.Path, m[1]+"/", "", 1) dir, err := getGitRepoPath(m[1]) if err != nil { - log.GitLogger.Error(err.Error()) + log.Error(err.Error()) ctx.NotFound("HTTPBackend", err) return } From 8eba27c79257c6bc68cefbdffbb36d3596e6d3ee Mon Sep 17 00:00:00 2001 From: Mario Lubenka Date: Sun, 2 Jun 2019 08:40:12 +0200 Subject: [PATCH 44/44] Repository avatar fallback configuration (#7087) * Only show repository avatar in list when one was selected Signed-off-by: Mario Lubenka * Adds fallback configuration option for repository avatar Signed-off-by: Mario Lubenka * Implements repository avatar fallback Signed-off-by: Mario Lubenka * Adds admin task for deleting generated repository avatars Signed-off-by: Mario Lubenka * Solve linting issues Signed-off-by: Mario Lubenka * Save avatar before updating database * Linting * Update models/repo.go Co-Authored-By: zeripath --- custom/conf/app.ini.sample | 4 + .../doc/advanced/config-cheat-sheet.en-us.md | 5 ++ models/repo.go | 77 ++++++++++++++++-- modules/setting/setting.go | 24 +++--- options/locale/locale_en-US.ini | 2 + public/img/repo_default.png | Bin 0 -> 2464 bytes routers/admin/admin.go | 4 + templates/admin/dashboard.tmpl | 4 + templates/explore/repo_list.tmpl | 4 +- 9 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 public/img/repo_default.png diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index e8e3ffada6865..a674984a2584e 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -505,6 +505,10 @@ SESSION_LIFE_TIME = 86400 [picture] AVATAR_UPLOAD_PATH = data/avatars REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars +; How Gitea deals with missing repository avatars +; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used +REPOSITORY_AVATAR_FALLBACK = none +REPOSITORY_AVATAR_FALLBACK_IMAGE = /img/repo_default.png ; Max Width and Height of uploaded avatars. ; This is to limit the amount of RAM used when resizing the image. AVATAR_MAX_WIDTH = 4096 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 052ced6e2a979..ecc196c86e136 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -292,6 +292,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. [http://www.libravatar.org](http://www.libravatar.org)). - `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. - `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: Path to store repository avatar image files. +- `REPOSITORY_AVATAR_FALLBACK`: **none**: How Gitea deals with missing repository avatars + - none = no avatar will be displayed + - random = random avatar will be generated + - image = default image will be used (which is set in `REPOSITORY_AVATAR_DEFAULT_IMAGE`) +- `REPOSITORY_AVATAR_FALLBACK_IMAGE`: **/img/repo_default.png**: Image used as default repository avatar (if `REPOSITORY_AVATAR_FALLBACK` is set to image and none was uploaded) - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. - `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. - `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): Maximum avatar image file size in bytes. diff --git a/models/repo.go b/models/repo.go index 16684bdeef6dd..d5eca3d22502e 100644 --- a/models/repo.go +++ b/models/repo.go @@ -2528,17 +2528,78 @@ func (repo *Repository) CustomAvatarPath() string { return filepath.Join(setting.RepositoryAvatarUploadPath, repo.Avatar) } -// RelAvatarLink returns a relative link to the user's avatar. -// The link a sub-URL to this site -// Since Gravatar support not needed here - just check for image path. +// GenerateRandomAvatar generates a random avatar for repository. +func (repo *Repository) GenerateRandomAvatar() error { + return repo.generateRandomAvatar(x) +} + +func (repo *Repository) generateRandomAvatar(e Engine) error { + idToString := fmt.Sprintf("%d", repo.ID) + + seed := idToString + img, err := avatar.RandomImage([]byte(seed)) + if err != nil { + return fmt.Errorf("RandomImage: %v", err) + } + + repo.Avatar = idToString + if err = os.MkdirAll(filepath.Dir(repo.CustomAvatarPath()), os.ModePerm); err != nil { + return fmt.Errorf("MkdirAll: %v", err) + } + fw, err := os.Create(repo.CustomAvatarPath()) + if err != nil { + return fmt.Errorf("Create: %v", err) + } + defer fw.Close() + + if err = png.Encode(fw, img); err != nil { + return fmt.Errorf("Encode: %v", err) + } + log.Info("New random avatar created for repository: %d", repo.ID) + + if _, err := e.ID(repo.ID).Cols("avatar").NoAutoTime().Update(repo); err != nil { + return err + } + + return nil +} + +// RemoveRandomAvatars removes the randomly generated avatars that were created for repositories +func RemoveRandomAvatars() error { + var ( + err error + ) + err = x. + Where("id > 0").BufferSize(setting.IterateBufferSize). + Iterate(new(Repository), + func(idx int, bean interface{}) error { + repository := bean.(*Repository) + stringifiedID := strconv.FormatInt(repository.ID, 10) + if repository.Avatar == stringifiedID { + return repository.DeleteAvatar() + } + return nil + }) + return err +} + +// RelAvatarLink returns a relative link to the repository's avatar. func (repo *Repository) RelAvatarLink() string { + // If no avatar - path is empty avatarPath := repo.CustomAvatarPath() - if len(avatarPath) <= 0 { - return "" - } - if !com.IsFile(avatarPath) { - return "" + if len(avatarPath) <= 0 || !com.IsFile(avatarPath) { + switch mode := setting.RepositoryAvatarFallback; mode { + case "image": + return setting.RepositoryAvatarFallbackImage + case "random": + if err := repo.GenerateRandomAvatar(); err != nil { + log.Error("GenerateRandomAvatar: %v", err) + } + default: + // default behaviour: do not display avatar + return "" + } } return setting.AppSubURL + "/repo-avatars/" + repo.Avatar } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 9e9610578823b..ff53e9a3757f1 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -250,16 +250,18 @@ var ( } // Picture settings - AvatarUploadPath string - AvatarMaxWidth int - AvatarMaxHeight int - GravatarSource string - GravatarSourceURL *url.URL - DisableGravatar bool - EnableFederatedAvatar bool - LibravatarService *libravatar.Libravatar - AvatarMaxFileSize int64 - RepositoryAvatarUploadPath string + AvatarUploadPath string + AvatarMaxWidth int + AvatarMaxHeight int + GravatarSource string + GravatarSourceURL *url.URL + DisableGravatar bool + EnableFederatedAvatar bool + LibravatarService *libravatar.Libravatar + AvatarMaxFileSize int64 + RepositoryAvatarUploadPath string + RepositoryAvatarFallback string + RepositoryAvatarFallbackImage string // Log settings LogLevel string @@ -842,6 +844,8 @@ func NewContext() { if !filepath.IsAbs(RepositoryAvatarUploadPath) { RepositoryAvatarUploadPath = path.Join(AppWorkPath, RepositoryAvatarUploadPath) } + RepositoryAvatarFallback = sec.Key("REPOSITORY_AVATAR_FALLBACK").MustString("none") + RepositoryAvatarFallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString("/img/repo_default.png") AvatarMaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) AvatarMaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) AvatarMaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 645c9770a4882..ebc6ca31ce30d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1522,6 +1522,8 @@ dashboard.delete_repo_archives = Delete all repository archives dashboard.delete_repo_archives_success = All repository archives have been deleted. dashboard.delete_missing_repos = Delete all repositories missing their Git files dashboard.delete_missing_repos_success = All repositories missing their Git files have been deleted. +dashboard.delete_generated_repository_avatars = Delete generated repository avatars +dashboard.delete_generated_repository_avatars_success = Generated repository avatars were deleted. dashboard.git_gc_repos = Garbage collect all repositories dashboard.git_gc_repos_success = All repositories have finished garbage collection. dashboard.resync_all_sshkeys = Update the '.ssh/authorized_keys' file with Gitea SSH keys. (Not needed for the built-in SSH server.) diff --git a/public/img/repo_default.png b/public/img/repo_default.png new file mode 100644 index 0000000000000000000000000000000000000000..dbfa8437235208c9c565581c3ea54c38f0a70d43 GIT binary patch literal 2464 zcmb_edsGuw9-hXy;XoBuTu}%KFLA3vLJ*pSsG!hM1Bv*6SW<)-C?YSzLnCNRBeK*- zh>92^Nmq+hc}TQsBM;w_kdRoQU0%v=nM{fY*b?Yw(?r>sp3~FQKX%Wq>>qQ!`F+3d z`|h1PckY?Wtt@bXtG6ov01Gy6iroeP?^L0e3l?Ek-gyHefc-coAqD`{Qa5M%e5Cf< zvuRrb02~SifTPC%;3tGS`aJ*?lL6pW8UWBQ0DxD)rF)4Cq=Dfv!B_yeekzcHRJfwe z330g5`Gf`UV_T~fp#b3W#pc+U9R~*|UUWCW8NQuUCTh}!K<~jVxW)bN&X8pu%QRUZi_LvY%f*8MO~2R&|^1XQqaP5!P!%No0M!>UmsZ!BZAuQoxmil~3e6!~!KdhLmmMp6y;(HU`@ zzYPl4Wa(N>uX4=?*D302yzg`Uh+WBr-|%_|m`Yu({SD=cdno0{@9=sx_nzUSOq1|% zp2;jE*IPi6TGAG3vs`gAWaLN5hNhh}&+XBnx_O$6vXzd98#TFf*;u_6SI8w?s!y>t zjU_YcRT8}3_p#>y`{lWf_9^JWKm~`WbzdT~l(*-uBI<1LvxL;lt$!$m=t%hI#^`W6bP zQqoNGTF2KXG$ru7sqM23q78)c&m=T@cvd!!HfCGfpD2al%CK-TxBdKx`yL{Vv43Yb z5Ah9to4rUy`1y1hV0-fAMk3+t`C~8%cGl#@<*A=G}kjR8Xn(OVg9tz zKU|FWMo3K#PixS(r8r<^OX(d^^}9~P;dQRA;%I-5KVmWE(Vuy^EXIg8_-%E;{KkpVzr-Am%Nm@oUo4U?TqR?THj>cHU_J+@mZ@sm88_ zZ?`tTOe)g;*RPs#M;;{>`gG1cirV>Le^SX?zp$StrPkp(b8{U_PG20N{vV=fzdL>F zreqGg$OZf9>%xNJZ+qO**3UKQHUWPiZEQX{uNs`4>t6cM{j{P5+1z;f-OxF%QyB7m zZo-Mke6?}oi{PBewIj`<_;=+4F-jlg%d|V5RLP^2zR0bxgr|-76D>vA*C@+66`y{K z5gEo0uG)PPy=8>);g;78(~uS)sf!S1+bRs>erz*Zc3$xm8rl;N&OXOSKAn<>Z5Nro zFCds6{(0B9hy)MHD<-x_X+7=m^PtFw?_X7ZV|*Rgrm(J3IeFkEqs&>7**Cnrlwv#8 z%@egASScO2f9+5P%Y6NzaGcGy9Czj-$DBlYA6lr|X~yK5dkcm9e^%Fxn5}L5iUJ(n z>N25Vc!qgny-wC9JQ(cgtSnnuzXHcvhog<2VN|6&tG`KPoEx$_$F+8^RF%mwJu!7{ z{O}5R`hw_B)j=e$5lhLhC6*3A zDy^#>p#P_Xk-viRKo)brF@7xZYoqS_UZrxYWL1=u!f@R}70$LVD zDZrcIl@GRz7ZG%C*Tl50Y`S5JBr%P>_xak_ftTR8T4mvZzo2EW4DC z3KXE=2r4Kc=GzzCg|j`oN5V^loo@t*45SgKv{1R zJ9Eb3e%LmP>9b$HO8&e!0QrqsUKGbE;-?p7(Dw*35CACTFj@#XA|#x=BP^UA5kV)f p2_dhclgU@Q_ka5vLvB8QZ>I3y8+xjA*@ywy9LI`n-jMdmzW{dZ%{Tx6 literal 0 HcmV?d00001 diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 0e6fa2c2426e4..5107e18b7d98d 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -125,6 +125,7 @@ const ( reinitMissingRepository syncExternalUsers gitFsck + deleteGeneratedRepositoryAvatars ) // Dashboard show admin panel dashboard @@ -167,6 +168,9 @@ func Dashboard(ctx *context.Context) { case gitFsck: success = ctx.Tr("admin.dashboard.git_fsck_started") go models.GitFsck() + case deleteGeneratedRepositoryAvatars: + success = ctx.Tr("admin.dashboard.delete_generated_repository_avatars_success") + err = models.RemoveRandomAvatars() } if err != nil { diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 13c06334a5e45..262db04b90608 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -53,6 +53,10 @@ {{.i18n.Tr "admin.dashboard.git_fsck"}} {{.i18n.Tr "admin.dashboard.operation_run"}} + + {{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}} + {{.i18n.Tr "admin.dashboard.operation_run"}} +
diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index 34aab6477a96a..8c7ba51a54005 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -2,7 +2,9 @@ {{range .Repos}}