diff --git a/.drone.yml b/.drone.yml index 6ab40a39d..8e571640b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,7 +4,7 @@ workspace: clone: git: - image: plugins/git:1 + image: plugins/git:next depth: 50 tags: true diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index ef88e5c32..f823f68e4 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -601,9 +601,9 @@ ko-KR = ko [U2F] ; Two Factor authentication with security keys ; https://developers.yubico.com/U2F/App_ID.html -APP_ID = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s +APP_ID = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ ; Comma seperated list of truisted facets -TRUSTED_FACETS = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s +TRUSTED_FACETS = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ ; Extension mapping to highlight class ; e.g. .toml=ini diff --git a/models/issue_watch.go b/models/issue_watch.go index 69e218af0..3e7d24821 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -71,3 +71,15 @@ func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error Find(&watches) return } + +func removeIssueWatchersByRepoID(e Engine, userID int64, repoID int64) error { + iw := &IssueWatch{ + IsWatching: false, + } + _, err := e. + Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", repoID). + Cols("is_watching", "updated_unix"). + Where("`issue_watch`.user_id = ?", userID). + Update(iw) + return err +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 1300065ab..2537e5712 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -186,6 +186,8 @@ var migrations = []Migration{ NewMigration("add u2f", addU2FReg), // v66 -> v67 NewMigration("add login source id column for public_key table", addLoginSourceIDToPublicKeyTable), + // v67 -> v68 + NewMigration("remove stale watches", removeStaleWatches), } // Migrate database to current version diff --git a/models/migrations/v67.go b/models/migrations/v67.go new file mode 100644 index 000000000..278221919 --- /dev/null +++ b/models/migrations/v67.go @@ -0,0 +1,158 @@ +// 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 migrations + +import ( + "code.gitea.io/gitea/modules/setting" + + "github.com/go-xorm/xorm" +) + +func removeStaleWatches(x *xorm.Engine) error { + type Watch struct { + ID int64 + UserID int64 + RepoID int64 + } + + type IssueWatch struct { + ID int64 + UserID int64 + RepoID int64 + IsWatching bool + } + + type Repository struct { + ID int64 + IsPrivate bool + OwnerID int64 + } + + type Access struct { + UserID int64 + RepoID int64 + Mode int + } + + const ( + // AccessModeNone no access + AccessModeNone int = iota // 0 + // AccessModeRead read access + AccessModeRead // 1 + ) + + accessLevel := func(userID int64, repo *Repository) (int, error) { + mode := AccessModeNone + if !repo.IsPrivate { + mode = AccessModeRead + } + + if userID == 0 { + return mode, nil + } + + if userID == repo.OwnerID { + return 4, nil + } + + a := &Access{UserID: userID, RepoID: repo.ID} + if has, err := x.Get(a); !has || err != nil { + return mode, err + } + return a.Mode, nil + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + repoCache := make(map[int64]*Repository) + err := x.BufferSize(setting.IterateBufferSize).Iterate(new(Watch), + func(idx int, bean interface{}) error { + watch := bean.(*Watch) + + repo := repoCache[watch.RepoID] + if repo == nil { + repo = &Repository{ + ID: watch.RepoID, + } + if _, err := x.Get(repo); err != nil { + return err + } + repoCache[watch.RepoID] = repo + } + + // Remove watches from now unaccessible repositories + mode, err := accessLevel(watch.UserID, repo) + if err != nil { + return err + } + has := AccessModeRead <= mode + if has { + return nil + } + + if _, err = sess.Delete(&Watch{0, watch.UserID, repo.ID}); err != nil { + return err + } + _, err = sess.Exec("UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?", repo.ID) + + return err + }) + if err != nil { + return err + } + + repoCache = make(map[int64]*Repository) + err = x.BufferSize(setting.IterateBufferSize). + Distinct("issue_watch.user_id", "issue.repo_id"). + Join("INNER", "issue", "issue_watch.issue_id = issue.id"). + Where("issue_watch.is_watching = ?", true). + Iterate(new(IssueWatch), + func(idx int, bean interface{}) error { + watch := bean.(*IssueWatch) + + repo := repoCache[watch.RepoID] + if repo == nil { + repo = &Repository{ + ID: watch.RepoID, + } + if _, err := x.Get(repo); err != nil { + return err + } + repoCache[watch.RepoID] = repo + } + + // Remove issue watches from now unaccssible repositories + mode, err := accessLevel(watch.UserID, repo) + if err != nil { + return err + } + has := AccessModeRead <= mode + if has { + return nil + } + + iw := &IssueWatch{ + IsWatching: false, + } + + _, err = sess. + Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", watch.RepoID). + Cols("is_watching", "updated_unix"). + Where("`issue_watch`.user_id = ?", watch.UserID). + Update(iw) + + return err + + }) + if err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/models.go b/models/models.go index ddf784dee..5743f1862 100644 --- a/models/models.go +++ b/models/models.go @@ -1,4 +1,5 @@ // Copyright 2014 The Gogs Authors. All rights reserved. +// 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. @@ -184,6 +185,18 @@ func parsePostgreSQLHostPort(info string) (string, string) { return host, port } +func getPostgreSQLConnectionString(DBHost, DBUser, DBPasswd, DBName, DBParam, DBSSLMode string) (connStr string) { + host, port := parsePostgreSQLHostPort(DBHost) + if host[0] == '/' { // looks like a unix socket + connStr = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&host=%s", + url.PathEscape(DBUser), url.PathEscape(DBPasswd), port, DBName, DBParam, DBSSLMode, host) + } else { + connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s", + url.PathEscape(DBUser), url.PathEscape(DBPasswd), host, port, DBName, DBParam, DBSSLMode) + } + return +} + func parseMSSQLHostPort(info string) (string, string) { host, port := "127.0.0.1", "1433" if strings.Contains(info, ":") { @@ -214,14 +227,7 @@ func getEngine() (*xorm.Engine, error) { DbCfg.User, DbCfg.Passwd, DbCfg.Host, DbCfg.Name, Param) } case "postgres": - host, port := parsePostgreSQLHostPort(DbCfg.Host) - if host[0] == '/' { // looks like a unix socket - connStr = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&host=%s", - url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Passwd), port, DbCfg.Name, Param, DbCfg.SSLMode, host) - } else { - connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s", - url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Passwd), host, port, DbCfg.Name, Param, DbCfg.SSLMode) - } + connStr = getPostgreSQLConnectionString(DbCfg.Host, DbCfg.User, DbCfg.Passwd, DbCfg.Name, Param, DbCfg.SSLMode) case "mssql": host, port := parseMSSQLHostPort(DbCfg.Host) connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, DbCfg.Name, DbCfg.User, DbCfg.Passwd) diff --git a/models/models_test.go b/models/models_test.go index 649b1e02e..7016fdb4b 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -1,4 +1,5 @@ // Copyright 2016 The Gogs Authors. All rights reserved. +// 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. @@ -53,3 +54,42 @@ func Test_parsePostgreSQLHostPort(t *testing.T) { assert.Equal(t, test.Port, port) } } + +func Test_getPostgreSQLConnectionString(t *testing.T) { + tests := []struct { + Host string + Port string + User string + Passwd string + Name string + Param string + SSLMode string + Output string + }{ + { + Host: "/tmp/pg.sock", + Port: "4321", + User: "testuser", + Passwd: "space space !#$%^^%^```-=?=", + Name: "gitea", + Param: "", + SSLMode: "false", + Output: "postgres://testuser:space%20space%20%21%23$%25%5E%5E%25%5E%60%60%60-=%3F=@:5432/giteasslmode=false&host=/tmp/pg.sock", + }, + { + Host: "localhost", + Port: "1234", + User: "pgsqlusername", + Passwd: "I love Gitea!", + Name: "gitea", + Param: "", + SSLMode: "true", + Output: "postgres://pgsqlusername:I%20love%20Gitea%21@localhost:5432/giteasslmode=true", + }, + } + + for _, test := range tests { + connStr := getPostgreSQLConnectionString(test.Host, test.User, test.Passwd, test.Name, test.Param, test.SSLMode) + assert.Equal(t, test.Output, connStr) + } +} diff --git a/models/org_team.go b/models/org_team.go index 9d8a03141..5ea6e76cd 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -178,6 +178,11 @@ func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (e if err = watchRepo(e, teamUser.UID, repo.ID, false); err != nil { return err } + + // Remove all IssueWatches a user has subscribed to in the repositories + if err := removeIssueWatchersByRepoID(e, teamUser.UID, repo.ID); err != nil { + return err + } } return nil @@ -374,11 +379,34 @@ func DeleteTeam(t *Team) error { return err } + if err := t.getMembers(sess); err != nil { + return err + } + // Delete all accesses. for _, repo := range t.Repos { if err := repo.recalculateTeamAccesses(sess, t.ID); err != nil { return err } + + // Remove watches from all users and now unaccessible repos + for _, user := range t.Members { + has, err := hasAccess(sess, user.ID, repo, AccessModeRead) + if err != nil { + return err + } else if has { + continue + } + + if err = watchRepo(sess, user.ID, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repositories + if err = removeIssueWatchersByRepoID(sess, user.ID, repo.ID); err != nil { + return err + } + } } // Delete team-repo @@ -518,6 +546,10 @@ func AddTeamMember(team *Team, userID int64) error { if err := repo.recalculateTeamAccesses(sess, 0); err != nil { return err } + + if err = watchRepo(sess, userID, repo.ID, true); err != nil { + return err + } } return sess.Commit() @@ -558,6 +590,23 @@ func removeTeamMember(e *xorm.Session, team *Team, userID int64) error { if err := repo.recalculateTeamAccesses(e, 0); err != nil { return err } + + // Remove watches from now unaccessible + has, err := hasAccess(e, userID, repo, AccessModeRead) + if err != nil { + return err + } else if has { + continue + } + + if err = watchRepo(e, userID, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repositories + if err := removeIssueWatchersByRepoID(e, userID, repo.ID); err != nil { + return err + } } // Check if the user is a member of any team in the organization. diff --git a/models/repo.go b/models/repo.go index f4923cf4a..7f2be502a 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1851,6 +1851,9 @@ func DeleteRepository(doer *User, uid, repoID int64) error { if _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{}); err != nil { return err } + if _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{}); err != nil { + return err + } attachments := make([]*Attachment, 0, 5) if err = sess. diff --git a/models/repo_collaboration.go b/models/repo_collaboration.go index 0448149e6..9d2935d58 100644 --- a/models/repo_collaboration.go +++ b/models/repo_collaboration.go @@ -172,5 +172,14 @@ func (repo *Repository) DeleteCollaboration(uid int64) (err error) { return err } + if err = watchRepo(sess, uid, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repository + if err := removeIssueWatchersByRepoID(sess, uid, repo.ID); err != nil { + return err + } + return sess.Commit() } diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index d618d2c9a..4b1b85f92 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -588,7 +588,7 @@ editor.edit_file=Datei bearbeiten editor.preview_changes=Vorschau der Änderungen editor.cannot_edit_non_text_files=Binärdateien können nicht im Webinterface bearbeitet werden. editor.edit_this_file=Datei bearbeiten -editor.must_be_on_a_branch=Du musst dich in einer Branch befinden, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen. +editor.must_be_on_a_branch=Du musst dich in einem Branch befinden, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen. editor.fork_before_edit=Du musst dieses Repository forken, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen. editor.delete_this_file=Datei löschen editor.must_have_write_access=Du benötigst Schreibzugriff, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen. @@ -1085,16 +1085,16 @@ settings.protect_whitelist_search_users=Benutzer suchen… settings.protect_whitelist_teams=Teams, die pushen dürfen: settings.protect_whitelist_search_teams=Suche nach Teams… settings.protect_merge_whitelist_committers=Merge-Whitelist aktivieren -settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Whitelist Pull-Requests in diese Branch zu mergen. +settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Whitelist Pull-Requests in diesen Branch zu mergen. settings.protect_merge_whitelist_users=Nutzer, die mergen dürfen: settings.protect_merge_whitelist_teams=Teams, die mergen dürfen: settings.add_protected_branch=Schutz aktivieren settings.delete_protected_branch=Schutz deaktivieren settings.update_protect_branch_success=Branch-Schutz für den Branch „%s“ wurde geändert. settings.remove_protected_branch_success=Branch-Schutz für den Branch „%s“ wurde deaktiviert. -settings.protected_branch_deletion=Brach-Schutz deaktivieren +settings.protected_branch_deletion=Branch-Schutz deaktivieren settings.protected_branch_deletion_desc=Wenn du den Branch-Schutz deaktivierst, können alle Nutzer mit Schreibrechten auf den Branch pushen. Fortfahren? -settings.default_branch_desc=Wähle eine Standardbranch für Pull-Requests und Code-Commits: +settings.default_branch_desc=Wähle einen Standardbranch für Pull-Requests und Code-Commits: settings.choose_branch=Wähle einen Branch … settings.no_protected_branch=Es gibt keine geschützten Branches. @@ -1238,7 +1238,7 @@ teams.update_settings=Einstellungen aktualisieren teams.delete_team=Team löschen teams.add_team_member=Teammitglied hinzufügen teams.delete_team_title=Team löschen -teams.delete_team_desc=Das Löschen eines Teams wiederruft den Repository-Zugriff für seine Mitglieder. Fortfahren? +teams.delete_team_desc=Das Löschen eines Teams widerruft den Repository-Zugriff für seine Mitglieder. Fortfahren? teams.delete_team_success=Das Team wurde gelöscht. teams.read_permission_desc=Dieses Team hat Lesezugriff: Mitglieder können Team-Repositories einsehen und klonen. teams.write_permission_desc=Dieses Team hat Schreibzugriff: Mitglieder können Team-Repositories einsehen und darauf pushen. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index b3e8a2fe0..8f90f3a3f 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -204,7 +204,7 @@ non_local_account=Нелокальні акаунти не можуть змін verify=Підтвердити scratch_code=Одноразовий пароль use_scratch_code=Використовувати одноразовий пароль -twofa_scratch_used=Ви використовували одноразовий пароль. Ви були перенаправлені на сторінку налаштувань для генерації нового коду або відключення двуфакторной аутентифікації. +twofa_scratch_used=Ви використовували одноразовий пароль. Ви були перенаправлені на сторінку налаштувань для генерації нового коду або відключення двуфакторної автентифікації. twofa_passcode_incorrect=Ваш пароль є невірним. Якщо ви втратили пристрій, використовуйте ваш одноразовий пароль. twofa_scratch_token_incorrect=Невірний одноразовий пароль. login_userpass=Увійти @@ -399,20 +399,25 @@ generate_token=Згенерувати токен delete_token=Видалити access_token_deletion=Видалити токен доступу -twofa_desc=Двофакторна аутентифікація підвищує безпеку вашого облікового запису. +twofa_desc=Двофакторна автентифікація підвищує безпеку вашого облікового запису. twofa_is_enrolled=Ваш обліковий запис на даний час використовує двофакторну автентифікацію. twofa_disable=Вимкнути двофакторну автентифікацію +twofa_scratch_token_regenerate=Перестворити токен одноразового пароля twofa_enroll=Увімкнути двофакторну автентифікацію +twofa_disable_note=При необхідності можна відключити двофакторну автентифікацію. +regenerate_scratch_token_desc=Якщо ви втратили свій токен одноразового пароля або вже використовували його для входу, ви можете скинути його тут. twofa_disabled=Двофакторна автентифікація вимкнена. -scan_this_image=Проскануйте це зображення вашим додатком для двуфакторної аутентифікації: +scan_this_image=Проскануйте це зображення вашим додатком для двуфакторної автентифікації: or_enter_secret=Або введіть секрет: %s passcode_invalid=Некоректний пароль. Спробуй ще раз. +u2f_desc=Ключами безпеки є апаратні пристрої, що містять криптографічні ключі. Вони можуть використовуватися для двофакторної автентифікації. Ключ безпеки повинен підтримувати стандарт FIDO U2F. u2f_register_key=Додати ключ безпеки u2f_nickname=Псевдонім u2f_delete_key=Видалити ключ безпеки manage_account_links=Керування обліковими записами +manage_account_links_desc=Ці зовнішні акаунти прив'язані до вашого аккаунту Gitea. remove_account_link=Видалити облікові записи orgs_none=Ви не є учасником будь-якої організації. @@ -428,6 +433,7 @@ owner=Власник repo_name=Назва репозиторію visibility=Видимість visiblity_helper=Зробити репозиторій приватним +visiblity_fork_helper=(Зміна цього вплине на всі форки.) clone_helper=Потрібна допомога у клонуванні? Відвідайте Допомогу. fork_repo=Форкнути репозиторій fork_from=Форк з @@ -607,6 +613,7 @@ issues.filter_sort.leastcomment=Найменш коментовані issues.filter_sort.moststars=Найбільш обраних issues.filter_sort.feweststars=Найменш обраних issues.filter_sort.mostforks=Найбільше форків +issues.filter_sort.fewestforks=Найменше форків issues.action_open=Відкрити issues.action_close=Закрити issues.action_label=Мітка @@ -971,6 +978,7 @@ topic.done=Готово [org] org_name_holder=Назва організації org_full_name_holder=Повна назва організації +org_name_helper=Назва організації має бути простою та зрозумілою. create_org=Створити організацію repo_updated=Оновлено people=Учасники @@ -1133,7 +1141,7 @@ repos.forks=Форки repos.issues=Проблеми repos.size=Розмір -auths.auth_manage_panel=Керування джерелом аутентифікації +auths.auth_manage_panel=Керування джерелом автентифікації auths.new=Додати джерело автентифікації auths.name=Ім'я auths.type=Тип @@ -1170,8 +1178,8 @@ auths.oauth2_profileURL=URL профілю auths.oauth2_emailURL=URL електронної пошти auths.enable_auto_register=Увімкнути автоматичну реєстрацію auths.tips=Поради -auths.tips.oauth2.general=OAuth2 аутентифікація -auths.tips.oauth2.general.tip=При додаванні нового OAuth2 провайдера, URL адреса переадресації по завершенні аутентифікації повинена виглядати так:/user/oauth2//callback +auths.tips.oauth2.general=OAuth2 автентифікація +auths.tips.oauth2.general.tip=При додаванні нового OAuth2 провайдера, URL адреса переадресації по завершенні автентифікації повинена виглядати так:/user/oauth2//callback auths.tip.oauth2_provider=Постачальник OAuth2 auths.tip.dropbox=Додайте новий додаток на https://www.dropbox.com/developers/apps auths.tip.facebook=Створіть новий додаток на https://developers.facebook.com/apps і додайте модуль "Facebook Login diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 658f133a2..4e59ed08e 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -213,6 +213,7 @@ send_reset_mail=单击此处(重新)发送您的密码重置邮件 reset_password=重置密码 invalid_code=此确认密钥无效或已过期。 reset_password_helper=单击此处重置密码 +password_too_short=密码长度不能少于 %d 位。 non_local_account=非本地帐户不能通过 Gitea 的 web 界面更改密码。 verify=验证 scratch_code=验证口令 diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 211d8045a..7be39166d 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -1,4 +1,5 @@ // Copyright 2016 The Gogs Authors. All rights reserved. +// 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. @@ -165,7 +166,7 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) { // "$ref": "#/responses/Issue" var deadlineUnix util.TimeStamp - if form.Deadline != nil { + if form.Deadline != nil && ctx.Repo.IsWriter() { deadlineUnix = util.TimeStamp(form.Deadline.Unix()) } @@ -178,15 +179,22 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) { DeadlineUnix: deadlineUnix, } - // Get all assignee IDs - assigneeIDs, err := models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees) - if err != nil { - if models.IsErrUserNotExist(err) { - ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) - } else { - ctx.Error(500, "AddAssigneeByName", err) + var assigneeIDs = make([]int64, 0) + var err error + if ctx.Repo.IsWriter() { + issue.MilestoneID = form.Milestone + assigneeIDs, err = models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) + } else { + ctx.Error(500, "AddAssigneeByName", err) + } + return } - return + } else { + // setting labels is not allowed if user is not a writer + form.Labels = make([]int64, 0) } if err := models.NewIssue(ctx.Repo.Repository, issue, form.Labels, assigneeIDs, nil); err != nil {