Merge pull request '[gitea] week 2024-42 cherry pick (gitea/main -> forgejo)' (#5543) from earl-warren/wcp/2024-42 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5543
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-10-19 13:54:24 +00:00
commit e2354703ed
10 changed files with 105 additions and 69 deletions

View file

@ -26,6 +26,7 @@ type PullRequestsOptions struct {
SortType string SortType string
Labels []int64 Labels []int64
MilestoneID int64 MilestoneID int64
PosterID int64
} }
func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session { func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session {
@ -46,6 +47,10 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
sess.And("issue.milestone_id=?", opts.MilestoneID) sess.And("issue.milestone_id=?", opts.MilestoneID)
} }
if opts.PosterID > 0 {
sess.And("issue.poster_id=?", opts.PosterID)
}
return sess return sess
} }

View file

@ -586,44 +586,46 @@ var (
".", ".",
"..", "..",
".well-known", ".well-known",
"admin",
"api", "api", // gitea api
"assets", "metrics", // prometheus metrics api
"attachments", "v2", // container registry api
"avatar",
"avatars", "assets", // static asset files
"captcha", "attachments", // issue attachments
"commits",
"debug", "avatar", // avatar by email hash
"devtest", "avatars", // user avatars by file name
"error",
"explore",
"favicon.ico",
"ghost",
"issues",
"login",
"manifest.json",
"metrics",
"milestones",
"new",
"notifications",
"org",
"pulls",
"raw",
"repo",
"repo-avatars", "repo-avatars",
"robots.txt",
"search", "captcha",
"serviceworker.js", "login", // oauth2 login
"ssh_info", "org", // org create/manage, or "/org/{org}", BUT if an org is named as "invite" then it goes wrong
"repo", // repo create/migrate, etc
"user", // user login/activate/settings, etc
"admin",
"devtest",
"explore",
"issues",
"pulls",
"milestones",
"notifications",
"favicon.ico",
"manifest.json", // web app manifests
"robots.txt", // search engine robots
"sitemap.xml", // search engine sitemap
"ssh_info", // agit info
"swagger.v1.json", "swagger.v1.json",
"user",
"v2", "ghost", // reserved name for deleted users (id: -1)
"gitea-actions", "gitea-actions", // gitea builtin user (id: -2)
"forgejo-actions", "forgejo-actions", // forgejo builtin user (id: -2)
} }
// DON'T ADD ANY NEW STUFF, WE SOLVE THIS WITH `/user/{obj}` PATHS! // These names are reserved for user accounts: user's keys, user's rss feed, user's avatar, etc.
// DO NOT add any new stuff! The paths with these names are processed by `/{username}` handler (UsernameSubRoute) manually.
reservedUserPatterns = []string{"*.keys", "*.gpg", "*.rss", "*.atom", "*.png"} reservedUserPatterns = []string{"*.keys", "*.gpg", "*.rss", "*.atom", "*.png"}
) )

1
release-notes/5543.md Normal file
View file

@ -0,0 +1 @@
feat: [commit](https://codeberg.org/forgejo/forgejo/commit/d0af8fe4dc7b294fe5409b2271468494267d5a7d) Allow filtering pull requests by poster in the API.

View file

@ -52,56 +52,79 @@ func ListPullRequests(ctx *context.APIContext) {
// parameters: // parameters:
// - name: owner // - name: owner
// in: path // in: path
// description: owner of the repo // description: Owner of the repo
// type: string // type: string
// required: true // required: true
// - name: repo // - name: repo
// in: path // in: path
// description: name of the repo // description: Name of the repo
// type: string // type: string
// required: true // required: true
// - name: state // - name: state
// in: query // in: query
// description: "State of pull request: open or closed (optional)" // description: State of pull request
// type: string // type: string
// enum: [closed, open, all] // enum: [open, closed, all]
// default: open
// - name: sort // - name: sort
// in: query // in: query
// description: "Type of sort" // description: Type of sort
// type: string // type: string
// enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority] // enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority]
// - name: milestone // - name: milestone
// in: query // in: query
// description: "ID of the milestone" // description: ID of the milestone
// type: integer // type: integer
// format: int64 // format: int64
// - name: labels // - name: labels
// in: query // in: query
// description: "Label IDs" // description: Label IDs
// type: array // type: array
// collectionFormat: multi // collectionFormat: multi
// items: // items:
// type: integer // type: integer
// format: int64 // format: int64
// - name: poster
// in: query
// description: Filter by pull request author
// type: string
// - name: page // - name: page
// in: query // in: query
// description: page number of results to return (1-based) // description: Page number of results to return (1-based)
// type: integer // type: integer
// minimum: 1
// default: 1
// - name: limit // - name: limit
// in: query // in: query
// description: page size of results // description: Page size of results
// type: integer // type: integer
// minimum: 0
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/PullRequestList" // "$ref": "#/responses/PullRequestList"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// "500":
// "$ref": "#/responses/error"
labelIDs, err := base.StringsToInt64s(ctx.FormStrings("labels")) labelIDs, err := base.StringsToInt64s(ctx.FormStrings("labels"))
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "PullRequests", err) ctx.Error(http.StatusInternalServerError, "PullRequests", err)
return return
} }
var posterID int64
if posterStr := ctx.FormString("poster"); posterStr != "" {
poster, err := user_model.GetUserByName(ctx, posterStr)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusBadRequest, "Poster not found", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
}
return
}
posterID = poster.ID
}
listOptions := utils.GetListOptions(ctx) listOptions := utils.GetListOptions(ctx)
prs, maxResults, err := issues_model.PullRequests(ctx, ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{ prs, maxResults, err := issues_model.PullRequests(ctx, ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{
ListOptions: listOptions, ListOptions: listOptions,
@ -109,6 +132,7 @@ func ListPullRequests(ctx *context.APIContext) {
SortType: ctx.FormTrim("sort"), SortType: ctx.FormTrim("sort"),
Labels: labelIDs, Labels: labelIDs,
MilestoneID: ctx.FormInt64("milestone"), MilestoneID: ctx.FormInt64("milestone"),
PosterID: posterID,
}) })
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "PullRequests", err) ctx.Error(http.StatusInternalServerError, "PullRequests", err)

View file

@ -33,6 +33,10 @@ import (
// RenameUser renames a user // RenameUser renames a user
func RenameUser(ctx context.Context, u *user_model.User, newUserName string) error { func RenameUser(ctx context.Context, u *user_model.User, newUserName string) error {
if newUserName == u.Name {
return nil
}
// Non-local users are not allowed to change their username. // Non-local users are not allowed to change their username.
if !u.IsOrganization() && !u.IsLocal() { if !u.IsOrganization() && !u.IsLocal() {
return user_model.ErrUserIsNotLocal{ return user_model.ErrUserIsNotLocal{
@ -41,10 +45,6 @@ func RenameUser(ctx context.Context, u *user_model.User, newUserName string) err
} }
} }
if newUserName == u.Name {
return nil
}
if err := user_model.IsUsableUsername(newUserName); err != nil { if err := user_model.IsUsableUsername(newUserName); err != nil {
return err return err
} }

View file

@ -114,12 +114,10 @@ func TestRenameUser(t *testing.T) {
}) })
t.Run("Non usable username", func(t *testing.T) { t.Run("Non usable username", func(t *testing.T) {
usernames := []string{"--diff", "aa.png", ".well-known", "search", "aaa.atom"} usernames := []string{"--diff", ".well-known", "gitea-actions", "aaa.atom", "aa.png"}
for _, username := range usernames { for _, username := range usernames {
t.Run(username, func(t *testing.T) { require.Error(t, user_model.IsUsableUsername(username), "non-usable username: %s", username)
require.Error(t, user_model.IsUsableUsername(username)) require.Error(t, RenameUser(db.DefaultContext, user, username), "non-usable username: %s", username)
require.Error(t, RenameUser(db.DefaultContext, user, username))
})
} }
}) })

View file

@ -12295,26 +12295,27 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "owner of the repo", "description": "Owner of the repo",
"name": "owner", "name": "owner",
"in": "path", "in": "path",
"required": true "required": true
}, },
{ {
"type": "string", "type": "string",
"description": "name of the repo", "description": "Name of the repo",
"name": "repo", "name": "repo",
"in": "path", "in": "path",
"required": true "required": true
}, },
{ {
"enum": [ "enum": [
"closed",
"open", "open",
"closed",
"all" "all"
], ],
"type": "string", "type": "string",
"description": "State of pull request: open or closed (optional)", "default": "open",
"description": "State of pull request",
"name": "state", "name": "state",
"in": "query" "in": "query"
}, },
@ -12351,14 +12352,23 @@
"in": "query" "in": "query"
}, },
{ {
"type": "string",
"description": "Filter by pull request author",
"name": "poster",
"in": "query"
},
{
"minimum": 1,
"type": "integer", "type": "integer",
"description": "page number of results to return (1-based)", "default": 1,
"description": "Page number of results to return (1-based)",
"name": "page", "name": "page",
"in": "query" "in": "query"
}, },
{ {
"minimum": 0,
"type": "integer", "type": "integer",
"description": "page size of results", "description": "Page size of results",
"name": "limit", "name": "limit",
"in": "query" "in": "query"
} }
@ -12369,6 +12379,9 @@
}, },
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
},
"500": {
"$ref": "#/responses/error"
} }
} }
}, },

View file

@ -114,10 +114,7 @@ func TestRenameReservedUsername(t *testing.T) {
"avatar", "avatar",
"avatars", "avatars",
"captcha", "captcha",
"commits",
"debug",
"devtest", "devtest",
"error",
"explore", "explore",
"favicon.ico", "favicon.ico",
"ghost", "ghost",
@ -126,16 +123,12 @@ func TestRenameReservedUsername(t *testing.T) {
"manifest.json", "manifest.json",
"metrics", "metrics",
"milestones", "milestones",
"new",
"notifications", "notifications",
"org", "org",
"pulls", "pulls",
"raw",
"repo", "repo",
"repo-avatars", "repo-avatars",
"robots.txt", "robots.txt",
"search",
"serviceworker.js",
"ssh_info", "ssh_info",
"swagger.v1.json", "swagger.v1.json",
"user", "user",

View file

@ -358,9 +358,9 @@ export default sfc; // activate the IDE's Vue plugin
<div class="menu"> <div class="menu">
<a class="item" @click="toggleArchivedFilter()"> <a class="item" @click="toggleArchivedFilter()">
<div class="ui checkbox" ref="checkboxArchivedFilter" :title="checkboxArchivedFilterTitle"> <div class="ui checkbox" ref="checkboxArchivedFilter" :title="checkboxArchivedFilterTitle">
<!--the "hidden" is necessary to make the checkbox work without Fomantic UI js, <!--the "tw-pointer-events-none" is necessary to prevent the checkbox from handling user's input,
otherwise if the "input" handles click event for intermediate status, it breaks the internal state--> otherwise if the "input" handles click event for intermediate status, it breaks the internal state-->
<input type="checkbox" class="hidden" v-bind.prop="checkboxArchivedFilterProps"> <input type="checkbox" class="tw-pointer-events-none" v-bind.prop="checkboxArchivedFilterProps">
<label> <label>
<svg-icon name="octicon-archive" :size="16" class-name="tw-mr-1"/> <svg-icon name="octicon-archive" :size="16" class-name="tw-mr-1"/>
{{ textShowArchived }} {{ textShowArchived }}
@ -369,7 +369,7 @@ export default sfc; // activate the IDE's Vue plugin
</a> </a>
<a class="item" @click="togglePrivateFilter()"> <a class="item" @click="togglePrivateFilter()">
<div class="ui checkbox" ref="checkboxPrivateFilter" :title="checkboxPrivateFilterTitle"> <div class="ui checkbox" ref="checkboxPrivateFilter" :title="checkboxPrivateFilterTitle">
<input type="checkbox" class="hidden" v-bind.prop="checkboxPrivateFilterProps"> <input type="checkbox" class="tw-pointer-events-none" v-bind.prop="checkboxPrivateFilterProps">
<label> <label>
<svg-icon name="octicon-lock" :size="16" class-name="tw-mr-1"/> <svg-icon name="octicon-lock" :size="16" class-name="tw-mr-1"/>
{{ textShowPrivate }} {{ textShowPrivate }}

View file

@ -60,7 +60,7 @@ export function initRepoTopicBar() {
// how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save" // how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save"
const responseData = await response.json(); const responseData = await response.json();
lastErrorToast = showErrorToast(responseData.message, {duration: 5000}); lastErrorToast = showErrorToast(responseData.message, {duration: 5000});
if (responseData.invalidTopics.length > 0) { if (responseData.invalidTopics && responseData.invalidTopics.length > 0) {
const {invalidTopics} = responseData; const {invalidTopics} = responseData;
const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label'); const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label');
for (const [index, value] of topics.split(',').entries()) { for (const [index, value] of topics.split(',').entries()) {