✨ (merge-requests): Address merge requests by per-repo number in URLs
Changes
7 files changed, +96 -49
MODIFY
README.md
+1 -1
@@ -23,7 +23,7 @@
23
23
- Per-repository issues: title, optional description (rendered as Markdown, XSS-safe), per-repo sequential number (`#1`, `#2`, …), an author, and an optional assignee (any local user, set by username; blank clears it); created and managed by the repo owner and collaborators, readable by anyone who can read the repo, via a dedicated "New issue" page; title and description can be edited afterwards via an "Edit issue" page. Issue pages are addressed by number (`…/issues/1`); old UUID URLs redirect permanently to the number form
24
24
- Issues move through a fixed lifecycle (Planned → In development → Done); the repo navigation shows the open (Planned + In development) issue count, and Done issues collapse into an "Archive" section on the issues page
25
25
- Issues auto-close from pushed commit messages, GitHub-style (`close(s|d)`/`fix(es|ed)`/`resolve(s|d)` + `#<number>`, e.g. `fixes #12`), over both HTTP and SSH pushes
26
-- Per-repository merge requests: source → target branch within one repo, with a title, optional description, a per-repo sequential number displayed bang-prefixed (`!1`, `!2`, …, distinct from issues' `#`), an author, and an optional assignee and reviewer (any local user, set by username from a GitHub-style picker; blank clears); created and managed by the repo owner and collaborators, readable by anyone who can read the repo, via a dedicated "New merge request" page where the author picks source and target from the repo's branches
26
+- Per-repository merge requests: source → target branch within one repo, with a title, optional description, a per-repo sequential number displayed bang-prefixed (`!1`, `!2`, …, distinct from issues' `#`), an author, and an optional assignee and reviewer (any local user, set by username from a GitHub-style picker; blank clears); created and managed by the repo owner and collaborators, readable by anyone who can read the repo, via a dedicated "New merge request" page where the author picks source and target from the repo's branches. Merge-request pages are addressed by number (`…/merge-requests/1`, matching the API and the issue URL scheme); old UUID URLs redirect permanently to the number form
27
27
- Merge requests move through the lifecycle Open → Merged / Closed; the repo navigation and left sidebar show the open merge-request count, and merged/closed ones collapse into an "Archive" section on the list page (same pattern as issues)
28
28
- Dashboard notifications: the signed-in home page (`/`) surfaces the open issues and merge requests you are involved in — ones you authored, are assigned to, or (for merge requests) are asked to review — each linking straight to the item and labelled with its repository; items in a repository you can no longer read never appear. Built on a pluggable `NotificationSource` aggregation, so future item types can contribute without touching the dashboard
29
29
- The merge request detail page renders the live diff of the source branch relative to the merge base with the target (three-dot diff), file by file with per-line add/delete coloring and a changed-files / +additions / −deletions summary — always computed live from git, never duplicated into the database
MODIFY
src/main/java/de/workaround/notify/MergeRequestNotificationSource.java
+1 -1
@@ -32,7 +32,7 @@
32
32
return mergeRequests.findOpenInvolving(user).stream()
33
33
.filter(mr -> accessPolicy.canRead(user, mr.repository))
34
34
.map(mr -> new NotificationItem("merge-request", "!" + mr.number + " " + mr.title, mr.repository,
35
- "/repos/" + mr.repository.ownerHandle() + "/" + mr.repository.name + "/merge-requests/" + mr.id))
35
+ "/repos/" + mr.repository.ownerHandle() + "/" + mr.repository.name + "/merge-requests/" + mr.number))
36
36
.toList();
37
37
}
38
38
}
MODIFY
src/main/java/de/workaround/web/MergeRequestResource.java
+41 -30
@@ -131,16 +131,16 @@
131
131
Repository repo = requireReadable(owner, name);
132
132
MergeRequest mr = mergeRequestService.create(currentUser.require(), repo, title, description, sourceBranch,
133
133
targetBranch);
134
- return Response.seeOther(detailUri(repo, mr.id)).build();
134
+ return Response.seeOther(detailUri(repo, mr.number)).build();
135
135
}
136
136
137
137
@GET
138
- @jakarta.ws.rs.Path("{id}")
138
+ @jakarta.ws.rs.Path("{number:\\d+}")
139
139
public TemplateInstance detail(@PathParam("owner") String owner, @PathParam("name") String name,
140
- @PathParam("id") String id)
140
+ @PathParam("number") int number)
141
141
{
142
142
Repository repo = requireReadable(owner, name);
143
- MergeRequest mr = mergeRequestService.find(repo, parseId(id)).orElseThrow(NotFoundException::new);
143
+ MergeRequest mr = mergeRequestService.find(repo, number).orElseThrow(NotFoundException::new);
144
144
GitMergeService.DiffView diff = mergeRequestService.diff(mr).orElse(null);
145
145
List<MergeRequestComment> comments = commentService.list(mr);
146
146
User user = currentUser.get();
@@ -195,35 +195,46 @@
195
195
return users;
196
196
}
197
197
198
+ /** Merge requests were originally addressed by UUID; keep old bookmarks and federated links working. */
199
+ @GET
200
+ @jakarta.ws.rs.Path("{id:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}}")
201
+ public Response legacyDetail(@PathParam("owner") String owner, @PathParam("name") String name,
202
+ @PathParam("id") UUID id)
203
+ {
204
+ Repository repo = requireReadable(owner, name);
205
+ MergeRequest mr = mergeRequestService.find(repo, id).orElseThrow(NotFoundException::new);
206
+ return Response.status(Response.Status.MOVED_PERMANENTLY).location(detailUri(repo, mr.number)).build();
207
+ }
208
+
198
209
@POST
199
- @jakarta.ws.rs.Path("{id}/comments")
210
+ @jakarta.ws.rs.Path("{number:\\d+}/comments")
200
211
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
201
212
public Response comment(@PathParam("owner") String owner, @PathParam("name") String name,
202
- @PathParam("id") String id, @FormParam("filePath") String filePath,
213
+ @PathParam("number") int number, @FormParam("filePath") String filePath,
203
214
@FormParam("oldLine") @DefaultValue("-1") int oldLine, @FormParam("newLine") @DefaultValue("-1") int newLine,
204
215
@FormParam("body") String body)
205
216
{
206
217
Repository repo = requireReadable(owner, name);
207
- MergeRequest mr = mergeRequestService.find(repo, parseId(id)).orElseThrow(NotFoundException::new);
218
+ MergeRequest mr = mergeRequestService.find(repo, number).orElseThrow(NotFoundException::new);
208
219
commentService.add(currentUser.require(), mr, filePath, oldLine, newLine, body);
209
- return Response.seeOther(detailUri(repo, mr.id)).build();
220
+ return Response.seeOther(detailUri(repo, mr.number)).build();
210
221
}
211
222
212
223
@POST
213
- @jakarta.ws.rs.Path("{id}/comments/{commentId}/delete")
224
+ @jakarta.ws.rs.Path("{number:\\d+}/comments/{commentId}/delete")
214
225
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
215
226
public Response deleteComment(@PathParam("owner") String owner, @PathParam("name") String name,
216
- @PathParam("id") String id, @PathParam("commentId") String commentId)
227
+ @PathParam("number") int number, @PathParam("commentId") String commentId)
217
228
{
218
229
Repository repo = requireReadable(owner, name);
219
- MergeRequest mr = mergeRequestService.find(repo, parseId(id)).orElseThrow(NotFoundException::new);
230
+ MergeRequest mr = mergeRequestService.find(repo, number).orElseThrow(NotFoundException::new);
220
231
MergeRequestComment comment = commentRepo.findById(parseId(commentId));
221
232
if (comment == null || !comment.mergeRequest.id.equals(mr.id))
222
233
{
223
234
throw new NotFoundException();
224
235
}
225
236
commentService.delete(currentUser.require(), comment);
226
- return Response.seeOther(detailUri(repo, mr.id)).build();
237
+ return Response.seeOther(detailUri(repo, mr.number)).build();
227
238
}
228
239
229
240
private static boolean isContent(String lineType)
@@ -232,57 +243,57 @@
232
243
}
233
244
234
245
@POST
235
- @jakarta.ws.rs.Path("{id}/merge")
246
+ @jakarta.ws.rs.Path("{number:\\d+}/merge")
236
247
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
237
248
public Response merge(@PathParam("owner") String owner, @PathParam("name") String name,
238
- @PathParam("id") String id)
249
+ @PathParam("number") int number)
239
250
{
240
251
Repository repo = requireReadable(owner, name);
241
- MergeRequest mr = mergeRequestService.find(repo, parseId(id)).orElseThrow(NotFoundException::new);
252
+ MergeRequest mr = mergeRequestService.find(repo, number).orElseThrow(NotFoundException::new);
242
253
mergeRequestService.merge(currentUser.require(), mr);
243
- return Response.seeOther(detailUri(repo, mr.id)).build();
254
+ return Response.seeOther(detailUri(repo, mr.number)).build();
244
255
}
245
256
246
257
@POST
247
- @jakarta.ws.rs.Path("{id}/assign")
258
+ @jakarta.ws.rs.Path("{number:\\d+}/assign")
248
259
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
249
260
public Response assign(@PathParam("owner") String owner, @PathParam("name") String name,
250
- @PathParam("id") String id, @FormParam("assignee") String assignee)
261
+ @PathParam("number") int number, @FormParam("assignee") String assignee)
251
262
{
252
263
Repository repo = requireReadable(owner, name);
253
- MergeRequest mr = mergeRequestService.find(repo, parseId(id)).orElseThrow(NotFoundException::new);
264
+ MergeRequest mr = mergeRequestService.find(repo, number).orElseThrow(NotFoundException::new);
254
265
// username resolution/validation lives in the service (InvalidMergeRequestException -> 400 via mapper)
255
266
mergeRequestService.assign(currentUser.require(), mr, assignee);
256
- return Response.seeOther(detailUri(repo, mr.id)).build();
267
+ return Response.seeOther(detailUri(repo, mr.number)).build();
257
268
}
258
269
259
270
@POST
260
- @jakarta.ws.rs.Path("{id}/reviewer")
271
+ @jakarta.ws.rs.Path("{number:\\d+}/reviewer")
261
272
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
262
273
public Response reviewer(@PathParam("owner") String owner, @PathParam("name") String name,
263
- @PathParam("id") String id, @FormParam("reviewer") String reviewer)
274
+ @PathParam("number") int number, @FormParam("reviewer") String reviewer)
264
275
{
265
276
Repository repo = requireReadable(owner, name);
266
- MergeRequest mr = mergeRequestService.find(repo, parseId(id)).orElseThrow(NotFoundException::new);
277
+ MergeRequest mr = mergeRequestService.find(repo, number).orElseThrow(NotFoundException::new);
267
278
mergeRequestService.setReviewer(currentUser.require(), mr, reviewer);
268
- return Response.seeOther(detailUri(repo, mr.id)).build();
279
+ return Response.seeOther(detailUri(repo, mr.number)).build();
269
280
}
270
281
271
282
@POST
272
- @jakarta.ws.rs.Path("{id}/close")
283
+ @jakarta.ws.rs.Path("{number:\\d+}/close")
273
284
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
274
285
public Response close(@PathParam("owner") String owner, @PathParam("name") String name,
275
- @PathParam("id") String id)
286
+ @PathParam("number") int number)
276
287
{
277
288
Repository repo = requireReadable(owner, name);
278
- MergeRequest mr = mergeRequestService.find(repo, parseId(id)).orElseThrow(NotFoundException::new);
289
+ MergeRequest mr = mergeRequestService.find(repo, number).orElseThrow(NotFoundException::new);
279
290
mergeRequestService.close(currentUser.require(), mr);
280
- return Response.seeOther(detailUri(repo, mr.id)).build();
291
+ return Response.seeOther(detailUri(repo, mr.number)).build();
281
292
}
282
293
283
- private URI detailUri(Repository repo, UUID id)
294
+ private URI detailUri(Repository repo, int number)
284
295
{
285
- return URI.create("/repos/" + repo.ownerHandle() + "/" + repo.name + "/merge-requests/" + id);
296
+ return URI.create("/repos/" + repo.ownerHandle() + "/" + repo.name + "/merge-requests/" + number);
286
297
}
287
298
288
299
private boolean isOwner(Repository repo)
MODIFY
src/main/resources/templates/MergeRequestResource/mergeRequest.html
+10 -10
@@ -24,13 +24,13 @@
24
24
</summary>
25
25
<div class="assignee-menu">
26
26
<p class="assignee-menu-head">Assign this merge request</p>
27
- <form class="assignee-search" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}/assign">
27
+ <form class="assignee-search" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/assign">
28
28
<input type="text" name="assignee" list="mr-people" placeholder="Type a username…" autocomplete="off">
29
29
</form>
30
30
<ul class="assignee-options">
31
31
{#for candidate in assignees}
32
32
<li>
33
- <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}/assign">
33
+ <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/assign">
34
34
<button type="submit" name="assignee" value="{candidate.username}"
35
35
class="assignee-option{#if mr.assignee && candidate.username == mr.assignee.username} is-selected{/if}">
36
36
<span class="assignee-check" aria-hidden="true">{#if mr.assignee && candidate.username == mr.assignee.username}✓{/if}</span>
@@ -41,7 +41,7 @@
41
41
{/for}
42
42
</ul>
43
43
{#if mr.assignee}
44
- <form class="assignee-clear" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}/assign">
44
+ <form class="assignee-clear" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/assign">
45
45
<button type="submit" class="assignee-option assignee-clear-btn">Clear assignee</button>
46
46
</form>
47
47
{/if}
@@ -62,13 +62,13 @@
62
62
</summary>
63
63
<div class="assignee-menu">
64
64
<p class="assignee-menu-head">Request a review</p>
65
- <form class="assignee-search" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}/reviewer">
65
+ <form class="assignee-search" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/reviewer">
66
66
<input type="text" name="reviewer" list="mr-people" placeholder="Type a username…" autocomplete="off">
67
67
</form>
68
68
<ul class="assignee-options">
69
69
{#for candidate in assignees}
70
70
<li>
71
- <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}/reviewer">
71
+ <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/reviewer">
72
72
<button type="submit" name="reviewer" value="{candidate.username}"
73
73
class="assignee-option{#if mr.reviewer && candidate.username == mr.reviewer.username} is-selected{/if}">
74
74
<span class="assignee-check" aria-hidden="true">{#if mr.reviewer && candidate.username == mr.reviewer.username}✓{/if}</span>
@@ -79,7 +79,7 @@
79
79
{/for}
80
80
</ul>
81
81
{#if mr.reviewer}
82
- <form class="assignee-clear" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}/reviewer">
82
+ <form class="assignee-clear" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/reviewer">
83
83
<button type="submit" class="assignee-option assignee-clear-btn">Clear reviewer</button>
84
84
</form>
85
85
{/if}
@@ -98,10 +98,10 @@
98
98
</div>
99
99
{#if owner && mr.status.name() == 'OPEN'}
100
100
<div class="issue-actions">
101
- <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}/merge">
101
+ <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/merge">
102
102
<button type="submit" class="btn btn-primary">Merge</button>
103
103
</form>
104
- <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}/close"
104
+ <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/close"
105
105
onsubmit="return confirm('Close this merge request without merging?')">
106
106
<button type="submit" class="btn btn-secondary">Close</button>
107
107
</form>
@@ -141,7 +141,7 @@
141
141
</div>
142
142
{#if lv.commentable}
143
143
<div class="dl-comment-form">
144
- <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}/comments">
144
+ <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/comments">
145
145
<input type="hidden" name="filePath" value="{file.path}">
146
146
<input type="hidden" name="oldLine" value="{lv.line.oldLine}">
147
147
<input type="hidden" name="newLine" value="{lv.line.newLine}">
@@ -163,7 +163,7 @@
163
163
</div>
164
164
{#if owner || c.author.id == currentUserId}
165
165
<form class="comment-del-form" method="post"
166
- action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}/comments/{c.id}/delete">
166
+ action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/comments/{c.id}/delete">
167
167
<button type="submit" class="comment-del" title="Delete comment" aria-label="Delete comment">
168
168
<svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true"><path fill="currentColor" d="M9 3h6l1 2h4v2H4V5h4l1-2Zm-3 6h12l-1 11a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L6 9Z"/></svg>
169
169
</button>
MODIFY
src/main/resources/templates/MergeRequestResource/mergeRequests.html
+2 -2
@@ -14,7 +14,7 @@
14
14
{#else}
15
15
<div class="panel">
16
16
{#for mr in open}
17
- <a class="frow" href="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}">
17
+ <a class="frow" href="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}">
18
18
<span class="fname">
19
19
<span class="badge status-{mr.status}">{mr.status.label}</span>
20
20
<span class="n">{mr.title}</span>
@@ -30,7 +30,7 @@
30
30
<summary>Archive <span class="ct">{closed.size}</span></summary>
31
31
<div class="panel">
32
32
{#for mr in closed}
33
- <a class="frow" href="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.id}">
33
+ <a class="frow" href="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}">
34
34
<span class="fname">
35
35
<span class="badge status-{mr.status}">{mr.status.label}</span>
36
36
<span class="n">{mr.title}</span>
MODIFY
src/test/java/de/workaround/web/MergeRequestCommentUiTest.java
+4 -4
@@ -48,7 +48,7 @@
48
48
{
49
49
User owner = persistUser("cmu-owner");
50
50
MergeRequest mr = seededMr(owner, "board");
51
- String detail = "/repos/" + owner.username + "/board/merge-requests/" + mr.id;
51
+ String detail = "/repos/" + owner.username + "/board/merge-requests/" + mr.number;
52
52
53
53
// a commentable line exposes a hover comment icon (not a full-width placeholder bar)
54
54
given().when().get(detail)
@@ -75,7 +75,7 @@
75
75
persistUser("cmu-reader");
76
76
User owner = persistUser("cmu-owner2-" + UUID.randomUUID().toString().substring(0, 8));
77
77
MergeRequest mr = seededMr(owner, "board");
78
- String detail = "/repos/" + owner.username + "/board/merge-requests/" + mr.id;
78
+ String detail = "/repos/" + owner.username + "/board/merge-requests/" + mr.number;
79
79
80
80
given().redirects().follow(false).contentType("application/x-www-form-urlencoded")
81
81
.formParam("filePath", "feature.txt").formParam("oldLine", "-1").formParam("newLine", "1")
@@ -91,7 +91,7 @@
91
91
{
92
92
User owner = persistUser("cmu-anon-" + UUID.randomUUID().toString().substring(0, 8));
93
93
MergeRequest mr = seededMr(owner, "board");
94
- String detail = "/repos/" + owner.username + "/board/merge-requests/" + mr.id;
94
+ String detail = "/repos/" + owner.username + "/board/merge-requests/" + mr.number;
95
95
96
96
given().contentType("application/x-www-form-urlencoded")
97
97
.formParam("filePath", "feature.txt").formParam("oldLine", "-1").formParam("newLine", "1")
@@ -107,7 +107,7 @@
107
107
User owner = persistUser("cmu-del");
108
108
MergeRequest mr = seededMr(owner, "board");
109
109
MergeRequestComment comment = comments.add(owner, mr, "feature.txt", -1, 1, "temporary note");
110
- String detail = "/repos/" + owner.username + "/board/merge-requests/" + mr.id;
110
+ String detail = "/repos/" + owner.username + "/board/merge-requests/" + mr.number;
111
111
112
112
given().when().get(detail).then().statusCode(200).body(containsString("temporary note"));
113
113
MODIFY
src/test/java/de/workaround/web/MergeRequestUiTest.java
+37 -1
@@ -22,6 +22,7 @@
22
22
23
23
import static io.restassured.RestAssured.given;
24
24
import static org.hamcrest.CoreMatchers.containsString;
25
+import static org.hamcrest.CoreMatchers.endsWith;
25
26
import static org.hamcrest.CoreMatchers.not;
26
27
27
28
@QuarkusTest
@@ -167,11 +168,46 @@
167
168
var mr = mergeRequestService.create(owner, repo, "x", null, "feature", "main");
168
169
169
170
given().contentType("application/x-www-form-urlencoded").formParam("assignee", owner.username)
170
- .when().post("/repos/" + owner.username + "/noassignboard/merge-requests/" + mr.id + "/assign")
171
+ .when().post("/repos/" + owner.username + "/noassignboard/merge-requests/" + mr.number + "/assign")
171
172
.then().statusCode(403);
172
173
}
173
174
174
175
@Test
176
+ @TestSecurity(user = "mru-numbers")
177
+ void mergeRequestsUseSequentialNumbersInUrls()
178
+ {
179
+ User owner = persistUser("mru-numbers");
180
+ seed(owner, "numboard");
181
+ String base = "/repos/" + owner.username + "/numboard/merge-requests";
182
+
183
+ // the first MR gets #1; the redirect URL is the number, not a UUID
184
+ given().redirects().follow(false).contentType("application/x-www-form-urlencoded")
185
+ .formParam("title", "First MR").formParam("sourceBranch", "feature").formParam("targetBranch", "main")
186
+ .when().post(base).then().statusCode(303)
187
+ .header("Location", endsWith("/merge-requests/1"));
188
+
189
+ // reachable by number
190
+ given().when().get(base + "/1").then().statusCode(200).body(containsString("First MR"));
191
+
192
+ // the list links to the number URL
193
+ given().when().get(base).then().statusCode(200).body(containsString("/merge-requests/1\""));
194
+ }
195
+
196
+ @Test
197
+ @TestSecurity(user = "mru-legacy")
198
+ void legacyUuidUrlRedirectsToNumber()
199
+ {
200
+ User owner = persistUser("mru-legacy");
201
+ Repository repo = seed(owner, "legboard");
202
+ var mr = mergeRequestService.create(owner, repo, "Legacy link", null, "feature", "main");
203
+ String base = "/repos/" + owner.username + "/legboard/merge-requests";
204
+
205
+ given().redirects().follow(false).when().get(base + "/" + mr.id)
206
+ .then().statusCode(301)
207
+ .header("Location", containsString("/merge-requests/" + mr.number));
208
+ }
209
+
210
+ @Test
175
211
void anonymousCannotCreateMergeRequests()
176
212
{
177
213
User owner = persistUser("mru-anon-" + UUID.randomUUID().toString().substring(0, 8));