Merge branch 'main' into kerwin612-add-file-tree-to-file-view-page

pull/32721/head
Lunny Xiao 2 months ago
commit ca964c1ce9

@ -175,10 +175,14 @@ func (p *Permission) LogString() string {
return fmt.Sprintf(format, args...) return fmt.Sprintf(format, args...)
} }
func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) { func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
if user == nil || user.ID <= 0 { if user == nil || user.ID <= 0 {
// for anonymous access, it could be:
// AccessMode is None or Read, units has repo units, unitModes is nil
return return
} }
// apply everyone access permissions
for _, u := range perm.units { for _, u := range perm.units {
if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] { if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] {
if perm.everyoneAccessMode == nil { if perm.everyoneAccessMode == nil {
@ -187,17 +191,40 @@ func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) {
perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode
} }
} }
if perm.unitsMode == nil {
// if unitsMode is not set, then it means that the default p.AccessMode applies to all units
return
}
// remove no permission units
origPermUnits := perm.units
perm.units = make([]*repo_model.RepoUnit, 0, len(perm.units))
for _, u := range origPermUnits {
shouldKeep := false
for t := range perm.unitsMode {
if shouldKeep = u.Type == t; shouldKeep {
break
}
}
for t := range perm.everyoneAccessMode {
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
break
}
}
if shouldKeep {
perm.units = append(perm.units, u)
}
}
} }
// GetUserRepoPermission returns the user permissions to the repository // GetUserRepoPermission returns the user permissions to the repository
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
defer func() { defer func() {
if err == nil { if err == nil {
applyEveryoneRepoPermission(user, &perm) finalProcessRepoUnitPermission(user, &perm)
}
if log.IsTrace() {
log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm)
} }
log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm)
}() }()
if err = repo.LoadUnits(ctx); err != nil { if err = repo.LoadUnits(ctx); err != nil {
@ -294,16 +321,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
} }
} }
// remove no permission units
perm.units = make([]*repo_model.RepoUnit, 0, len(repo.Units))
for t := range perm.unitsMode {
for _, u := range repo.Units {
if u.Type == t {
perm.units = append(perm.units, u)
}
}
}
return perm, err return perm, err
} }

@ -50,7 +50,7 @@ func TestApplyEveryoneRepoPermission(t *testing.T) {
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
}, },
} }
applyEveryoneRepoPermission(nil, &perm) finalProcessRepoUnitPermission(nil, &perm)
assert.False(t, perm.CanRead(unit.TypeWiki)) assert.False(t, perm.CanRead(unit.TypeWiki))
perm = Permission{ perm = Permission{
@ -59,7 +59,7 @@ func TestApplyEveryoneRepoPermission(t *testing.T) {
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
}, },
} }
applyEveryoneRepoPermission(&user_model.User{ID: 0}, &perm) finalProcessRepoUnitPermission(&user_model.User{ID: 0}, &perm)
assert.False(t, perm.CanRead(unit.TypeWiki)) assert.False(t, perm.CanRead(unit.TypeWiki))
perm = Permission{ perm = Permission{
@ -68,7 +68,7 @@ func TestApplyEveryoneRepoPermission(t *testing.T) {
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
}, },
} }
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm)
assert.True(t, perm.CanRead(unit.TypeWiki)) assert.True(t, perm.CanRead(unit.TypeWiki))
perm = Permission{ perm = Permission{
@ -77,20 +77,22 @@ func TestApplyEveryoneRepoPermission(t *testing.T) {
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
}, },
} }
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm)
// it should work the same as "EveryoneAccessMode: none" because the default AccessMode should be applied to units // it should work the same as "EveryoneAccessMode: none" because the default AccessMode should be applied to units
assert.True(t, perm.CanWrite(unit.TypeWiki)) assert.True(t, perm.CanWrite(unit.TypeWiki))
perm = Permission{ perm = Permission{
units: []*repo_model.RepoUnit{ units: []*repo_model.RepoUnit{
{Type: unit.TypeCode}, // will be removed
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
}, },
unitsMode: map[unit.Type]perm_model.AccessMode{ unitsMode: map[unit.Type]perm_model.AccessMode{
unit.TypeWiki: perm_model.AccessModeWrite, unit.TypeWiki: perm_model.AccessModeWrite,
}, },
} }
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm)
assert.True(t, perm.CanWrite(unit.TypeWiki)) assert.True(t, perm.CanWrite(unit.TypeWiki))
assert.Len(t, perm.units, 1)
} }
func TestUnitAccessMode(t *testing.T) { func TestUnitAccessMode(t *testing.T) {

@ -54,6 +54,7 @@ func UpdateRepoLicenses(ctx context.Context, repo *Repository, commitID string,
for _, o := range oldLicenses { for _, o := range oldLicenses {
// Update already existing license // Update already existing license
if o.License == license { if o.License == license {
o.CommitID = commitID
if _, err := db.GetEngine(ctx).ID(o.ID).Cols("`commit_id`").Update(o); err != nil { if _, err := db.GetEngine(ctx).ID(o.ID).Cols("`commit_id`").Update(o); err != nil {
return err return err
} }

@ -11,35 +11,13 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
// Copy copies file from source to target path. // SyncFile synchronizes the two files. This is skipped if both files
func Copy(src, dest string) error {
// Gather file information to set back later.
si, err := os.Lstat(src)
if err != nil {
return err
}
// Handle symbolic link.
if si.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(src)
if err != nil {
return err
}
// NOTE: os.Chmod and os.Chtimes don't recognize symbolic link,
// which will lead "no such file or directory" error.
return os.Symlink(target, dest)
}
return util.CopyFile(src, dest)
}
// Sync synchronizes the two files. This is skipped if both files
// exist and the size, modtime, and mode match. // exist and the size, modtime, and mode match.
func Sync(srcPath, destPath string) error { func SyncFile(srcPath, destPath string) error {
dest, err := os.Stat(destPath) dest, err := os.Stat(destPath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return Copy(srcPath, destPath) return util.CopyFile(srcPath, destPath)
} }
return err return err
} }
@ -55,7 +33,7 @@ func Sync(srcPath, destPath string) error {
return nil return nil
} }
return Copy(srcPath, destPath) return util.CopyFile(srcPath, destPath)
} }
// SyncDirs synchronizes files recursively from source to target directory. // SyncDirs synchronizes files recursively from source to target directory.
@ -66,6 +44,10 @@ func SyncDirs(srcPath, destPath string) error {
return err return err
} }
// the keep file is used to keep the directory in a git repository, it doesn't need to be synced
// and go-git doesn't work with the ".keep" file (it would report errors like "ref is empty")
const keepFile = ".keep"
// find and delete all untracked files // find and delete all untracked files
destFiles, err := util.ListDirRecursively(destPath, &util.ListDirOptions{IncludeDir: true}) destFiles, err := util.ListDirRecursively(destPath, &util.ListDirOptions{IncludeDir: true})
if err != nil { if err != nil {
@ -73,16 +55,20 @@ func SyncDirs(srcPath, destPath string) error {
} }
for _, destFile := range destFiles { for _, destFile := range destFiles {
destFilePath := filepath.Join(destPath, destFile) destFilePath := filepath.Join(destPath, destFile)
shouldRemove := filepath.Base(destFilePath) == keepFile
if _, err = os.Stat(filepath.Join(srcPath, destFile)); err != nil { if _, err = os.Stat(filepath.Join(srcPath, destFile)); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
// if src file does not exist, remove dest file shouldRemove = true
if err = os.RemoveAll(destFilePath); err != nil {
return err
}
} else { } else {
return err return err
} }
} }
// if src file does not exist, remove dest file
if shouldRemove {
if err = os.RemoveAll(destFilePath); err != nil {
return err
}
}
} }
// sync src files to dest // sync src files to dest
@ -95,8 +81,8 @@ func SyncDirs(srcPath, destPath string) error {
// util.ListDirRecursively appends a slash to the directory name // util.ListDirRecursively appends a slash to the directory name
if strings.HasSuffix(srcFile, "/") { if strings.HasSuffix(srcFile, "/") {
err = os.MkdirAll(destFilePath, os.ModePerm) err = os.MkdirAll(destFilePath, os.ModePerm)
} else { } else if filepath.Base(destFilePath) != keepFile {
err = Sync(filepath.Join(srcPath, srcFile), destFilePath) err = SyncFile(filepath.Join(srcPath, srcFile), destFilePath)
} }
if err != nil { if err != nil {
return err return err

@ -1953,6 +1953,7 @@ pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[
pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s
pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes
pulls.upstream_diverging_merge = Sync fork pulls.upstream_diverging_merge = Sync fork
pulls.upstream_diverging_merge_confirm = Would you like to merge base repository's default branch onto this repository's branch %s?
pull.deleted_branch = (deleted):%s pull.deleted_branch = (deleted):%s
pull.agit_documentation = Review documentation about AGit pull.agit_documentation = Review documentation about AGit
@ -2158,7 +2159,7 @@ settings.advanced_settings = Advanced Settings
settings.wiki_desc = Enable Repository Wiki settings.wiki_desc = Enable Repository Wiki
settings.use_internal_wiki = Use Built-In Wiki settings.use_internal_wiki = Use Built-In Wiki
settings.default_wiki_branch_name = Default Wiki Branch Name settings.default_wiki_branch_name = Default Wiki Branch Name
settings.default_wiki_everyone_access = Default Access Permission for signed-in users: settings.default_permission_everyone_access = Default access permission for all signed-in users:
settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch. settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch.
settings.use_external_wiki = Use External Wiki settings.use_external_wiki = Use External Wiki
settings.external_wiki_url = External Wiki URL settings.external_wiki_url = External Wiki URL

@ -26,13 +26,9 @@ type userSearchResponse struct {
Results []*userSearchInfo `json:"results"` Results []*userSearchInfo `json:"results"`
} }
// IssuePosters get posters for current repo's issues/pull requests func IssuePullPosters(ctx *context.Context) {
func IssuePosters(ctx *context.Context) { isPullList := ctx.PathParam("type") == "pulls"
issuePosters(ctx, false) issuePosters(ctx, isPullList)
}
func PullPosters(ctx *context.Context) {
issuePosters(ctx, true)
} }
func issuePosters(ctx *context.Context, isPullList bool) { func issuePosters(ctx *context.Context, isPullList bool) {

@ -17,7 +17,6 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -330,12 +329,42 @@ func LatestRelease(ctx *context.Context) {
ctx.Redirect(release.Link()) ctx.Redirect(release.Link())
} }
// NewRelease render creating or edit release page func newReleaseCommon(ctx *context.Context) {
func NewRelease(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.release.new_release") ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
ctx.Data["PageIsReleaseList"] = true ctx.Data["PageIsReleaseList"] = true
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
return
}
ctx.Data["Tags"] = tags
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("GetRepoAssignees", err)
return
}
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
upload.AddUploadContext(ctx, "release")
PrepareBranchList(ctx) // for New Release page
}
// NewRelease render creating or edit release page
func NewRelease(ctx *context.Context) {
newReleaseCommon(ctx)
if ctx.Written() {
return
}
ctx.Data["ShowCreateTagOnlyButton"] = true
// pre-fill the form with the tag name, target branch and the existing release (if exists)
ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch
if tagName := ctx.FormString("tag"); len(tagName) > 0 { if tagName := ctx.FormString("tag"); tagName != "" {
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName) rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName)
if err != nil && !repo_model.IsErrReleaseNotExist(err) { if err != nil && !repo_model.IsErrReleaseNotExist(err) {
ctx.ServerError("GetRelease", err) ctx.ServerError("GetRelease", err)
@ -344,59 +373,51 @@ func NewRelease(ctx *context.Context) {
if rel != nil { if rel != nil {
rel.Repo = ctx.Repo.Repository rel.Repo = ctx.Repo.Repository
if err := rel.LoadAttributes(ctx); err != nil { if err = rel.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err) ctx.ServerError("LoadAttributes", err)
return return
} }
ctx.Data["ShowCreateTagOnlyButton"] = false
ctx.Data["tag_name"] = rel.TagName ctx.Data["tag_name"] = rel.TagName
if rel.Target != "" { ctx.Data["tag_target"] = rel.Target
ctx.Data["tag_target"] = rel.Target
}
ctx.Data["title"] = rel.Title ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note ctx.Data["content"] = rel.Note
ctx.Data["attachments"] = rel.Attachments ctx.Data["attachments"] = rel.Attachments
} }
} }
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("GetRepoAssignees", err)
return
}
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
upload.AddUploadContext(ctx, "release")
// For New Release page
PrepareBranchList(ctx)
if ctx.Written() {
return
}
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
return
}
ctx.Data["Tags"] = tags
ctx.HTML(http.StatusOK, tplReleaseNew) ctx.HTML(http.StatusOK, tplReleaseNew)
} }
// NewReleasePost response for creating a release // NewReleasePost response for creating a release
func NewReleasePost(ctx *context.Context) { func NewReleasePost(ctx *context.Context) {
newReleaseCommon(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*forms.NewReleaseForm) form := web.GetForm(ctx).(*forms.NewReleaseForm)
ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
ctx.Data["PageIsReleaseList"] = true
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) // first, check whether the release exists, and prepare "ShowCreateTagOnlyButton"
if err != nil { // the logic should be done before the form error check to make the tmpl has correct variables
ctx.ServerError("GetTagNamesByRepoID", err) rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
ctx.ServerError("GetRelease", err)
return return
} }
ctx.Data["Tags"] = tags
// We should still show the "tag only" button if the user clicks it, no matter the release exists or not.
// Because if error occurs, end users need to have the chance to edit the name and submit the form with "tag-only" again.
// It is still not completely right, because there could still be cases like this:
// * user visit "new release" page, see the "tag only" button
// * input something, click other buttons but not "tag only"
// * error occurs, the "new release" page is rendered again, but the "tag only" button is gone
// Such cases are not able to be handled by current code, it needs frontend code to toggle the "tag-only" button if the input changes.
// Or another choice is "always show the tag-only button" if error occurs.
ctx.Data["ShowCreateTagOnlyButton"] = form.TagOnly || rel == nil
// do some form checks
if ctx.HasError() { if ctx.HasError() {
ctx.HTML(http.StatusOK, tplReleaseNew) ctx.HTML(http.StatusOK, tplReleaseNew)
return return
@ -407,59 +428,49 @@ func NewReleasePost(ctx *context.Context) {
return return
} }
// Title of release cannot be empty if !form.TagOnly && form.Title == "" {
if len(form.TagOnly) == 0 && len(form.Title) == 0 { // if not "tag only", then the title of the release cannot be empty
ctx.RenderWithErr(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form) ctx.RenderWithErr(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form)
return return
} }
var attachmentUUIDs []string handleTagReleaseError := func(err error) {
if setting.Attachment.Enabled { ctx.Data["Err_TagName"] = true
attachmentUUIDs = form.Files switch {
} case release_service.IsErrTagAlreadyExists(err):
ctx.RenderWithErr(ctx.Tr("repo.branch.tag_collision", form.TagName), tplReleaseNew, &form)
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName) case repo_model.IsErrReleaseAlreadyExist(err):
if err != nil { ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
if !repo_model.IsErrReleaseNotExist(err) { case release_service.IsErrInvalidTagName(err):
ctx.ServerError("GetRelease", err) ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
return case release_service.IsErrProtectedTagName(err):
} ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form)
default:
msg := "" ctx.ServerError("handleTagReleaseError", err)
if len(form.Title) > 0 && form.AddTagMsg {
msg = form.Title + "\n\n" + form.Content
} }
}
if len(form.TagOnly) > 0 { // prepare the git message for creating a new tag
if err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, form.Target, form.TagName, msg); err != nil { newTagMsg := ""
if release_service.IsErrTagAlreadyExists(err) { if form.Title != "" && form.AddTagMsg {
e := err.(release_service.ErrTagAlreadyExists) newTagMsg = form.Title + "\n\n" + form.Content
ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) }
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL())
return
}
if release_service.IsErrInvalidTagName(err) {
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_invalid"))
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL())
return
}
if release_service.IsErrProtectedTagName(err) {
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL())
return
}
ctx.ServerError("release_service.CreateNewTag", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.TagName)) // no release, and tag only
ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.TagName)) if rel == nil && form.TagOnly {
if err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, form.Target, form.TagName, newTagMsg); err != nil {
handleTagReleaseError(err)
return return
} }
ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.TagName))
ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.TagName))
return
}
attachmentUUIDs := util.Iif(setting.Attachment.Enabled, form.Files, nil)
// no existing release, create a new release
if rel == nil {
rel = &repo_model.Release{ rel = &repo_model.Release{
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
Repo: ctx.Repo.Repository, Repo: ctx.Repo.Repository,
@ -469,48 +480,39 @@ func NewReleasePost(ctx *context.Context) {
TagName: form.TagName, TagName: form.TagName,
Target: form.Target, Target: form.Target,
Note: form.Content, Note: form.Content,
IsDraft: len(form.Draft) > 0, IsDraft: form.Draft,
IsPrerelease: form.Prerelease, IsPrerelease: form.Prerelease,
IsTag: false, IsTag: false,
} }
if err = release_service.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, newTagMsg); err != nil {
if err = release_service.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, msg); err != nil { handleTagReleaseError(err)
ctx.Data["Err_TagName"] = true
switch {
case repo_model.IsErrReleaseAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
case release_service.IsErrInvalidTagName(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
case release_service.IsErrProtectedTagName(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form)
default:
ctx.ServerError("CreateRelease", err)
}
return
}
} else {
if !rel.IsTag {
ctx.Data["Err_TagName"] = true
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
return return
} }
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
return
}
rel.Title = form.Title // tag exists, try to convert it to a real release
rel.Note = form.Content // old logic: if the release is not a tag (it is a real release), do not update it on the "new release" page
rel.Target = form.Target // add new logic: if tag-only, do not convert the tag to a release
rel.IsDraft = len(form.Draft) > 0 if form.TagOnly || !rel.IsTag {
rel.IsPrerelease = form.Prerelease ctx.Data["Err_TagName"] = true
rel.PublisherID = ctx.Doer.ID ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
rel.IsTag = false return
if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, attachmentUUIDs, nil, nil); err != nil {
ctx.Data["Err_TagName"] = true
ctx.ServerError("UpdateRelease", err)
return
}
} }
log.Trace("Release created: %s/%s:%s", ctx.Doer.LowerName, ctx.Repo.Repository.Name, form.TagName)
// convert a tag to a real release (set is_tag=false)
rel.Title = form.Title
rel.Note = form.Content
rel.Target = form.Target
rel.IsDraft = form.Draft
rel.IsPrerelease = form.Prerelease
rel.PublisherID = ctx.Doer.ID
rel.IsTag = false
if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, attachmentUUIDs, nil, nil); err != nil {
handleTagReleaseError(err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/releases") ctx.Redirect(ctx.Repo.RepoLink + "/releases")
} }

@ -11,60 +11,135 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/contexttest" "code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestNewReleasePost(t *testing.T) { func TestNewReleasePost(t *testing.T) {
for _, testCase := range []struct { unittest.PrepareTestEnv(t)
RepoID int64
UserID int64
TagName string
Form forms.NewReleaseForm
}{
{
RepoID: 1,
UserID: 2,
TagName: "v1.1", // pre-existing tag
Form: forms.NewReleaseForm{
TagName: "newtag",
Target: "master",
Title: "title",
Content: "content",
},
},
{
RepoID: 1,
UserID: 2,
TagName: "newtag",
Form: forms.NewReleaseForm{
TagName: "newtag",
Target: "master",
Title: "title",
Content: "content",
},
},
} {
unittest.PrepareTestEnv(t)
get := func(t *testing.T, tagName string) *context.Context {
ctx, _ := contexttest.MockContext(t, "user2/repo1/releases/new?tag="+tagName)
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
contexttest.LoadGitRepo(t, ctx)
defer ctx.Repo.GitRepo.Close()
NewRelease(ctx)
return ctx
}
t.Run("NewReleasePage", func(t *testing.T) {
ctx := get(t, "v1.1")
assert.Empty(t, ctx.Data["ShowCreateTagOnlyButton"])
ctx = get(t, "new-tag-name")
assert.NotEmpty(t, ctx.Data["ShowCreateTagOnlyButton"])
})
post := func(t *testing.T, form forms.NewReleaseForm) *context.Context {
ctx, _ := contexttest.MockContext(t, "user2/repo1/releases/new") ctx, _ := contexttest.MockContext(t, "user2/repo1/releases/new")
contexttest.LoadUser(t, ctx, 2) contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
contexttest.LoadGitRepo(t, ctx) contexttest.LoadGitRepo(t, ctx)
web.SetForm(ctx, &testCase.Form) defer ctx.Repo.GitRepo.Close()
web.SetForm(ctx, &form)
NewReleasePost(ctx) NewReleasePost(ctx)
unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ return ctx
RepoID: 1, }
PublisherID: 2,
TagName: testCase.Form.TagName, loadRelease := func(t *testing.T, tagName string) *repo_model.Release {
Target: testCase.Form.Target, return unittest.GetBean(t, &repo_model.Release{}, unittest.Cond("repo_id=1 AND tag_name=?", tagName))
Title: testCase.Form.Title,
Note: testCase.Form.Content,
}, unittest.Cond("is_draft=?", len(testCase.Form.Draft) > 0))
ctx.Repo.GitRepo.Close()
} }
t.Run("NewTagRelease", func(t *testing.T) {
post(t, forms.NewReleaseForm{
TagName: "newtag",
Target: "master",
Title: "title",
Content: "content",
})
rel := loadRelease(t, "newtag")
require.NotNil(t, rel)
assert.False(t, rel.IsTag)
assert.Equal(t, "master", rel.Target)
assert.Equal(t, "title", rel.Title)
assert.Equal(t, "content", rel.Note)
})
t.Run("ReleaseExistsDoUpdate(non-tag)", func(t *testing.T) {
ctx := post(t, forms.NewReleaseForm{
TagName: "v1.1",
Target: "master",
Title: "updated-title",
Content: "updated-content",
})
rel := loadRelease(t, "v1.1")
require.NotNil(t, rel)
assert.False(t, rel.IsTag)
assert.Equal(t, "testing-release", rel.Title)
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
})
t.Run("ReleaseExistsDoUpdate(tag-only)", func(t *testing.T) {
ctx := post(t, forms.NewReleaseForm{
TagName: "delete-tag", // a strange name, but it is the only "is_tag=true" fixture
Target: "master",
Title: "updated-title",
Content: "updated-content",
TagOnly: true,
})
rel := loadRelease(t, "delete-tag")
require.NotNil(t, rel)
assert.True(t, rel.IsTag) // the record should not be updated because the request is "tag-only". TODO: need to improve the logic?
assert.Equal(t, "delete-tag", rel.Title)
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
assert.NotEmpty(t, ctx.Data["ShowCreateTagOnlyButton"]) // still show the "tag-only" button
})
t.Run("ReleaseExistsDoUpdate(tag-release)", func(t *testing.T) {
ctx := post(t, forms.NewReleaseForm{
TagName: "delete-tag", // a strange name, but it is the only "is_tag=true" fixture
Target: "master",
Title: "updated-title",
Content: "updated-content",
})
rel := loadRelease(t, "delete-tag")
require.NotNil(t, rel)
assert.False(t, rel.IsTag) // the tag has been "updated" to be a real "release"
assert.Equal(t, "updated-title", rel.Title)
assert.Empty(t, ctx.Flash.ErrorMsg)
})
t.Run("TagOnly", func(t *testing.T) {
ctx := post(t, forms.NewReleaseForm{
TagName: "new-tag-only",
Target: "master",
Title: "title",
Content: "content",
TagOnly: true,
})
rel := loadRelease(t, "new-tag-only")
require.NotNil(t, rel)
assert.True(t, rel.IsTag)
assert.Empty(t, ctx.Flash.ErrorMsg)
})
t.Run("TagOnlyConflict", func(t *testing.T) {
ctx := post(t, forms.NewReleaseForm{
TagName: "v1.1",
Target: "master",
Title: "title",
Content: "content",
TagOnly: true,
})
rel := loadRelease(t, "v1.1")
require.NotNil(t, rel)
assert.False(t, rel.IsTag)
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
})
} }
func TestCalReleaseNumCommitsBehind(t *testing.T) { func TestCalReleaseNumCommitsBehind(t *testing.T) {

@ -53,7 +53,7 @@ func MustBeNotEmpty(ctx *context.Context) {
// MustBeEditable check that repo can be edited // MustBeEditable check that repo can be edited
func MustBeEditable(ctx *context.Context) { func MustBeEditable(ctx *context.Context) {
if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit { if !ctx.Repo.Repository.CanEnableEditor() {
ctx.NotFound("", nil) ctx.NotFound("", nil)
return return
} }

@ -49,6 +49,15 @@ const (
tplDeployKeys templates.TplName = "repo/settings/deploy_keys" tplDeployKeys templates.TplName = "repo/settings/deploy_keys"
) )
func parseEveryoneAccessMode(permission string, allowed ...perm.AccessMode) perm.AccessMode {
// if site admin forces repositories to be private, then do not allow any other access mode,
// otherwise the "force private" setting would be bypassed
if setting.Repository.ForcePrivate {
return perm.AccessModeNone
}
return perm.ParseAccessMode(permission, allowed...)
}
// SettingsCtxData is a middleware that sets all the general context data for the // SettingsCtxData is a middleware that sets all the general context data for the
// settings template. // settings template.
func SettingsCtxData(ctx *context.Context) { func SettingsCtxData(ctx *context.Context) {
@ -447,8 +456,9 @@ func SettingsPost(ctx *context.Context) {
if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() { if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
units = append(units, repo_model.RepoUnit{ units = append(units, repo_model.RepoUnit{
RepoID: repo.ID, RepoID: repo.ID,
Type: unit_model.TypeCode, Type: unit_model.TypeCode,
EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultCodeEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
}) })
} else if !unit_model.TypeCode.UnitGlobalDisabled() { } else if !unit_model.TypeCode.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
@ -474,7 +484,7 @@ func SettingsPost(ctx *context.Context) {
RepoID: repo.ID, RepoID: repo.ID,
Type: unit_model.TypeWiki, Type: unit_model.TypeWiki,
Config: new(repo_model.UnitConfig), Config: new(repo_model.UnitConfig),
EveryoneAccessMode: perm.ParseAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite), EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite),
}) })
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
} else { } else {
@ -524,6 +534,7 @@ func SettingsPost(ctx *context.Context) {
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime, AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
EnableDependencies: form.EnableIssueDependencies, EnableDependencies: form.EnableIssueDependencies,
}, },
EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultIssuesEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
}) })
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
} else { } else {

@ -251,7 +251,7 @@ func handleRepoEmptyOrBroken(ctx *context.Context) {
} else if reallyEmpty { } else if reallyEmpty {
showEmpty = true // the repo is really empty showEmpty = true // the repo is really empty
updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady) updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
} else if ctx.Repo.Commit == nil { } else if branches, _, _ := ctx.Repo.GitRepo.GetBranches(0, 1); len(branches) == 0 {
showEmpty = true // it is not really empty, but there is no branch showEmpty = true // it is not really empty, but there is no branch
// at the moment, other repo units like "actions" are not able to handle such case, // at the moment, other repo units like "actions" are not able to handle such case,
// so we just mark the repo as empty to prevent from displaying these units. // so we just mark the repo as empty to prevent from displaying these units.

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/metrics" "code.gitea.io/gitea/modules/metrics"
"code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/public"
@ -101,7 +102,7 @@ func buildAuthGroup() *auth_service.Group {
group.Add(&auth_service.Basic{}) // FIXME: this should be removed and only applied in download and git/lfs routers group.Add(&auth_service.Basic{}) // FIXME: this should be removed and only applied in download and git/lfs routers
if setting.Service.EnableReverseProxyAuth { if setting.Service.EnableReverseProxyAuth {
group.Add(&auth_service.ReverseProxy{}) // reverseproxy should before Session, otherwise the header will be ignored if user has login group.Add(&auth_service.ReverseProxy{}) // reverse-proxy should before Session, otherwise the header will be ignored if user has login
} }
group.Add(&auth_service.Session{}) group.Add(&auth_service.Session{})
@ -816,21 +817,23 @@ func registerRoutes(m *web.Router) {
m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.Action) m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.Action)
reqRepoAdmin := context.RequireRepoAdmin() reqRepoAdmin := context.RequireRepoAdmin()
reqRepoCodeWriter := context.RequireRepoWriter(unit.TypeCode) reqRepoCodeWriter := context.RequireUnitWriter(unit.TypeCode)
canEnableEditor := context.CanEnableEditor() reqRepoReleaseWriter := context.RequireUnitWriter(unit.TypeReleases)
reqRepoCodeReader := context.RequireRepoReader(unit.TypeCode) reqRepoReleaseReader := context.RequireUnitReader(unit.TypeReleases)
reqRepoReleaseWriter := context.RequireRepoWriter(unit.TypeReleases) reqRepoIssuesOrPullsWriter := context.RequireUnitWriter(unit.TypeIssues, unit.TypePullRequests)
reqRepoReleaseReader := context.RequireRepoReader(unit.TypeReleases) reqRepoIssuesOrPullsReader := context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)
reqRepoWikiReader := context.RequireRepoReader(unit.TypeWiki) reqRepoProjectsReader := context.RequireUnitReader(unit.TypeProjects)
reqRepoWikiWriter := context.RequireRepoWriter(unit.TypeWiki) reqRepoProjectsWriter := context.RequireUnitWriter(unit.TypeProjects)
reqRepoIssueReader := context.RequireRepoReader(unit.TypeIssues) reqRepoActionsReader := context.RequireUnitReader(unit.TypeActions)
reqRepoPullsReader := context.RequireRepoReader(unit.TypePullRequests) reqRepoActionsWriter := context.RequireUnitWriter(unit.TypeActions)
reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(unit.TypeIssues, unit.TypePullRequests)
reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(unit.TypeIssues, unit.TypePullRequests) // the legacy names "reqRepoXxx" should be renamed to the correct name "reqUnitXxx", these permissions are for units, not repos
reqRepoProjectsReader := context.RequireRepoReader(unit.TypeProjects) reqUnitsWithMarkdown := context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases, unit.TypeWiki)
reqRepoProjectsWriter := context.RequireRepoWriter(unit.TypeProjects) reqUnitCodeReader := context.RequireUnitReader(unit.TypeCode)
reqRepoActionsReader := context.RequireRepoReader(unit.TypeActions) reqUnitIssuesReader := context.RequireUnitReader(unit.TypeIssues)
reqRepoActionsWriter := context.RequireRepoWriter(unit.TypeActions) reqUnitPullsReader := context.RequireUnitReader(unit.TypePullRequests)
reqUnitWikiReader := context.RequireUnitReader(unit.TypeWiki)
reqUnitWikiWriter := context.RequireUnitWriter(unit.TypeWiki)
reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) { reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) {
return func(ctx *context.Context) { return func(ctx *context.Context) {
@ -1054,7 +1057,7 @@ func registerRoutes(m *web.Router) {
m.Group("/migrate", func() { m.Group("/migrate", func() {
m.Get("/status", repo.MigrateStatus) m.Get("/status", repo.MigrateStatus)
}) })
}, optSignIn, context.RepoAssignment, reqRepoCodeReader) }, optSignIn, context.RepoAssignment, reqUnitCodeReader)
// end "/{username}/{reponame}/-": migrate // end "/{username}/{reponame}/-": migrate
m.Group("/{username}/{reponame}/settings", func() { m.Group("/{username}/{reponame}/settings", func() {
@ -1150,61 +1153,59 @@ func registerRoutes(m *web.Router) {
// end "/{username}/{reponame}/settings" // end "/{username}/{reponame}/settings"
// user/org home, including rss feeds // user/org home, including rss feeds
m.Get("/{username}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRef(), repo.SetEditorconfigIfExists, repo.Home) m.Get("/{username}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home)
// TODO: maybe it should relax the permission to allow "any access" m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup)
m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases, unit.TypeWiki), web.Bind(structs.MarkupOption{}), misc.Markup)
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
m.Get("/find/*", repo.FindFiles) m.Get("/find/*", repo.FindFiles)
m.Group("/tree-list", func() { // for find files m.Group("/tree-list", func() {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.TreeList) m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeList)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.TreeList) m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList)
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.TreeList) m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.TreeList)
}) })
m.Group("/tree", func() { m.Group("/tree", func() {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Tree) m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.Tree)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Tree) m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.Tree)
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.Tree) m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.Tree)
}) })
m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff) m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff)
m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists). m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists).
Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff). Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
Post(reqSignIn, context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) Post(reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost)
}, optSignIn, context.RepoAssignment, reqRepoCodeReader) }, optSignIn, context.RepoAssignment, reqUnitCodeReader)
// end "/{username}/{reponame}": repo code: find, compare, list // end "/{username}/{reponame}": repo code: find, compare, list
addIssuesPullsViewRoutes := func() {
// for /{username}/{reponame}/issues" or "/{username}/{reponame}/pulls"
m.Get("/posters", repo.IssuePullPosters)
m.Group("/{index}", func() {
m.Get("/info", repo.GetIssueInfo)
m.Get("/attachments", repo.GetIssueAttachments)
m.Get("/attachments/{uuid}", repo.GetAttachment)
m.Group("/content-history", func() {
m.Get("/overview", repo.GetContentHistoryOverview)
m.Get("/list", repo.GetContentHistoryList)
m.Get("/detail", repo.GetContentHistoryDetail)
})
})
}
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because it would conflict with other routes like "/pulls/{index}"
m.Get("/pulls/posters", repo.PullPosters)
m.Get("/comments/{id}/attachments", repo.GetCommentAttachments) m.Get("/comments/{id}/attachments", repo.GetCommentAttachments)
m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels) m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels)
m.Get("/milestones", repo.Milestones) m.Get("/milestones", repo.Milestones)
m.Get("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls) m.Get("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls)
m.Group("/{type:issues|pulls}", func() {
m.Group("/{index}", func() {
m.Get("/info", repo.GetIssueInfo)
m.Get("/attachments", repo.GetIssueAttachments)
m.Get("/attachments/{uuid}", repo.GetAttachment)
m.Group("/content-history", func() {
m.Get("/overview", repo.GetContentHistoryOverview)
m.Get("/list", repo.GetContentHistoryList)
m.Get("/detail", repo.GetContentHistoryDetail)
})
})
}, context.RepoRef())
m.Get("/issues/suggestions", repo.IssueSuggestions) m.Get("/issues/suggestions", repo.IssueSuggestions)
}, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) }, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones
m.Group("/{username}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitIssuesReader)
m.Group("/{username}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader)
// end "/{username}/{reponame}": view milestone, label, issue, pull, etc // end "/{username}/{reponame}": view milestone, label, issue, pull, etc
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}/{type:issues}", func() {
m.Group("/{type:issues|pulls}", func() { m.Get("", repo.Issues)
m.Get("", repo.Issues) m.Get("/{index}", repo.ViewIssue)
m.Group("/{index}", func() { }, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker))
m.Get("", repo.ViewIssue)
})
})
}, optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeIssues, unit.TypePullRequests, unit.TypeExternalTracker))
// end "/{username}/{reponame}": issue/pull list, issue/pull view, external tracker // end "/{username}/{reponame}": issue/pull list, issue/pull view, external tracker
m.Group("/{username}/{reponame}", func() { // edit issues, pulls, labels, milestones, etc m.Group("/{username}/{reponame}", func() { // edit issues, pulls, labels, milestones, etc
@ -1215,11 +1216,10 @@ func registerRoutes(m *web.Router) {
m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate) m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
}) })
m.Get("/search", repo.SearchRepoIssuesJSON) m.Get("/search", repo.SearchRepoIssuesJSON)
}, context.RepoMustNotBeArchived(), reqRepoIssueReader) }, reqUnitIssuesReader)
// FIXME: should use different URLs but mostly same logic for comments of issue and pull request. addIssuesPullsRoutes := func() {
// So they can apply their own enable/disable logic on routers. // for "/{username}/{reponame}/issues" or "/{username}/{reponame}/pulls"
m.Group("/{type:issues|pulls}", func() {
m.Group("/{index}", func() { m.Group("/{index}", func() {
m.Post("/title", repo.UpdateIssueTitle) m.Post("/title", repo.UpdateIssueTitle)
m.Post("/content", repo.UpdateIssueContent) m.Post("/content", repo.UpdateIssueContent)
@ -1246,39 +1246,37 @@ func registerRoutes(m *web.Router) {
m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue) m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue)
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue) m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
m.Post("/delete", reqRepoAdmin, repo.DeleteIssue) m.Post("/delete", reqRepoAdmin, repo.DeleteIssue)
}, context.RepoMustNotBeArchived())
m.Group("/{index}", func() {
m.Post("/content-history/soft-delete", repo.SoftDeleteContentHistory) m.Post("/content-history/soft-delete", repo.SoftDeleteContentHistory)
}) })
m.Post("/attachments", repo.UploadIssueAttachment)
m.Post("/attachments/remove", repo.DeleteAttachment)
m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone) m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone)
m.Post("/projects", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProject) m.Post("/projects", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProject)
m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee) m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
m.Post("/request_review", repo.UpdatePullReviewRequest)
m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview)
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus) m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues) m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
m.Post("/resolve_conversation", repo.SetShowOutdatedComments, repo.UpdateResolveConversation)
m.Post("/attachments", repo.UploadIssueAttachment)
m.Post("/attachments/remove", repo.DeleteAttachment)
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin) m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove) m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
}, context.RepoMustNotBeArchived()) }
m.Group("/{type:issues}", addIssuesPullsRoutes, reqUnitIssuesReader, context.RepoMustNotBeArchived())
m.Group("/{type:pulls}", addIssuesPullsRoutes, reqUnitPullsReader, context.RepoMustNotBeArchived())
m.Group("/comments/{id}", func() { m.Group("/comments/{id}", func() {
m.Post("", repo.UpdateCommentContent) m.Post("", repo.UpdateCommentContent)
m.Post("/delete", repo.DeleteComment) m.Post("/delete", repo.DeleteComment)
m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeCommentReaction) m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeCommentReaction)
}, context.RepoMustNotBeArchived()) }, reqRepoIssuesOrPullsReader) // edit issue/pull comment
m.Group("/labels", func() { m.Group("/labels", func() {
m.Post("/new", web.Bind(forms.CreateLabelForm{}), repo.NewLabel) m.Post("/new", web.Bind(forms.CreateLabelForm{}), repo.NewLabel)
m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel) m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel)
m.Post("/delete", repo.DeleteLabel) m.Post("/delete", repo.DeleteLabel)
m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), repo.InitializeLabels) m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), repo.InitializeLabels)
}, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef()) }, reqRepoIssuesOrPullsWriter, context.RepoRef())
m.Group("/milestones", func() { m.Group("/milestones", func() {
m.Combo("/new").Get(repo.NewMilestone). m.Combo("/new").Get(repo.NewMilestone).
Post(web.Bind(forms.CreateMilestoneForm{}), repo.NewMilestonePost) Post(web.Bind(forms.CreateMilestoneForm{}), repo.NewMilestonePost)
@ -1286,11 +1284,16 @@ func registerRoutes(m *web.Router) {
m.Post("/{id}/edit", web.Bind(forms.CreateMilestoneForm{}), repo.EditMilestonePost) m.Post("/{id}/edit", web.Bind(forms.CreateMilestoneForm{}), repo.EditMilestonePost)
m.Post("/{id}/{action}", repo.ChangeMilestoneStatus) m.Post("/{id}/{action}", repo.ChangeMilestoneStatus)
m.Post("/delete", repo.DeleteMilestone) m.Post("/delete", repo.DeleteMilestone)
}, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef()) }, reqRepoIssuesOrPullsWriter, context.RepoRef())
m.Group("/pull", func() {
m.Post("/{index}/target_branch", repo.UpdatePullRequestTarget) // FIXME: need to move these routes to the proper place
}, context.RepoMustNotBeArchived()) m.Group("/issues", func() {
}, reqSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) m.Post("/request_review", repo.UpdatePullReviewRequest)
m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview)
m.Post("/resolve_conversation", repo.SetShowOutdatedComments, repo.UpdateResolveConversation)
}, reqUnitPullsReader)
m.Post("/pull/{index}/target_branch", reqUnitPullsReader, repo.UpdatePullRequestTarget)
}, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived())
// end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones // end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones
m.Group("/{username}/{reponame}", func() { // repo code m.Group("/{username}/{reponame}", func() { // repo code
@ -1310,18 +1313,18 @@ func registerRoutes(m *web.Router) {
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick). m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick).
Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
}, repo.MustBeEditable) }, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch())
m.Group("", func() { m.Group("", func() {
m.Post("/upload-file", repo.UploadFileToServer) m.Post("/upload-file", repo.UploadFileToServer)
m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
}, repo.MustBeEditable, repo.MustBeAbleToUpload) }, repo.MustBeAbleToUpload, reqRepoCodeWriter)
}, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived()) }, repo.MustBeEditable, context.RepoMustNotBeArchived())
m.Group("/branches", func() { m.Group("/branches", func() {
m.Group("/_new", func() { m.Group("/_new", func() {
m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch) m.Post("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.CreateBranch)
m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch) m.Post("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.CreateBranch)
m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch) m.Post("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.CreateBranch)
}, web.Bind(forms.NewBranchForm{})) }, web.Bind(forms.NewBranchForm{}))
m.Post("/delete", repo.DeleteBranchPost) m.Post("/delete", repo.DeleteBranchPost)
m.Post("/restore", repo.RestoreBranchPost) m.Post("/restore", repo.RestoreBranchPost)
@ -1330,45 +1333,42 @@ func registerRoutes(m *web.Router) {
}, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty) }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
m.Combo("/fork").Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) m.Combo("/fork").Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
}, reqSignIn, context.RepoAssignment, reqRepoCodeReader) }, reqSignIn, context.RepoAssignment, reqUnitCodeReader)
// end "/{username}/{reponame}": repo code // end "/{username}/{reponame}": repo code
m.Group("/{username}/{reponame}", func() { // repo tags m.Group("/{username}/{reponame}", func() { // repo tags
m.Group("/tags", func() { m.Group("/tags", func() {
m.Get("", repo.TagsList) m.Get("", repo.TagsList)
m.Get("/list", repo.GetTagList)
m.Get(".rss", feedEnabled, repo.TagsListFeedRSS) m.Get(".rss", feedEnabled, repo.TagsListFeedRSS)
m.Get(".atom", feedEnabled, repo.TagsListFeedAtom) m.Get(".atom", feedEnabled, repo.TagsListFeedAtom)
}, ctxDataSet("EnableFeed", setting.Other.EnableFeed), m.Get("/list", repo.GetTagList)
repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, context.RepoRefByTypeOptions{IgnoreNotExistErr: true})) }, ctxDataSet("EnableFeed", setting.Other.EnableFeed))
m.Post("/tags/delete", repo.DeleteTag, reqSignIn, m.Post("/tags/delete", reqSignIn, reqRepoCodeWriter, context.RepoMustNotBeArchived(), repo.DeleteTag)
repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, context.RepoRef()) }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader)
}, optSignIn, context.RepoAssignment, reqRepoCodeReader)
// end "/{username}/{reponame}": repo tags // end "/{username}/{reponame}": repo tags
m.Group("/{username}/{reponame}", func() { // repo releases m.Group("/{username}/{reponame}", func() { // repo releases
m.Group("/releases", func() { m.Group("/releases", func() {
m.Get("", repo.Releases) m.Get("", repo.Releases)
m.Get("/tag/*", repo.SingleRelease)
m.Get("/latest", repo.LatestRelease)
m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS) m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS)
m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom) m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom)
}, ctxDataSet("EnableFeed", setting.Other.EnableFeed), m.Get("/tag/*", repo.SingleRelease)
repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, context.RepoRefByTypeOptions{IgnoreNotExistErr: true})) m.Get("/latest", repo.LatestRelease)
m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment) }, ctxDataSet("EnableFeed", setting.Other.EnableFeed))
m.Get("/releases/download/{vTag}/{fileName}", repo.MustBeNotEmpty, repo.RedirectDownload) m.Get("/releases/attachments/{uuid}", repo.GetAttachment)
m.Get("/releases/download/{vTag}/{fileName}", repo.RedirectDownload)
m.Group("/releases", func() { m.Group("/releases", func() {
m.Get("/new", repo.NewRelease) m.Get("/new", repo.NewRelease)
m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost) m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)
m.Post("/delete", repo.DeleteRelease) m.Post("/delete", repo.DeleteRelease)
m.Post("/attachments", repo.UploadReleaseAttachment) m.Post("/attachments", repo.UploadReleaseAttachment)
m.Post("/attachments/remove", repo.DeleteAttachment) m.Post("/attachments/remove", repo.DeleteAttachment)
}, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef()) }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef())
m.Group("/releases", func() { m.Group("/releases", func() {
m.Get("/edit/*", repo.EditRelease) m.Get("/edit/*", repo.EditRelease)
m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost) m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost)
}, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache) }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache)
}, optSignIn, context.RepoAssignment, reqRepoReleaseReader) }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader)
// end "/{username}/{reponame}": repo releases // end "/{username}/{reponame}": repo releases
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
@ -1446,43 +1446,48 @@ func registerRoutes(m *web.Router) {
m.Group("/{username}/{reponame}/wiki", func() { m.Group("/{username}/{reponame}/wiki", func() {
m.Combo(""). m.Combo("").
Get(repo.Wiki). Get(repo.Wiki).
Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) Post(context.RepoMustNotBeArchived(), reqSignIn, reqUnitWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost)
m.Combo("/*"). m.Combo("/*").
Get(repo.Wiki). Get(repo.Wiki).
Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) Post(context.RepoMustNotBeArchived(), reqSignIn, reqUnitWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost)
m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob)
m.Get("/commit/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) m.Get("/commit/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
m.Get("/commit/{sha:[a-f0-9]{7,64}}.{ext:patch|diff}", repo.RawDiff) m.Get("/commit/{sha:[a-f0-9]{7,64}}.{ext:patch|diff}", repo.RawDiff)
m.Get("/raw/*", repo.WikiRaw) m.Get("/raw/*", repo.WikiRaw)
}, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqRepoWikiReader, func(ctx *context.Context) { }, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) {
ctx.Data["PageIsWiki"] = true ctx.Data["PageIsWiki"] = true
ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink(ctx, ctx.Doer) ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink(ctx, ctx.Doer)
}) })
// end "/{username}/{reponame}/wiki" // end "/{username}/{reponame}/wiki"
m.Group("/{username}/{reponame}/activity", func() { m.Group("/{username}/{reponame}/activity", func() {
// activity has its own permission checks
m.Get("", repo.Activity) m.Get("", repo.Activity)
m.Get("/{period}", repo.Activity) m.Get("/{period}", repo.Activity)
m.Group("/contributors", func() {
m.Get("", repo.Contributors) m.Group("", func() {
m.Get("/data", repo.ContributorsData) m.Group("/contributors", func() {
}) m.Get("", repo.Contributors)
m.Group("/code-frequency", func() { m.Get("/data", repo.ContributorsData)
m.Get("", repo.CodeFrequency) })
m.Get("/data", repo.CodeFrequencyData) m.Group("/code-frequency", func() {
}) m.Get("", repo.CodeFrequency)
m.Group("/recent-commits", func() { m.Get("/data", repo.CodeFrequencyData)
m.Get("", repo.RecentCommits) })
m.Get("/data", repo.RecentCommitsData) m.Group("/recent-commits", func() {
}) m.Get("", repo.RecentCommits)
m.Get("/data", repo.RecentCommitsData)
})
}, reqUnitCodeReader)
}, },
optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases), optSignIn, context.RepoAssignment, repo.MustBeNotEmpty,
context.RepoRef(), repo.MustBeNotEmpty, context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases),
) )
// end "/{username}/{reponame}/activity" // end "/{username}/{reponame}/activity"
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
m.Group("/pulls/{index}", func() { m.Get("/{type:pulls}", repo.Issues)
m.Group("/{type:pulls}/{index}", func() {
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue) m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue)
m.Get(".diff", repo.DownloadPullDiff) m.Get(".diff", repo.DownloadPullDiff)
m.Get(".patch", repo.DownloadPullPatch) m.Get(".patch", repo.DownloadPullPatch)
@ -1507,7 +1512,7 @@ func registerRoutes(m *web.Router) {
}, context.RepoMustNotBeArchived()) }, context.RepoMustNotBeArchived())
}) })
}) })
}, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqRepoPullsReader) }, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader)
// end "/{username}/{reponame}/pulls/{index}": repo pull request // end "/{username}/{reponame}/pulls/{index}": repo pull request
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
@ -1527,42 +1532,39 @@ func registerRoutes(m *web.Router) {
}, repo.MustBeNotEmpty, context.RepoRef()) }, repo.MustBeNotEmpty, context.RepoRef())
m.Group("/media", func() { m.Group("/media", func() {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownloadOrLFS)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownloadOrLFS)
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownloadOrLFS)
m.Get("/blob/{sha}", repo.DownloadByIDOrLFS) m.Get("/blob/{sha}", repo.DownloadByIDOrLFS)
// "/*" route is deprecated, and kept for backward compatibility m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.SingleDownloadOrLFS)
m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.SingleDownloadOrLFS) m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.SingleDownloadOrLFS)
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.SingleDownloadOrLFS)
m.Get("/*", context.RepoRefByType(""), repo.SingleDownloadOrLFS) // "/*" route is deprecated, and kept for backward compatibility
}, repo.MustBeNotEmpty) }, repo.MustBeNotEmpty)
m.Group("/raw", func() { m.Group("/raw", func() {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownload)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownload)
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownload)
m.Get("/blob/{sha}", repo.DownloadByID) m.Get("/blob/{sha}", repo.DownloadByID)
// "/*" route is deprecated, and kept for backward compatibility m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.SingleDownload)
m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.SingleDownload) m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.SingleDownload)
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.SingleDownload)
m.Get("/*", context.RepoRefByType(""), repo.SingleDownload) // "/*" route is deprecated, and kept for backward compatibility
}, repo.MustBeNotEmpty) }, repo.MustBeNotEmpty)
m.Group("/render", func() { m.Group("/render", func() {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RenderFile) m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.RenderFile)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RenderFile) m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.RenderFile)
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RenderFile) m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.RenderFile)
m.Get("/blob/{sha}", repo.RenderFile) m.Get("/blob/{sha}", repo.RenderFile)
}, repo.MustBeNotEmpty) }, repo.MustBeNotEmpty)
m.Group("/commits", func() { m.Group("/commits", func() {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits) m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.RefCommits)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits) m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.RefCommits)
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefCommits) m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.RefCommits)
// "/*" route is deprecated, and kept for backward compatibility m.Get("/*", context.RepoRefByType(""), repo.RefCommits) // "/*" route is deprecated, and kept for backward compatibility
m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.RefCommits)
}, repo.MustBeNotEmpty) }, repo.MustBeNotEmpty)
m.Group("/blame", func() { m.Group("/blame", func() {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefBlame) m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.RefBlame)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefBlame) m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.RefBlame)
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefBlame) m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.RefBlame)
}, repo.MustBeNotEmpty) }, repo.MustBeNotEmpty)
m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob)
@ -1574,27 +1576,27 @@ func registerRoutes(m *web.Router) {
m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick) m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
}, repo.MustBeNotEmpty, context.RepoRef()) }, repo.MustBeNotEmpty, context.RepoRef())
m.Get("/rss/branch/*", context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed) m.Get("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeed)
m.Get("/atom/branch/*", context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed) m.Get("/atom/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeed)
m.Group("/src", func() { m.Group("/src", func() {
m.Get("", func(ctx *context.Context) { ctx.Redirect(ctx.Repo.RepoLink) }) // there is no "{owner}/{repo}/src" page, so redirect to "{owner}/{repo}" to avoid 404 m.Get("", func(ctx *context.Context) { ctx.Redirect(ctx.Repo.RepoLink) }) // there is no "{owner}/{repo}/src" page, so redirect to "{owner}/{repo}" to avoid 404
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home) m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.Home)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Home) m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.Home)
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.Home) m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.Home)
m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.Home) // "/*" route is deprecated, and kept for backward compatibility m.Get("/*", context.RepoRefByType(""), repo.Home) // "/*" route is deprecated, and kept for backward compatibility
}, repo.SetEditorconfigIfExists) }, repo.SetEditorconfigIfExists)
m.Get("/forks", context.RepoRef(), repo.Forks) m.Get("/forks", context.RepoRef(), repo.Forks)
m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff) m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff)
m.Post("/lastcommit/*", context.RepoRefByType(context.RepoRefCommit), repo.LastCommit) m.Post("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit)
}, optSignIn, context.RepoAssignment, reqRepoCodeReader) }, optSignIn, context.RepoAssignment, reqUnitCodeReader)
// end "/{username}/{reponame}": repo code // end "/{username}/{reponame}": repo code
m.Group("/{username}/{reponame}", func() { m.Group("/{username}/{reponame}", func() {
m.Get("/stars", repo.Stars) m.Get("/stars", repo.Stars)
m.Get("/watchers", repo.Watchers) m.Get("/watchers", repo.Watchers)
m.Get("/search", reqRepoCodeReader, repo.Search) m.Get("/search", reqUnitCodeReader, repo.Search)
m.Post("/action/{action}", reqSignIn, repo.Action) m.Post("/action/{action}", reqSignIn, repo.Action)
}, optSignIn, context.RepoAssignment, context.RepoRef()) }, optSignIn, context.RepoAssignment, context.RepoRef())

@ -9,31 +9,20 @@ import (
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log"
) )
// RequireRepoAdmin returns a middleware for requiring repository admin permission // RequireRepoAdmin returns a middleware for requiring repository admin permission
func RequireRepoAdmin() func(ctx *Context) { func RequireRepoAdmin() func(ctx *Context) {
return func(ctx *Context) { return func(ctx *Context) {
if !ctx.IsSigned || !ctx.Repo.IsAdmin() { if !ctx.IsSigned || !ctx.Repo.IsAdmin() {
ctx.NotFound(ctx.Req.URL.RequestURI(), nil) ctx.NotFound("RequireRepoAdmin denies the request", nil)
return return
} }
} }
} }
// RequireRepoWriter returns a middleware for requiring repository write to the specify unitType // CanWriteToBranch checks if the user is allowed to write to the branch of the repo
func RequireRepoWriter(unitType unit.Type) func(ctx *Context) { func CanWriteToBranch() func(ctx *Context) {
return func(ctx *Context) {
if !ctx.Repo.CanWrite(unitType) {
ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
return
}
}
}
// CanEnableEditor checks if the user is allowed to write to the branch of the repo
func CanEnableEditor() func(ctx *Context) {
return func(ctx *Context) { return func(ctx *Context) {
if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
ctx.NotFound("CanWriteToBranch denies permission", nil) ctx.NotFound("CanWriteToBranch denies permission", nil)
@ -42,75 +31,30 @@ func CanEnableEditor() func(ctx *Context) {
} }
} }
// RequireRepoWriterOr returns a middleware for requiring repository write to one of the unit permission // RequireUnitWriter returns a middleware for requiring repository write to one of the unit permission
func RequireRepoWriterOr(unitTypes ...unit.Type) func(ctx *Context) { func RequireUnitWriter(unitTypes ...unit.Type) func(ctx *Context) {
return func(ctx *Context) { return func(ctx *Context) {
for _, unitType := range unitTypes { for _, unitType := range unitTypes {
if ctx.Repo.CanWrite(unitType) { if ctx.Repo.CanWrite(unitType) {
return return
} }
} }
ctx.NotFound(ctx.Req.URL.RequestURI(), nil) ctx.NotFound("RequireUnitWriter denies the request", nil)
} }
} }
// RequireRepoReader returns a middleware for requiring repository read to the specify unitType // RequireUnitReader returns a middleware for requiring repository write to one of the unit permission
func RequireRepoReader(unitType unit.Type) func(ctx *Context) { func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) {
return func(ctx *Context) {
if !ctx.Repo.CanRead(unitType) {
if unitType == unit.TypeCode && canWriteAsMaintainer(ctx) {
return
}
if log.IsTrace() {
if ctx.IsSigned {
log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
unitType,
ctx.Repo.Repository,
ctx.Repo.Permission)
} else {
log.Trace("Permission Denied: Anonymous user cannot read %-v in Repo %-v\n"+
"Anonymous user in Repo has Permissions: %-+v",
unitType,
ctx.Repo.Repository,
ctx.Repo.Permission)
}
}
ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
return
}
}
}
// RequireRepoReaderOr returns a middleware for requiring repository write to one of the unit permission
func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) {
return func(ctx *Context) { return func(ctx *Context) {
for _, unitType := range unitTypes { for _, unitType := range unitTypes {
if ctx.Repo.CanRead(unitType) { if ctx.Repo.CanRead(unitType) {
return return
} }
} if unitType == unit.TypeCode && canWriteAsMaintainer(ctx) {
if log.IsTrace() { return
var format string
var args []any
if ctx.IsSigned {
format = "Permission Denied: User %-v cannot read ["
args = append(args, ctx.Doer)
} else {
format = "Permission Denied: Anonymous user cannot read ["
}
for _, unit := range unitTypes {
format += "%-v, "
args = append(args, unit)
} }
format = format[:len(format)-2] + "] in Repo %-v\n" +
"User in Repo has Permissions: %-+v"
args = append(args, ctx.Repo.Repository, ctx.Repo.Permission)
log.Trace(format, args...)
} }
ctx.NotFound(ctx.Req.URL.RequestURI(), nil) ctx.NotFound("RequireUnitReader denies the request", nil)
} }
} }

@ -677,24 +677,12 @@ func RepoAssignment(ctx *Context) {
} }
} }
// RepoRefType type of repo reference
type RepoRefType int
const (
// RepoRefUnknown is for legacy support, makes the code to "guess" the ref type
RepoRefUnknown RepoRefType = iota
RepoRefBranch
RepoRefTag
RepoRefCommit
)
const headRefName = "HEAD" const headRefName = "HEAD"
// RepoRef handles repository reference names when the ref name is not
// explicitly given
func RepoRef() func(*Context) { func RepoRef() func(*Context) {
// since no ref name is explicitly specified, ok to just use branch // old code does: return RepoRefByType(git.RefTypeBranch)
return RepoRefByType(RepoRefBranch) // in most cases, it is an abuse, so we just disable it completely and fix the abuses one by one (if there is anything wrong)
return nil
} }
func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool) string { func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool) string {
@ -710,29 +698,29 @@ func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool
return "" return ""
} }
func getRefNameLegacy(ctx *Base, repo *Repository, reqPath, extraRef string) (string, RepoRefType) { func getRefNameLegacy(ctx *Base, repo *Repository, reqPath, extraRef string) (string, git.RefType) {
reqRefPath := path.Join(extraRef, reqPath) reqRefPath := path.Join(extraRef, reqPath)
reqRefPathParts := strings.Split(reqRefPath, "/") reqRefPathParts := strings.Split(reqRefPath, "/")
if refName := getRefName(ctx, repo, reqRefPath, RepoRefBranch); refName != "" { if refName := getRefName(ctx, repo, reqRefPath, git.RefTypeBranch); refName != "" {
return refName, RepoRefBranch return refName, git.RefTypeBranch
} }
if refName := getRefName(ctx, repo, reqRefPath, RepoRefTag); refName != "" { if refName := getRefName(ctx, repo, reqRefPath, git.RefTypeTag); refName != "" {
return refName, RepoRefTag return refName, git.RefTypeTag
} }
if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), reqRefPathParts[0]) { if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), reqRefPathParts[0]) {
// FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists
repo.TreePath = strings.Join(reqRefPathParts[1:], "/") repo.TreePath = strings.Join(reqRefPathParts[1:], "/")
return reqRefPathParts[0], RepoRefCommit return reqRefPathParts[0], git.RefTypeCommit
} }
// FIXME: the old code falls back to default branch if "ref" doesn't exist, there could be an edge case: // FIXME: the old code falls back to default branch if "ref" doesn't exist, there could be an edge case:
// "README?ref=no-such" would read the README file from the default branch, but the user might expect a 404 // "README?ref=no-such" would read the README file from the default branch, but the user might expect a 404
repo.TreePath = reqPath repo.TreePath = reqPath
return repo.Repository.DefaultBranch, RepoRefBranch return repo.Repository.DefaultBranch, git.RefTypeBranch
} }
func getRefName(ctx *Base, repo *Repository, path string, pathType RepoRefType) string { func getRefName(ctx *Base, repo *Repository, path string, refType git.RefType) string {
switch pathType { switch refType {
case RepoRefBranch: case git.RefTypeBranch:
ref := getRefNameFromPath(repo, path, repo.GitRepo.IsBranchExist) ref := getRefNameFromPath(repo, path, repo.GitRepo.IsBranchExist)
if len(ref) == 0 { if len(ref) == 0 {
// check if ref is HEAD // check if ref is HEAD
@ -762,9 +750,9 @@ func getRefName(ctx *Base, repo *Repository, path string, pathType RepoRefType)
} }
return ref return ref
case RepoRefTag: case git.RefTypeTag:
return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist) return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist)
case RepoRefCommit: case git.RefTypeCommit:
parts := strings.Split(path, "/") parts := strings.Split(path, "/")
if git.IsStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) { if git.IsStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) {
// FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists
@ -782,22 +770,18 @@ func getRefName(ctx *Base, repo *Repository, path string, pathType RepoRefType)
return commit.ID.String() return commit.ID.String()
} }
default: default:
panic(fmt.Sprintf("Unrecognized path type: %v", pathType)) panic(fmt.Sprintf("Unrecognized ref type: %v", refType))
} }
return "" return ""
} }
type RepoRefByTypeOptions struct { func repoRefFullName(typ git.RefType, shortName string) git.RefName {
IgnoreNotExistErr bool
}
func repoRefFullName(shortName string, typ RepoRefType) git.RefName {
switch typ { switch typ {
case RepoRefBranch: case git.RefTypeBranch:
return git.RefNameFromBranch(shortName) return git.RefNameFromBranch(shortName)
case RepoRefTag: case git.RefTypeTag:
return git.RefNameFromTag(shortName) return git.RefNameFromTag(shortName)
case RepoRefCommit: case git.RefTypeCommit:
return git.RefNameFromCommit(shortName) return git.RefNameFromCommit(shortName)
default: default:
setting.PanicInDevOrTesting("Unknown RepoRefType: %v", typ) setting.PanicInDevOrTesting("Unknown RepoRefType: %v", typ)
@ -807,8 +791,7 @@ func repoRefFullName(shortName string, typ RepoRefType) git.RefName {
// RepoRefByType handles repository reference name for a specific type // RepoRefByType handles repository reference name for a specific type
// of repository reference // of repository reference
func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func(*Context) { func RepoRefByType(detectRefType git.RefType) func(*Context) {
opt := util.OptionalArg(opts)
return func(ctx *Context) { return func(ctx *Context) {
var err error var err error
refType := detectRefType refType := detectRefType
@ -824,14 +807,6 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
return return
} }
if ctx.Repo.GitRepo == nil {
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError(fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err)
return
}
}
// Get default branch. // Get default branch.
var refShortName string var refShortName string
reqPath := ctx.PathParam("*") reqPath := ctx.PathParam("*")
@ -861,13 +836,13 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
} }
ctx.Repo.IsViewBranch = true ctx.Repo.IsViewBranch = true
} else { // there is a path in request } else { // there is a path in request
guessLegacyPath := refType == RepoRefUnknown guessLegacyPath := refType == ""
if guessLegacyPath { if guessLegacyPath {
refShortName, refType = getRefNameLegacy(ctx.Base, ctx.Repo, reqPath, "") refShortName, refType = getRefNameLegacy(ctx.Base, ctx.Repo, reqPath, "")
} else { } else {
refShortName = getRefName(ctx.Base, ctx.Repo, reqPath, refType) refShortName = getRefName(ctx.Base, ctx.Repo, reqPath, refType)
} }
ctx.Repo.RefFullName = repoRefFullName(refShortName, refType) ctx.Repo.RefFullName = repoRefFullName(refType, refShortName)
isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool) isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool)
if isRenamedBranch && has { if isRenamedBranch && has {
renamedBranchName := ctx.Data["RenamedBranchName"].(string) renamedBranchName := ctx.Data["RenamedBranchName"].(string)
@ -877,7 +852,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
return return
} }
if refType == RepoRefBranch && ctx.Repo.GitRepo.IsBranchExist(refShortName) { if refType == git.RefTypeBranch && ctx.Repo.GitRepo.IsBranchExist(refShortName) {
ctx.Repo.IsViewBranch = true ctx.Repo.IsViewBranch = true
ctx.Repo.BranchName = refShortName ctx.Repo.BranchName = refShortName
ctx.Repo.RefFullName = git.RefNameFromBranch(refShortName) ctx.Repo.RefFullName = git.RefNameFromBranch(refShortName)
@ -888,7 +863,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
return return
} }
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
} else if refType == RepoRefTag && ctx.Repo.GitRepo.IsTagExist(refShortName) { } else if refType == git.RefTypeTag && ctx.Repo.GitRepo.IsTagExist(refShortName) {
ctx.Repo.IsViewTag = true ctx.Repo.IsViewTag = true
ctx.Repo.RefFullName = git.RefNameFromTag(refShortName) ctx.Repo.RefFullName = git.RefNameFromTag(refShortName)
ctx.Repo.TagName = refShortName ctx.Repo.TagName = refShortName
@ -919,9 +894,6 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
ctx.RespHeader().Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, canonicalURL)) ctx.RespHeader().Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, canonicalURL))
} }
} else { } else {
if opt.IgnoreNotExistErr {
return
}
ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refShortName)) ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refShortName))
return return
} }

@ -110,41 +110,51 @@ type RepoSettingForm struct {
EnablePrune bool EnablePrune bool
// Advanced settings // Advanced settings
EnableCode bool EnableCode bool
EnableWiki bool DefaultCodeEveryoneAccess string
EnableExternalWiki bool
DefaultWikiBranch string EnableWiki bool
DefaultWikiEveryoneAccess string EnableExternalWiki bool
ExternalWikiURL string DefaultWikiBranch string
DefaultWikiEveryoneAccess string
ExternalWikiURL string
EnableIssues bool EnableIssues bool
DefaultIssuesEveryoneAccess string
EnableExternalTracker bool EnableExternalTracker bool
ExternalTrackerURL string ExternalTrackerURL string
TrackerURLFormat string TrackerURLFormat string
TrackerIssueStyle string TrackerIssueStyle string
ExternalTrackerRegexpPattern string ExternalTrackerRegexpPattern string
EnableCloseIssuesViaCommitInAnyBranch bool EnableCloseIssuesViaCommitInAnyBranch bool
EnableProjects bool
ProjectsMode string EnableProjects bool
EnableReleases bool ProjectsMode string
EnablePackages bool
EnablePulls bool EnableReleases bool
EnableActions bool
PullsIgnoreWhitespace bool EnablePackages bool
PullsAllowMerge bool
PullsAllowRebase bool EnablePulls bool
PullsAllowRebaseMerge bool PullsIgnoreWhitespace bool
PullsAllowSquash bool PullsAllowMerge bool
PullsAllowFastForwardOnly bool PullsAllowRebase bool
PullsAllowManualMerge bool PullsAllowRebaseMerge bool
PullsDefaultMergeStyle string PullsAllowSquash bool
EnableAutodetectManualMerge bool PullsAllowFastForwardOnly bool
PullsAllowRebaseUpdate bool PullsAllowManualMerge bool
DefaultDeleteBranchAfterMerge bool PullsDefaultMergeStyle string
DefaultAllowMaintainerEdit bool EnableAutodetectManualMerge bool
EnableTimetracker bool PullsAllowRebaseUpdate bool
AllowOnlyContributorsToTrackTime bool DefaultDeleteBranchAfterMerge bool
EnableIssueDependencies bool DefaultAllowMaintainerEdit bool
IsArchived bool EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool
EnableIssueDependencies bool
EnableActions bool
IsArchived bool
// Signing Settings // Signing Settings
TrustModel string TrustModel string
@ -651,8 +661,8 @@ type NewReleaseForm struct {
Target string `form:"tag_target" binding:"Required;MaxSize(255)"` Target string `form:"tag_target" binding:"Required;MaxSize(255)"`
Title string `binding:"MaxSize(255)"` Title string `binding:"MaxSize(255)"`
Content string Content string
Draft string Draft bool
TagOnly string TagOnly bool
Prerelease bool Prerelease bool
AddTagMsg bool AddTagMsg bool
Files []string Files []string

@ -10,7 +10,10 @@
{{end}} {{end}}
</div> </div>
{{if .CanWriteCode}} {{if .CanWriteCode}}
<button class="ui compact primary button tw-m-0 link-action" data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}"> <button class="ui compact primary button tw-m-0 link-action"
data-modal-confirm-header="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}"
data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" .BranchName}}"
data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}">
{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}} {{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}
</button> </button>
{{end}} {{end}}

@ -24,7 +24,7 @@
{{if $data.OpenMilestones}} {{if $data.OpenMilestones}}
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div> <div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div>
{{range $data.OpenMilestones}} {{range $data.OpenMilestones}}
<a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/issues?milestone={{.ID}}"> <a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/milestone/{{.ID}}">
{{svg "octicon-milestone" 18}} {{.Name}} {{svg "octicon-milestone" 18}} {{.Name}}
</a> </a>
{{end}} {{end}}
@ -33,7 +33,7 @@
{{if $data.ClosedMilestones}} {{if $data.ClosedMilestones}}
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div> <div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div>
{{range $data.ClosedMilestones}} {{range $data.ClosedMilestones}}
<a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/issues?milestone={{.ID}}"> <a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/milestone/{{.ID}}">
{{svg "octicon-milestone" 18}} {{.Name}} {{svg "octicon-milestone" 18}} {{.Name}}
</a> </a>
{{end}} {{end}}
@ -43,10 +43,10 @@
</div> </div>
</div> </div>
<div class="ui list"> <div class="ui list muted-links flex-items-block">
<span class="item empty-list {{if $issueMilestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span> <span class="item empty-list {{if $issueMilestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
{{if $issueMilestone}} {{if $issueMilestone}}
<a class="item muted" href="{{$pageMeta.RepoLink}}/milestone/{{$issueMilestone.ID}}"> <a class="item" href="{{$pageMeta.RepoLink}}/milestone/{{$issueMilestone.ID}}">
{{svg "octicon-milestone" 18}} {{$issueMilestone.Name}} {{svg "octicon-milestone" 18}} {{$issueMilestone.Name}}
</a> </a>
{{end}} {{end}}

@ -39,10 +39,10 @@
</div> </div>
</div> </div>
</div> </div>
<div class="ui list"> <div class="ui list muted-links flex-items-block">
<span class="item empty-list {{if $issueProject}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span> <span class="item empty-list {{if $issueProject}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
{{if $issueProject}} {{if $issueProject}}
<a class="item muted" href="{{$issueProject.Link ctx}}"> <a class="item" href="{{$issueProject.Link ctx}}">
{{svg $issueProject.IconName 18}} {{$issueProject.Title}} {{svg $issueProject.IconName 18}} {{$issueProject.Title}}
</a> </a>
{{end}} {{end}}

@ -51,9 +51,9 @@
<div class="item"> <div class="item">
<div class="flex-text-inline tw-flex-1"> <div class="flex-text-inline tw-flex-1">
{{if .User}} {{if .User}}
<a class="muted flex-text-inline" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}} {{.User.GetDisplayName}}</a> <a class="muted flex-text-inline tw-gap-2" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}} {{.User.GetDisplayName}}</a>
{{else if .Team}} {{else if .Team}}
{{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}} <span class="flex-text-inline tw-gap-2">{{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}}</span>
{{end}} {{end}}
</div> </div>
<div class="flex-text-inline"> <div class="flex-text-inline">
@ -92,7 +92,7 @@
<div class="flex-text-inline tw-flex-1"> <div class="flex-text-inline tw-flex-1">
{{$originalURLHostname := $pageMeta.Repository.GetOriginalURLHostname}} {{$originalURLHostname := $pageMeta.Repository.GetOriginalURLHostname}}
{{$originalURL := $pageMeta.Repository.OriginalURL}} {{$originalURL := $pageMeta.Repository.OriginalURL}}
<a class="muted flex-text-inline" href="{{$originalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $originalURLHostname}}"> <a class="muted flex-text-inline tw-gap-2" href="{{$originalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $originalURLHostname}}">
{{svg (MigrationIcon $originalURLHostname) 20}} {{.OriginalAuthor}} {{svg (MigrationIcon $originalURLHostname) 20}} {{.OriginalAuthor}}
</a> </a>
</div> </div>

@ -3,13 +3,15 @@
<div class="ui container medium-width"> <div class="ui container medium-width">
<h3 class="ui top attached header"> <h3 class="ui top attached header">
{{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3> </h3>
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui form left-right-form" action="{{.Link}}" method="post"> <form class="ui form left-right-form" action="{{.Link}}" method="post">
{{template "base/disable_form_autofill"}} {{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> <div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
<label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required>

@ -3,13 +3,15 @@
<div class="ui container medium-width"> <div class="ui container medium-width">
<h3 class="ui top attached header"> <h3 class="ui top attached header">
{{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3> </h3>
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui form left-right-form" action="{{.Link}}" method="post"> <form class="ui form left-right-form" action="{{.Link}}" method="post">
{{template "base/disable_form_autofill"}} {{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> <div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
<label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required>

@ -3,13 +3,15 @@
<div class="ui container medium-width"> <div class="ui container medium-width">
<h3 class="ui top attached header"> <h3 class="ui top attached header">
{{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3> </h3>
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui form left-right-form" action="{{.Link}}" method="post"> <form class="ui form left-right-form" action="{{.Link}}" method="post">
{{template "base/disable_form_autofill"}} {{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> <div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
<label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required>

@ -3,13 +3,15 @@
<div class="ui container medium-width"> <div class="ui container medium-width">
<h3 class="ui top attached header"> <h3 class="ui top attached header">
{{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3> </h3>
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui form left-right-form" action="{{.Link}}" method="post"> <form class="ui form left-right-form" action="{{.Link}}" method="post">
{{template "base/disable_form_autofill"}} {{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> <div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
<label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required>

@ -3,12 +3,14 @@
<div class="ui container medium-width"> <div class="ui container medium-width">
<h3 class="ui top attached header"> <h3 class="ui top attached header">
{{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3> </h3>
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui form left-right-form" action="{{.Link}}" method="post"> <form class="ui form left-right-form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> <div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
<label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required>

@ -3,12 +3,14 @@
<div class="ui container medium-width"> <div class="ui container medium-width">
<h3 class="ui top attached header"> <h3 class="ui top attached header">
{{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3> </h3>
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui form left-right-form" action="{{.Link}}" method="post"> <form class="ui form left-right-form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> <div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
<label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required>

@ -3,12 +3,14 @@
<div class="ui container medium-width"> <div class="ui container medium-width">
<h3 class="ui top attached header"> <h3 class="ui top attached header">
{{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3> </h3>
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui form left-right-form" action="{{.Link}}" method="post"> <form class="ui form left-right-form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> <div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
<label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required>

@ -3,12 +3,14 @@
<div class="ui container medium-width"> <div class="ui container medium-width">
<h3 class="ui top attached header"> <h3 class="ui top attached header">
{{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3> </h3>
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui form left-right-form" action="{{.Link}}" method="post"> <form class="ui form left-right-form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> <div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
<label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required>

@ -3,13 +3,15 @@
<div class="ui container medium-width"> <div class="ui container medium-width">
<h3 class="ui top attached header"> <h3 class="ui top attached header">
{{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3> </h3>
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui form left-right-form" action="{{.Link}}" method="post"> <form class="ui form left-right-form" action="{{.Link}}" method="post">
{{template "base/disable_form_autofill"}} {{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> <div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
<label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label> <label for="clone_addr">{{ctx.Locale.Tr "repo.migrate.clone_address"}}</label>
<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required>

@ -109,23 +109,17 @@
{{ctx.Locale.Tr "repo.release.delete_release"}} {{ctx.Locale.Tr "repo.release.delete_release"}}
</a> </a>
{{if .IsDraft}} {{if .IsDraft}}
<button class="ui small button" type="submit" name="draft" value="{{ctx.Locale.Tr "repo.release.save_draft"}}">{{ctx.Locale.Tr "repo.release.save_draft"}}</button> <button class="ui small button" type="submit" name="draft" value="1">{{ctx.Locale.Tr "repo.release.save_draft"}}</button>
<button class="ui small primary button"> <button class="ui small primary button">{{ctx.Locale.Tr "repo.release.publish"}}</button>
{{ctx.Locale.Tr "repo.release.publish"}}
</button>
{{else}} {{else}}
<button class="ui small primary button"> <button class="ui small primary button">{{ctx.Locale.Tr "repo.release.edit_release"}}</button>
{{ctx.Locale.Tr "repo.release.edit_release"}}
</button>
{{end}} {{end}}
{{else}} {{else}}
{{if not .tag_name}} {{if .ShowCreateTagOnlyButton}}
<button class="ui small button" name="tag_only" value="1">{{ctx.Locale.Tr "repo.release.add_tag"}}</button> <button class="ui small button" name="tag_only" value="1">{{ctx.Locale.Tr "repo.release.add_tag"}}</button>
{{end}} {{end}}
<button class="ui small button" name="draft" value="1">{{ctx.Locale.Tr "repo.release.save_draft"}}</button> <button class="ui small button" name="draft" value="1">{{ctx.Locale.Tr "repo.release.save_draft"}}</button>
<button class="ui small primary button"> <button class="ui small primary button">{{ctx.Locale.Tr "repo.release.publish"}}</button>
{{ctx.Locale.Tr "repo.release.publish"}}
</button>
{{end}} {{end}}
</div> </div>
</div> </div>

@ -302,6 +302,15 @@
<input class="enable-system" name="enable_code" type="checkbox"{{if $isCodeEnabled}} checked{{end}}> <input class="enable-system" name="enable_code" type="checkbox"{{if $isCodeEnabled}} checked{{end}}>
<label>{{ctx.Locale.Tr "repo.code.desc"}}</label> <label>{{ctx.Locale.Tr "repo.code.desc"}}</label>
</div> </div>
<div class="inline field tw-pl-4">
{{$unitCode := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeCode}}
<label>{{ctx.Locale.Tr "repo.settings.default_permission_everyone_access"}}</label>
<select name="default_code_everyone_access" class="ui selection dropdown">
{{/* everyone access mode is different from others, none means it is unset and won't be applied */}}
<option value="none" {{Iif (eq $unitCode.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option>
<option value="read" {{Iif (eq $unitCode.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option>
</select>
</div>
</div> </div>
{{$isInternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}} {{$isInternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}}
@ -331,7 +340,7 @@
</div> </div>
<div class="inline field"> <div class="inline field">
{{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}} {{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}}
<label>{{ctx.Locale.Tr "repo.settings.default_wiki_everyone_access"}}</label> <label>{{ctx.Locale.Tr "repo.settings.default_permission_everyone_access"}}</label>
<select name="default_wiki_everyone_access" class="ui selection dropdown"> <select name="default_wiki_everyone_access" class="ui selection dropdown">
{{/* everyone access mode is different from others, none means it is unset and won't be applied */}} {{/* everyone access mode is different from others, none means it is unset and won't be applied */}}
<option value="none" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option> <option value="none" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option>
@ -374,6 +383,15 @@
</div> </div>
</div> </div>
<div class="field tw-pl-4 {{if (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box"> <div class="field tw-pl-4 {{if (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box">
<div class="inline field">
{{$unitIssue := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues}}
<label>{{ctx.Locale.Tr "repo.settings.default_permission_everyone_access"}}</label>
<select name="default_issues_everyone_access" class="ui selection dropdown">
{{/* everyone access mode is different from others, none means it is unset and won't be applied */}}
<option value="none" {{Iif (eq $unitIssue.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option>
<option value="read" {{Iif (eq $unitIssue.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option>
</select>
</div>
{{if .Repository.CanEnableTimetracker}} {{if .Repository.CanEnableTimetracker}}
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">

@ -14,6 +14,7 @@ import (
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -24,6 +25,7 @@ import (
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder { func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder {
@ -82,6 +84,29 @@ func TestEmptyRepoAddFile(t *testing.T) {
req = NewRequest(t, "GET", redirect) req = NewRequest(t, "GET", redirect)
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "newly-added-test-file") assert.Contains(t, resp.Body.String(), "newly-added-test-file")
// the repo is not empty anymore
req = NewRequest(t, "GET", "/user30/empty")
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "test-file.md")
// if the repo is in incorrect state, it should be able to self-heal (recover to correct state)
user30EmptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"})
user30EmptyRepo.IsEmpty = true
user30EmptyRepo.DefaultBranch = "no-such"
_, err := db.GetEngine(db.DefaultContext).ID(user30EmptyRepo.ID).Cols("is_empty", "default_branch").Update(user30EmptyRepo)
require.NoError(t, err)
user30EmptyRepo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"})
assert.True(t, user30EmptyRepo.IsEmpty)
req = NewRequest(t, "GET", "/user30/empty")
resp = session.MakeRequest(t, req, http.StatusSeeOther)
redirect = test.RedirectURL(resp)
assert.Equal(t, "/user30/empty", redirect)
req = NewRequest(t, "GET", "/user30/empty")
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "test-file.md")
} }
func TestEmptyRepoUploadFile(t *testing.T) { func TestEmptyRepoUploadFile(t *testing.T) {

@ -79,8 +79,12 @@ func TestMigrateGiteaForm(t *testing.T) {
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
// Step 2: load the form // Step 2: load the form
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find(`form.ui.form[action^="/repo/migrate"]`).Attr("action") form := htmlDoc.doc.Find(`form.ui.form[action^="/repo/migrate"]`)
link, exists := form.Attr("action")
assert.True(t, exists, "The template has changed") assert.True(t, exists, "The template has changed")
serviceInput, exists := form.Find(`input[name="service"]`).Attr("value")
assert.True(t, exists)
assert.EqualValues(t, fmt.Sprintf("%d", structs.GiteaService), serviceInput)
// Step 4: submit the migration to only migrate issues // Step 4: submit the migration to only migrate issues
migratedRepoName := "otherrepo" migratedRepoName := "otherrepo"
req = NewRequestWithValues(t, "POST", link, map[string]string{ req = NewRequestWithValues(t, "POST", link, map[string]string{

@ -39,7 +39,7 @@ func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title st
postData["prerelease"] = "on" postData["prerelease"] = "on"
} }
if draft { if draft {
postData["draft"] = "Save Draft" postData["draft"] = "1"
} }
req = NewRequestWithValues(t, "POST", link, postData) req = NewRequestWithValues(t, "POST", link, postData)

Loading…
Cancel
Save