+5
models/issues/pull_list.go
+5
models/issues/pull_list.go
···
26
26
SortType string
27
27
Labels []int64
28
28
MilestoneID int64
29
+
PosterID int64
29
30
}
30
31
31
32
func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session {
···
44
45
45
46
if opts.MilestoneID > 0 {
46
47
sess.And("issue.milestone_id=?", opts.MilestoneID)
48
+
}
49
+
50
+
if opts.PosterID > 0 {
51
+
sess.And("issue.poster_id=?", opts.PosterID)
47
52
}
48
53
49
54
return sess
+31
-29
models/user/user.go
+31
-29
models/user/user.go
···
586
586
".",
587
587
"..",
588
588
".well-known",
589
-
"admin",
590
-
"api",
591
-
"assets",
592
-
"attachments",
593
-
"avatar",
594
-
"avatars",
589
+
590
+
"api", // gitea api
591
+
"metrics", // prometheus metrics api
592
+
"v2", // container registry api
593
+
594
+
"assets", // static asset files
595
+
"attachments", // issue attachments
596
+
597
+
"avatar", // avatar by email hash
598
+
"avatars", // user avatars by file name
599
+
"repo-avatars",
600
+
595
601
"captcha",
596
-
"commits",
597
-
"debug",
602
+
"login", // oauth2 login
603
+
"org", // org create/manage, or "/org/{org}", BUT if an org is named as "invite" then it goes wrong
604
+
"repo", // repo create/migrate, etc
605
+
"user", // user login/activate/settings, etc
606
+
607
+
"admin",
598
608
"devtest",
599
-
"error",
600
609
"explore",
601
-
"favicon.ico",
602
-
"ghost",
603
610
"issues",
604
-
"login",
605
-
"manifest.json",
606
-
"metrics",
611
+
"pulls",
607
612
"milestones",
608
-
"new",
609
613
"notifications",
610
-
"org",
611
-
"pulls",
612
-
"raw",
613
-
"repo",
614
-
"repo-avatars",
615
-
"robots.txt",
616
-
"search",
617
-
"serviceworker.js",
618
-
"ssh_info",
614
+
615
+
"favicon.ico",
616
+
"manifest.json", // web app manifests
617
+
"robots.txt", // search engine robots
618
+
"sitemap.xml", // search engine sitemap
619
+
"ssh_info", // agit info
619
620
"swagger.v1.json",
620
-
"user",
621
-
"v2",
622
-
"gitea-actions",
623
-
"forgejo-actions",
621
+
622
+
"ghost", // reserved name for deleted users (id: -1)
623
+
"gitea-actions", // gitea builtin user (id: -2)
624
+
"forgejo-actions", // forgejo builtin user (id: -2)
624
625
}
625
626
626
-
// DON'T ADD ANY NEW STUFF, WE SOLVE THIS WITH `/user/{obj}` PATHS!
627
+
// These names are reserved for user accounts: user's keys, user's rss feed, user's avatar, etc.
628
+
// DO NOT add any new stuff! The paths with these names are processed by `/{username}` handler (UsernameSubRoute) manually.
627
629
reservedUserPatterns = []string{"*.keys", "*.gpg", "*.rss", "*.atom", "*.png"}
628
630
)
629
631
+1
release-notes/5543.md
+1
release-notes/5543.md
···
1
+
feat: [commit](https://codeberg.org/forgejo/forgejo/commit/d0af8fe4dc7b294fe5409b2271468494267d5a7d) Allow filtering pull requests by poster in the API.
+33
-9
routers/api/v1/repo/pull.go
+33
-9
routers/api/v1/repo/pull.go
···
52
52
// parameters:
53
53
// - name: owner
54
54
// in: path
55
-
// description: owner of the repo
55
+
// description: Owner of the repo
56
56
// type: string
57
57
// required: true
58
58
// - name: repo
59
59
// in: path
60
-
// description: name of the repo
60
+
// description: Name of the repo
61
61
// type: string
62
62
// required: true
63
63
// - name: state
64
64
// in: query
65
-
// description: "State of pull request: open or closed (optional)"
65
+
// description: State of pull request
66
66
// type: string
67
-
// enum: [closed, open, all]
67
+
// enum: [open, closed, all]
68
+
// default: open
68
69
// - name: sort
69
70
// in: query
70
-
// description: "Type of sort"
71
+
// description: Type of sort
71
72
// type: string
72
73
// enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority]
73
74
// - name: milestone
74
75
// in: query
75
-
// description: "ID of the milestone"
76
+
// description: ID of the milestone
76
77
// type: integer
77
78
// format: int64
78
79
// - name: labels
79
80
// in: query
80
-
// description: "Label IDs"
81
+
// description: Label IDs
81
82
// type: array
82
83
// collectionFormat: multi
83
84
// items:
84
85
// type: integer
85
86
// format: int64
87
+
// - name: poster
88
+
// in: query
89
+
// description: Filter by pull request author
90
+
// type: string
86
91
// - name: page
87
92
// in: query
88
-
// description: page number of results to return (1-based)
93
+
// description: Page number of results to return (1-based)
89
94
// type: integer
95
+
// minimum: 1
96
+
// default: 1
90
97
// - name: limit
91
98
// in: query
92
-
// description: page size of results
99
+
// description: Page size of results
93
100
// type: integer
101
+
// minimum: 0
94
102
// responses:
95
103
// "200":
96
104
// "$ref": "#/responses/PullRequestList"
97
105
// "404":
98
106
// "$ref": "#/responses/notFound"
107
+
// "500":
108
+
// "$ref": "#/responses/error"
99
109
100
110
labelIDs, err := base.StringsToInt64s(ctx.FormStrings("labels"))
101
111
if err != nil {
102
112
ctx.Error(http.StatusInternalServerError, "PullRequests", err)
103
113
return
104
114
}
115
+
var posterID int64
116
+
if posterStr := ctx.FormString("poster"); posterStr != "" {
117
+
poster, err := user_model.GetUserByName(ctx, posterStr)
118
+
if err != nil {
119
+
if user_model.IsErrUserNotExist(err) {
120
+
ctx.Error(http.StatusBadRequest, "Poster not found", err)
121
+
} else {
122
+
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
123
+
}
124
+
return
125
+
}
126
+
posterID = poster.ID
127
+
}
105
128
listOptions := utils.GetListOptions(ctx)
106
129
prs, maxResults, err := issues_model.PullRequests(ctx, ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{
107
130
ListOptions: listOptions,
···
109
132
SortType: ctx.FormTrim("sort"),
110
133
Labels: labelIDs,
111
134
MilestoneID: ctx.FormInt64("milestone"),
135
+
PosterID: posterID,
112
136
})
113
137
if err != nil {
114
138
ctx.Error(http.StatusInternalServerError, "PullRequests", err)
+4
-4
services/user/user.go
+4
-4
services/user/user.go
···
33
33
34
34
// RenameUser renames a user
35
35
func RenameUser(ctx context.Context, u *user_model.User, newUserName string) error {
36
+
if newUserName == u.Name {
37
+
return nil
38
+
}
39
+
36
40
// Non-local users are not allowed to change their username.
37
41
if !u.IsOrganization() && !u.IsLocal() {
38
42
return user_model.ErrUserIsNotLocal{
39
43
UID: u.ID,
40
44
Name: u.Name,
41
45
}
42
-
}
43
-
44
-
if newUserName == u.Name {
45
-
return nil
46
46
}
47
47
48
48
if err := user_model.IsUsableUsername(newUserName); err != nil {
+3
-5
services/user/user_test.go
+3
-5
services/user/user_test.go
···
114
114
})
115
115
116
116
t.Run("Non usable username", func(t *testing.T) {
117
-
usernames := []string{"--diff", "aa.png", ".well-known", "search", "aaa.atom"}
117
+
usernames := []string{"--diff", ".well-known", "gitea-actions", "aaa.atom", "aa.png"}
118
118
for _, username := range usernames {
119
-
t.Run(username, func(t *testing.T) {
120
-
require.Error(t, user_model.IsUsableUsername(username))
121
-
require.Error(t, RenameUser(db.DefaultContext, user, username))
122
-
})
119
+
require.Error(t, user_model.IsUsableUsername(username), "non-usable username: %s", username)
120
+
require.Error(t, RenameUser(db.DefaultContext, user, username), "non-usable username: %s", username)
123
121
}
124
122
})
125
123
+19
-6
templates/swagger/v1_json.tmpl
+19
-6
templates/swagger/v1_json.tmpl
···
12295
12295
"parameters": [
12296
12296
{
12297
12297
"type": "string",
12298
-
"description": "owner of the repo",
12298
+
"description": "Owner of the repo",
12299
12299
"name": "owner",
12300
12300
"in": "path",
12301
12301
"required": true
12302
12302
},
12303
12303
{
12304
12304
"type": "string",
12305
-
"description": "name of the repo",
12305
+
"description": "Name of the repo",
12306
12306
"name": "repo",
12307
12307
"in": "path",
12308
12308
"required": true
12309
12309
},
12310
12310
{
12311
12311
"enum": [
12312
+
"open",
12312
12313
"closed",
12313
-
"open",
12314
12314
"all"
12315
12315
],
12316
12316
"type": "string",
12317
-
"description": "State of pull request: open or closed (optional)",
12317
+
"default": "open",
12318
+
"description": "State of pull request",
12318
12319
"name": "state",
12319
12320
"in": "query"
12320
12321
},
···
12351
12352
"in": "query"
12352
12353
},
12353
12354
{
12355
+
"type": "string",
12356
+
"description": "Filter by pull request author",
12357
+
"name": "poster",
12358
+
"in": "query"
12359
+
},
12360
+
{
12361
+
"minimum": 1,
12354
12362
"type": "integer",
12355
-
"description": "page number of results to return (1-based)",
12363
+
"default": 1,
12364
+
"description": "Page number of results to return (1-based)",
12356
12365
"name": "page",
12357
12366
"in": "query"
12358
12367
},
12359
12368
{
12369
+
"minimum": 0,
12360
12370
"type": "integer",
12361
-
"description": "page size of results",
12371
+
"description": "Page size of results",
12362
12372
"name": "limit",
12363
12373
"in": "query"
12364
12374
}
···
12369
12379
},
12370
12380
"404": {
12371
12381
"$ref": "#/responses/notFound"
12382
+
},
12383
+
"500": {
12384
+
"$ref": "#/responses/error"
12372
12385
}
12373
12386
}
12374
12387
},
-7
tests/integration/user_test.go
-7
tests/integration/user_test.go
···
114
114
"avatar",
115
115
"avatars",
116
116
"captcha",
117
-
"commits",
118
-
"debug",
119
117
"devtest",
120
-
"error",
121
118
"explore",
122
119
"favicon.ico",
123
120
"ghost",
···
126
123
"manifest.json",
127
124
"metrics",
128
125
"milestones",
129
-
"new",
130
126
"notifications",
131
127
"org",
132
128
"pulls",
133
-
"raw",
134
129
"repo",
135
130
"repo-avatars",
136
131
"robots.txt",
137
-
"search",
138
-
"serviceworker.js",
139
132
"ssh_info",
140
133
"swagger.v1.json",
141
134
"user",
+3
-3
web_src/js/components/DashboardRepoList.vue
+3
-3
web_src/js/components/DashboardRepoList.vue
···
358
358
<div class="menu">
359
359
<a class="item" @click="toggleArchivedFilter()">
360
360
<div class="ui checkbox" ref="checkboxArchivedFilter" :title="checkboxArchivedFilterTitle">
361
-
<!--the "hidden" is necessary to make the checkbox work without Fomantic UI js,
361
+
<!--the "tw-pointer-events-none" is necessary to prevent the checkbox from handling user's input,
362
362
otherwise if the "input" handles click event for intermediate status, it breaks the internal state-->
363
-
<input type="checkbox" class="hidden" v-bind.prop="checkboxArchivedFilterProps">
363
+
<input type="checkbox" class="tw-pointer-events-none" v-bind.prop="checkboxArchivedFilterProps">
364
364
<label>
365
365
<svg-icon name="octicon-archive" :size="16" class-name="tw-mr-1"/>
366
366
{{ textShowArchived }}
···
369
369
</a>
370
370
<a class="item" @click="togglePrivateFilter()">
371
371
<div class="ui checkbox" ref="checkboxPrivateFilter" :title="checkboxPrivateFilterTitle">
372
-
<input type="checkbox" class="hidden" v-bind.prop="checkboxPrivateFilterProps">
372
+
<input type="checkbox" class="tw-pointer-events-none" v-bind.prop="checkboxPrivateFilterProps">
373
373
<label>
374
374
<svg-icon name="octicon-lock" :size="16" class-name="tw-mr-1"/>
375
375
{{ textShowPrivate }}
+1
-1
web_src/js/features/repo-home.js
+1
-1
web_src/js/features/repo-home.js
···
60
60
// how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save"
61
61
const responseData = await response.json();
62
62
lastErrorToast = showErrorToast(responseData.message, {duration: 5000});
63
-
if (responseData.invalidTopics.length > 0) {
63
+
if (responseData.invalidTopics && responseData.invalidTopics.length > 0) {
64
64
const {invalidTopics} = responseData;
65
65
const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label');
66
66
for (const [index, value] of topics.split(',').entries()) {