1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.server.metadata;
18
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static com.google.common.collect.ImmutableMap.toImmutableMap;
21 import static com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation.asJsonArray;
22 import static com.linecorp.centraldogma.internal.jsonpatch.JsonPatchUtil.encodeSegment;
23 import static com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager.listProjectsWithoutInternal;
24 import static com.linecorp.centraldogma.server.metadata.RepositoryMetadata.DEFAULT_PROJECT_ROLES;
25 import static com.linecorp.centraldogma.server.metadata.Tokens.SECRET_PREFIX;
26 import static com.linecorp.centraldogma.server.metadata.Tokens.validateSecret;
27 import static com.linecorp.centraldogma.server.storage.project.InternalProjectInitializer.INTERNAL_PROJECT_DOGMA;
28 import static java.util.Objects.requireNonNull;
29
30 import java.util.Collection;
31 import java.util.Map;
32 import java.util.Map.Entry;
33 import java.util.Set;
34 import java.util.UUID;
35 import java.util.concurrent.CompletableFuture;
36 import java.util.concurrent.ConcurrentHashMap;
37
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 import com.fasterxml.jackson.core.JsonPointer;
42 import com.fasterxml.jackson.databind.JsonNode;
43 import com.google.common.collect.ImmutableList;
44 import com.google.common.collect.ImmutableMap;
45 import com.google.common.collect.ImmutableMap.Builder;
46 import com.spotify.futures.CompletableFutures;
47
48 import com.linecorp.armeria.common.annotation.Nullable;
49 import com.linecorp.armeria.common.util.Exceptions;
50 import com.linecorp.centraldogma.common.Author;
51 import com.linecorp.centraldogma.common.Change;
52 import com.linecorp.centraldogma.common.ChangeConflictException;
53 import com.linecorp.centraldogma.common.EntryNotFoundException;
54 import com.linecorp.centraldogma.common.ProjectRole;
55 import com.linecorp.centraldogma.common.RedundantChangeException;
56 import com.linecorp.centraldogma.common.RepositoryExistsException;
57 import com.linecorp.centraldogma.common.RepositoryRole;
58 import com.linecorp.centraldogma.common.RepositoryStatus;
59 import com.linecorp.centraldogma.common.Revision;
60 import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
61 import com.linecorp.centraldogma.internal.Jackson;
62 import com.linecorp.centraldogma.server.command.CommandExecutor;
63 import com.linecorp.centraldogma.server.internal.metadata.ProjectMetadataTransformer;
64 import com.linecorp.centraldogma.server.management.ServerStatus;
65 import com.linecorp.centraldogma.server.storage.project.InternalProjectInitializer;
66 import com.linecorp.centraldogma.server.storage.project.Project;
67 import com.linecorp.centraldogma.server.storage.project.ProjectManager;
68
69
70
71
72 public class MetadataService {
73
74 private static final Logger logger = LoggerFactory.getLogger(MetadataService.class);
75
76
77
78
79 public static final String METADATA_JSON = "/metadata.json";
80
81
82
83
84 public static final String TOKEN_JSON = "/tokens.json";
85
86
87
88
89 private static final JsonPointer PROJECT_REMOVAL = JsonPointer.compile("/removal");
90
91 private final ProjectManager projectManager;
92 private final RepositorySupport<ProjectMetadata> metadataRepo;
93 private final RepositorySupport<Tokens> tokenRepo;
94 private final InternalProjectInitializer projectInitializer;
95
96 private final Map<String, CompletableFuture<Revision>> reposInAddingMetadata = new ConcurrentHashMap<>();
97
98
99
100
101 public MetadataService(ProjectManager projectManager, CommandExecutor executor,
102 InternalProjectInitializer projectInitializer) {
103 this.projectManager = requireNonNull(projectManager, "projectManager");
104 this.projectInitializer = requireNonNull(projectInitializer, "projectInitializer");
105 metadataRepo = new RepositorySupport<>(projectManager, executor, ProjectMetadata.class);
106 tokenRepo = new RepositorySupport<>(projectManager, executor, Tokens.class);
107 }
108
109
110
111
112 public CompletableFuture<ProjectMetadata> getProject(String projectName) {
113 requireNonNull(projectName, "projectName");
114 return getOrFetchMetadata(projectName);
115 }
116
117 private CompletableFuture<ProjectMetadata> getOrFetchMetadata(String projectName) {
118 final ProjectMetadata metadata = getMetadata(projectName);
119 final Set<String> reposWithMetadata = metadata.repos().keySet();
120 final Set<String> repos = projectManager.get(projectName).repos().list().keySet();
121
122
123
124
125 final ImmutableList.Builder<CompletableFuture<Revision>> builder = ImmutableList.builder();
126 for (String repo : repos) {
127 if (reposWithMetadata.contains(repo) || Project.isInternalRepo(repo)) {
128 continue;
129 }
130
131 final String projectAndRepositoryName = projectName + '/' + repo;
132 final CompletableFuture<Revision> future = new CompletableFuture<>();
133 final CompletableFuture<Revision> futureInMap =
134 reposInAddingMetadata.computeIfAbsent(projectAndRepositoryName, key -> future);
135 if (futureInMap != future) {
136 builder.add(futureInMap);
137 continue;
138 }
139
140 logger.warn("Adding missing repository metadata: {}/{}", projectName, repo);
141 final Author author = projectManager.get(projectName).repos().get(repo).author();
142 final CompletableFuture<Revision> addRepoFuture = addRepo(author, projectName, repo);
143 addRepoFuture.handle((revision, cause) -> {
144 if (cause != null) {
145 future.completeExceptionally(cause);
146 } else {
147 future.complete(revision);
148 }
149 reposInAddingMetadata.remove(projectAndRepositoryName);
150 return null;
151 });
152 builder.add(future);
153 }
154
155 final ImmutableList<CompletableFuture<Revision>> futures = builder.build();
156 if (futures.isEmpty()) {
157
158 return CompletableFuture.completedFuture(metadata);
159 }
160
161
162 return CompletableFutures.successfulAsList(futures, cause -> {
163 final Throwable peeled = Exceptions.peel(cause);
164
165 if (peeled instanceof RepositoryExistsException) {
166 return null;
167 }
168 return Exceptions.throwUnsafely(cause);
169 }).thenCompose(unused -> {
170 logger.info("Fetching {}/{}{} again",
171 projectName, Project.REPO_DOGMA, METADATA_JSON);
172 return fetchMetadata(projectName);
173 });
174 }
175
176 private ProjectMetadata getMetadata(String projectName) {
177 final Project project = projectManager.get(projectName);
178 final ProjectMetadata metadata = project.metadata();
179 if (metadata == null) {
180 throw new EntryNotFoundException("project metadata not found: " + projectName);
181 }
182 return metadata;
183 }
184
185 private CompletableFuture<ProjectMetadata> fetchMetadata(String projectName) {
186 return metadataRepo.fetch(projectName, Project.REPO_DOGMA, METADATA_JSON)
187 .thenApply(HolderWithRevision::object);
188 }
189
190
191
192
193 public CompletableFuture<Revision> removeProject(Author author, String projectName) {
194 requireNonNull(author, "author");
195 requireNonNull(projectName, "projectName");
196
197 final Change<JsonNode> change = Change.ofJsonPatch(
198 METADATA_JSON,
199 asJsonArray(JsonPatchOperation.testAbsence(PROJECT_REMOVAL),
200 JsonPatchOperation.add(PROJECT_REMOVAL,
201 Jackson.valueToTree(UserAndTimestamp.of(author)))));
202 return metadataRepo.push(projectName, Project.REPO_DOGMA, author,
203 "Remove the project: " + projectName, change);
204 }
205
206
207
208
209 public CompletableFuture<Revision> restoreProject(Author author, String projectName) {
210 requireNonNull(author, "author");
211 requireNonNull(projectName, "projectName");
212
213 final Change<JsonNode> change =
214 Change.ofJsonPatch(METADATA_JSON, JsonPatchOperation.remove(PROJECT_REMOVAL).toJsonNode());
215 return metadataRepo.push(projectName, Project.REPO_DOGMA, author,
216 "Restore the project: " + projectName, change);
217 }
218
219
220
221
222 public CompletableFuture<Member> getMember(String projectName, User user) {
223 requireNonNull(projectName, "projectName");
224 requireNonNull(user, "user");
225
226 return getProject(projectName).thenApply(
227 project -> project.memberOrDefault(user.id(), null));
228 }
229
230
231
232
233
234 public CompletableFuture<Revision> addMember(Author author, String projectName,
235 User member, ProjectRole projectRole) {
236 requireNonNull(author, "author");
237 requireNonNull(projectName, "projectName");
238 requireNonNull(member, "member");
239 requireNonNull(projectRole, "projectRole");
240
241 final Member newMember = new Member(member, projectRole, UserAndTimestamp.of(author));
242 final JsonPointer path = JsonPointer.compile("/members" + encodeSegment(newMember.id()));
243 final Change<JsonNode> change =
244 Change.ofJsonPatch(METADATA_JSON,
245 asJsonArray(JsonPatchOperation.testAbsence(path),
246 JsonPatchOperation.add(path, Jackson.valueToTree(newMember))));
247 final String commitSummary =
248 "Add a member '" + newMember.id() + "' to the project '" + projectName + '\'';
249 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
250 }
251
252
253
254
255
256
257 public CompletableFuture<Revision> removeMember(Author author, String projectName, User user) {
258 requireNonNull(author, "author");
259 requireNonNull(projectName, "projectName");
260 requireNonNull(user, "user");
261
262 final String memberId = user.id();
263 final String commitSummary =
264 "Remove the member '" + memberId + "' from the project '" + projectName + '\'';
265
266 final ProjectMetadataTransformer transformer =
267 new ProjectMetadataTransformer((headRevision, projectMetadata) -> {
268 projectMetadata.member(memberId);
269 final Map<String, Member> newMembers = removeFromMap(projectMetadata.members(), memberId);
270 final ImmutableMap<String, RepositoryMetadata> newRepos =
271 removeMemberFromRepositories(projectMetadata, memberId);
272 return new ProjectMetadata(projectMetadata.name(),
273 newRepos,
274 newMembers,
275 projectMetadata.tokens(),
276 projectMetadata.creation(),
277 projectMetadata.removal());
278 });
279 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, transformer);
280 }
281
282 private static ImmutableMap<String, RepositoryMetadata> removeMemberFromRepositories(
283 ProjectMetadata projectMetadata, String memberId) {
284 final ImmutableMap.Builder<String, RepositoryMetadata> reposBuilder =
285 ImmutableMap.builderWithExpectedSize(projectMetadata.repos().size());
286 for (Entry<String, RepositoryMetadata> entry : projectMetadata.repos().entrySet()) {
287 final RepositoryMetadata repositoryMetadata = entry.getValue();
288 final Roles roles = repositoryMetadata.roles();
289 final Map<String, RepositoryRole> users = roles.users();
290 if (users.get(memberId) != null) {
291 final ImmutableMap<String, RepositoryRole> newUsers = removeFromMap(users, memberId);
292 final Roles newRoles = new Roles(roles.projectRoles(), newUsers, roles.tokens());
293 reposBuilder.put(entry.getKey(),
294 new RepositoryMetadata(repositoryMetadata.name(),
295 newRoles,
296 repositoryMetadata.creation(),
297 repositoryMetadata.removal(),
298 repositoryMetadata.status()));
299 } else {
300 reposBuilder.put(entry);
301 }
302 }
303 return reposBuilder.build();
304 }
305
306
307
308
309 public CompletableFuture<Revision> updateMemberRole(Author author, String projectName,
310 User member, ProjectRole projectRole) {
311 requireNonNull(author, "author");
312 requireNonNull(projectName, "projectName");
313 requireNonNull(member, "member");
314 requireNonNull(projectRole, "projectRole");
315
316 final Change<JsonNode> change = Change.ofJsonPatch(
317 METADATA_JSON,
318 JsonPatchOperation.replace(
319 JsonPointer.compile("/members" + encodeSegment(member.id()) + "/role"),
320 Jackson.valueToTree(projectRole)).toJsonNode());
321 final String commitSummary = "Updates the role of the member '" + member.id() +
322 "' as '" + projectRole + "' for the project '" + projectName + '\'';
323 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
324 }
325
326
327
328
329
330 public CompletableFuture<RepositoryMetadata> getRepo(String projectName, String repoName) {
331 requireNonNull(projectName, "projectName");
332 requireNonNull(repoName, "repoName");
333
334 return getProject(projectName).thenApply(project -> project.repo(repoName));
335 }
336
337
338
339
340
341
342 public CompletableFuture<Revision> addRepo(Author author, String projectName, String repoName) {
343 return addRepo(author, projectName, repoName, DEFAULT_PROJECT_ROLES);
344 }
345
346
347
348
349
350 public CompletableFuture<Revision> addRepo(Author author, String projectName, String repoName,
351 ProjectRoles projectRoles) {
352
353 requireNonNull(author, "author");
354 requireNonNull(projectName, "projectName");
355 requireNonNull(repoName, "repoName");
356
357 final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName));
358 final RepositoryMetadata newRepositoryMetadata =
359 RepositoryMetadata.of(repoName, UserAndTimestamp.of(author), projectRoles);
360 final Change<JsonNode> change =
361 Change.ofJsonPatch(METADATA_JSON,
362 asJsonArray(JsonPatchOperation.testAbsence(path),
363 JsonPatchOperation.add(
364 path, Jackson.valueToTree(newRepositoryMetadata))));
365 final String commitSummary =
366 "Add a repo '" + newRepositoryMetadata.id() + "' to the project '" + projectName + '\'';
367 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change)
368 .handle((revision, cause) -> {
369 if (cause != null) {
370 if (Exceptions.peel(cause) instanceof ChangeConflictException) {
371 throw new RepositoryExistsException(repoName);
372 } else {
373 return Exceptions.throwUnsafely(cause);
374 }
375 }
376 return revision;
377 });
378 }
379
380
381
382
383
384 public CompletableFuture<Revision> removeRepo(Author author, String projectName, String repoName) {
385 requireNonNull(author, "author");
386 requireNonNull(projectName, "projectName");
387 requireNonNull(repoName, "repoName");
388
389 final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName) + "/removal");
390 final Change<JsonNode> change =
391 Change.ofJsonPatch(METADATA_JSON,
392 asJsonArray(JsonPatchOperation.testAbsence(path),
393 JsonPatchOperation.add(path, Jackson.valueToTree(
394 UserAndTimestamp.of(author)))));
395 final String commitSummary =
396 "Remove the repo '" + repoName + "' from the project '" + projectName + '\'';
397 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
398 }
399
400
401
402
403
404 public CompletableFuture<Revision> purgeRepo(Author author, String projectName, String repoName) {
405 requireNonNull(author, "author");
406 requireNonNull(projectName, "projectName");
407 requireNonNull(repoName, "repoName");
408
409 final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName));
410 final Change<JsonNode> change = Change.ofJsonPatch(METADATA_JSON,
411 JsonPatchOperation.remove(path).toJsonNode());
412 final String commitSummary =
413 "Purge the repo '" + repoName + "' from the project '" + projectName + '\'';
414 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
415 }
416
417
418
419
420
421 public CompletableFuture<Revision> restoreRepo(Author author, String projectName, String repoName) {
422 requireNonNull(author, "author");
423 requireNonNull(projectName, "projectName");
424 requireNonNull(repoName, "repoName");
425
426 final Change<JsonNode> change =
427 Change.ofJsonPatch(METADATA_JSON,
428 JsonPatchOperation.remove(JsonPointer.compile(
429 "/repos" + encodeSegment(repoName) + "/removal")).toJsonNode());
430 final String commitSummary =
431 "Restore the repo '" + repoName + "' from the project '" + projectName + '\'';
432 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
433 }
434
435
436
437
438
439 public CompletableFuture<Revision> updateRepositoryProjectRoles(Author author,
440 String projectName, String repoName,
441 ProjectRoles projectRoles) {
442 requireNonNull(author, "author");
443 requireNonNull(projectName, "projectName");
444 requireNonNull(repoName, "repoName");
445
446 if (Project.isInternalRepo(repoName)) {
447 throw new UnsupportedOperationException(
448 "Can't update role for internal repository: " + repoName);
449 }
450
451 final String commitSummary =
452 "Update the project roles of the '" + repoName + "' in the project '" + projectName + '\'';
453 final RepositoryMetadataTransformer transformer = new RepositoryMetadataTransformer(
454 repoName, (headRevision, repositoryMetadata) -> {
455 final Roles newRoles = new Roles(projectRoles, repositoryMetadata.roles().users(),
456 repositoryMetadata.roles().tokens());
457 return new RepositoryMetadata(repositoryMetadata.name(),
458 newRoles,
459 repositoryMetadata.creation(),
460 repositoryMetadata.removal(),
461 repositoryMetadata.status());
462 });
463 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, transformer);
464 }
465
466
467
468
469 public CompletableFuture<Revision> addToken(Author author, String projectName,
470 Token token, ProjectRole role) {
471 return addToken(author, projectName, requireNonNull(token, "token").appId(), role);
472 }
473
474
475
476
477 public CompletableFuture<Revision> addToken(Author author, String projectName,
478 String appId, ProjectRole role) {
479 requireNonNull(author, "author");
480 requireNonNull(projectName, "projectName");
481 requireNonNull(appId, "appId");
482 requireNonNull(role, "role");
483
484 getTokens().get(appId);
485 final TokenRegistration registration = new TokenRegistration(appId, role,
486 UserAndTimestamp.of(author));
487 final JsonPointer path = JsonPointer.compile("/tokens" + encodeSegment(registration.id()));
488 final Change<JsonNode> change =
489 Change.ofJsonPatch(METADATA_JSON,
490 asJsonArray(JsonPatchOperation.testAbsence(path),
491 JsonPatchOperation.add(path,
492 Jackson.valueToTree(registration))));
493 final String commitSummary = "Add a token '" + registration.id() +
494 "' to the project '" + projectName + "' with a role '" + role + '\'';
495 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
496 }
497
498
499
500
501
502 public CompletableFuture<Revision> removeToken(Author author, String projectName, Token token) {
503 return removeToken(author, projectName, requireNonNull(token, "token").appId());
504 }
505
506
507
508
509
510
511 public CompletableFuture<Revision> removeToken(Author author, String projectName, String appId) {
512 requireNonNull(author, "author");
513 requireNonNull(projectName, "projectName");
514 requireNonNull(appId, "appId");
515
516 return removeToken(projectName, author, appId, false);
517 }
518
519 private CompletableFuture<Revision> removeToken(String projectName, Author author, String appId,
520 boolean quiet) {
521 final String commitSummary = "Remove the token '" + appId + "' from the project '" + projectName + '\'';
522 final ProjectMetadataTransformer transformer =
523 new ProjectMetadataTransformer((headRevision, projectMetadata) -> {
524 final Map<String, TokenRegistration> tokens = projectMetadata.tokens();
525 final Map<String, TokenRegistration> newTokens;
526 if (tokens.get(appId) == null) {
527 if (!quiet) {
528 throw new TokenNotFoundException(
529 "failed to find the token " + appId + " in project " + projectName);
530 }
531 newTokens = tokens;
532 } else {
533 newTokens = tokens.entrySet()
534 .stream()
535 .filter(entry -> !entry.getKey().equals(appId))
536 .collect(toImmutableMap(Entry::getKey, Entry::getValue));
537 }
538
539 final ImmutableMap<String, RepositoryMetadata> newRepos =
540 removeTokenFromRepositories(appId, projectMetadata);
541 return new ProjectMetadata(projectMetadata.name(),
542 newRepos,
543 projectMetadata.members(),
544 newTokens,
545 projectMetadata.creation(),
546 projectMetadata.removal());
547 });
548 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, transformer);
549 }
550
551 private static ImmutableMap<String, RepositoryMetadata> removeTokenFromRepositories(
552 String appId, ProjectMetadata projectMetadata) {
553 final ImmutableMap.Builder<String, RepositoryMetadata> builder =
554 ImmutableMap.builderWithExpectedSize(projectMetadata.repos().size());
555 for (Entry<String, RepositoryMetadata> entry : projectMetadata.repos().entrySet()) {
556 final RepositoryMetadata repositoryMetadata = entry.getValue();
557 final Roles roles = repositoryMetadata.roles();
558 if (roles.tokens().get(appId) != null) {
559 final Map<String, RepositoryRole> newTokens = removeFromMap(roles.tokens(), appId);
560 final Roles newRoles = new Roles(roles.projectRoles(), roles.users(), newTokens);
561 builder.put(entry.getKey(), new RepositoryMetadata(repositoryMetadata.name(),
562 newRoles,
563 repositoryMetadata.creation(),
564 repositoryMetadata.removal(),
565 repositoryMetadata.status()));
566 } else {
567 builder.put(entry);
568 }
569 }
570 return builder.build();
571 }
572
573
574
575
576 public CompletableFuture<Revision> updateTokenRole(Author author, String projectName,
577 Token token, ProjectRole role) {
578 requireNonNull(author, "author");
579 requireNonNull(projectName, "projectName");
580 requireNonNull(token, "token");
581 requireNonNull(role, "role");
582 final TokenRegistration registration = new TokenRegistration(token.appId(), role,
583 UserAndTimestamp.of(author));
584 final JsonPointer path = JsonPointer.compile("/tokens" + encodeSegment(registration.id()));
585 final Change<JsonNode> change =
586 Change.ofJsonPatch(METADATA_JSON,
587 JsonPatchOperation.replace(
588 path, Jackson.valueToTree(registration)).toJsonNode());
589 final String commitSummary = "Update the role of a token '" + token.appId() +
590 "' as '" + role + "' for the project '" + projectName + '\'';
591 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
592 }
593
594
595
596
597
598 public CompletableFuture<Revision> addUserRepositoryRole(Author author, String projectName,
599 String repoName, User member,
600 RepositoryRole role) {
601 requireNonNull(author, "author");
602 requireNonNull(projectName, "projectName");
603 requireNonNull(repoName, "repoName");
604 requireNonNull(member, "member");
605 requireNonNull(role, "role");
606
607 return getProject(projectName).thenCompose(project -> {
608 project.repo(repoName);
609 ensureProjectMember(project, member);
610 final String commitSummary = "Add repository role of '" + member.id() +
611 "' as '" + role + "' to '" + projectName + '/' + repoName + '\n';
612 final RepositoryMetadataTransformer transformer = new RepositoryMetadataTransformer(
613 repoName, (headRevision, repositoryMetadata) -> {
614 final Roles roles = repositoryMetadata.roles();
615 if (roles.users().get(member.id()) != null) {
616 throw new ChangeConflictException(
617 "the member " + member.id() + " is already added to '" +
618 projectName + '/' + repoName + '\'');
619 }
620
621 final Map<String, RepositoryRole> users = roles.users();
622 final ImmutableMap<String, RepositoryRole> newUsers = addToMap(users, member.id(), role);
623 final Roles newRoles = new Roles(roles.projectRoles(), newUsers, roles.tokens());
624 return new RepositoryMetadata(repositoryMetadata.name(),
625 newRoles,
626 repositoryMetadata.creation(),
627 repositoryMetadata.removal(),
628 repositoryMetadata.status());
629 });
630 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, transformer);
631 });
632 }
633
634
635
636
637
638 public CompletableFuture<Revision> removeUserRepositoryRole(Author author, String projectName,
639 String repoName, User member) {
640 requireNonNull(author, "author");
641 requireNonNull(projectName, "projectName");
642 requireNonNull(repoName, "repoName");
643 requireNonNull(member, "member");
644
645 final String memberId = member.id();
646 final RepositoryMetadataTransformer transformer = new RepositoryMetadataTransformer(
647 repoName, (headRevision, repositoryMetadata) -> {
648 final Roles roles = repositoryMetadata.roles();
649 if (roles.users().get(memberId) == null) {
650 throw new MemberNotFoundException(memberId, projectName, repoName);
651 }
652
653 final Map<String, RepositoryRole> newUsers = removeFromMap(roles.users(), memberId);
654 final Roles newRoles = new Roles(roles.projectRoles(), newUsers, roles.tokens());
655 return new RepositoryMetadata(repositoryMetadata.name(),
656 newRoles,
657 repositoryMetadata.creation(),
658 repositoryMetadata.removal(),
659 repositoryMetadata.status());
660 });
661 final String commitSummary = "Remove repository role of the '" + memberId +
662 "' from '" + projectName + '/' + repoName + '\'';
663 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, transformer);
664 }
665
666
667
668
669
670 public CompletableFuture<Revision> updateUserRepositoryRole(Author author, String projectName,
671 String repoName, User member,
672 RepositoryRole role) {
673 requireNonNull(author, "author");
674 requireNonNull(projectName, "projectName");
675 requireNonNull(repoName, "repoName");
676 requireNonNull(member, "member");
677 requireNonNull(role, "role");
678
679 final String memberId = member.id();
680 final RepositoryMetadataTransformer transformer = new RepositoryMetadataTransformer(
681 repoName, (headRevision, repositoryMetadata) -> {
682 final Roles roles = repositoryMetadata.roles();
683 final RepositoryRole oldRepositoryRole = roles.users().get(memberId);
684 if (oldRepositoryRole == null) {
685 throw new MemberNotFoundException(memberId, projectName, repoName);
686 }
687
688 if (oldRepositoryRole == role) {
689 throw new RedundantChangeException(
690 headRevision,
691 "the repository role of " + memberId + " in '" + projectName + '/' + repoName +
692 "' isn't changed.");
693 }
694
695 final Map<String, RepositoryRole> newUsers = updateMap(roles.users(), memberId, role);
696 final Roles newRoles = new Roles(roles.projectRoles(), newUsers, roles.tokens());
697 return new RepositoryMetadata(repositoryMetadata.name(),
698 newRoles,
699 repositoryMetadata.creation(),
700 repositoryMetadata.removal(),
701 repositoryMetadata.status());
702 });
703 final String commitSummary = "Update repository role of the '" + memberId + "' as '" + role +
704 "' for '" + projectName + '/' + repoName + '\'';
705 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, transformer);
706 }
707
708
709
710
711
712 public CompletableFuture<Revision> addTokenRepositoryRole(Author author, String projectName,
713 String repoName, String appId,
714 RepositoryRole role) {
715 requireNonNull(author, "author");
716 requireNonNull(projectName, "projectName");
717 requireNonNull(repoName, "repoName");
718 requireNonNull(appId, "appId");
719 requireNonNull(role, "role");
720
721 return getProject(projectName).thenCompose(project -> {
722 project.repo(repoName);
723 ensureProjectToken(project, appId);
724 final String commitSummary = "Add repository role of the token '" + appId + "' as '" + role +
725 "' to '" + projectName + '/' + repoName + "'\n";
726 final RepositoryMetadataTransformer transformer = new RepositoryMetadataTransformer(
727 repoName, (headRevision, repositoryMetadata) -> {
728 final Roles roles = repositoryMetadata.roles();
729 if (roles.tokens().get(appId) != null) {
730 throw new ChangeConflictException(
731 "the token " + appId + " is already added to '" +
732 projectName + '/' + repoName + '\'');
733 }
734
735 final Map<String, RepositoryRole> newTokens = addToMap(roles.tokens(), appId, role);
736 final Roles newRoles = new Roles(roles.projectRoles(), roles.users(), newTokens);
737 return new RepositoryMetadata(repositoryMetadata.name(),
738 newRoles,
739 repositoryMetadata.creation(),
740 repositoryMetadata.removal(),
741 repositoryMetadata.status());
742 });
743 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, transformer);
744 });
745 }
746
747
748
749
750
751 public CompletableFuture<Revision> removeTokenRepositoryRole(Author author, String projectName,
752 String repoName, String appId) {
753 requireNonNull(author, "author");
754 requireNonNull(projectName, "projectName");
755 requireNonNull(repoName, "repoName");
756 requireNonNull(appId, "appId");
757
758 final RepositoryMetadataTransformer transformer = new RepositoryMetadataTransformer(
759 repoName, (headRevision, repositoryMetadata) -> {
760 final Roles roles = repositoryMetadata.roles();
761 if (roles.tokens().get(appId) == null) {
762 throw new ChangeConflictException(
763 "the token " + appId + " doesn't exist at '" +
764 projectName + '/' + repoName + '\'');
765 }
766
767 final Map<String, RepositoryRole> newTokens = removeFromMap(roles.tokens(), appId);
768 final Roles newRoles = new Roles(roles.projectRoles(), roles.users(), newTokens);
769 return new RepositoryMetadata(repositoryMetadata.name(),
770 newRoles,
771 repositoryMetadata.creation(),
772 repositoryMetadata.removal(),
773 repositoryMetadata.status());
774 });
775 final String commitSummary = "Remove repository role of the token '" + appId +
776 "' from '" + projectName + '/' + repoName + '\'';
777 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, transformer);
778 }
779
780
781
782
783
784 public CompletableFuture<Revision> updateTokenRepositoryRole(Author author, String projectName,
785 String repoName, String appId,
786 RepositoryRole role) {
787 requireNonNull(author, "author");
788 requireNonNull(projectName, "projectName");
789 requireNonNull(repoName, "repoName");
790 requireNonNull(appId, "appId");
791 requireNonNull(role, "role");
792
793 final RepositoryMetadataTransformer transformer = new RepositoryMetadataTransformer(
794 repoName, (headRevision, repositoryMetadata) -> {
795 final Roles roles = repositoryMetadata.roles();
796 final RepositoryRole oldRepositoryRole = roles.tokens().get(appId);
797 if (oldRepositoryRole == null) {
798 throw new TokenNotFoundException(
799 "the token " + appId + " doesn't exist at '" +
800 projectName + '/' + repoName + '\'');
801 }
802
803 if (oldRepositoryRole == role) {
804 throw new RedundantChangeException(
805 headRevision,
806 "the permission of " + appId + " in '" + projectName + '/' + repoName +
807 "' isn't changed.");
808 }
809
810 final Map<String, RepositoryRole> newTokens = updateMap(roles.tokens(), appId, role);
811 final Roles newRoles = new Roles(roles.projectRoles(), roles.users(), newTokens);
812 return new RepositoryMetadata(repositoryMetadata.name(),
813 newRoles,
814 repositoryMetadata.creation(),
815 repositoryMetadata.removal(),
816 repositoryMetadata.status());
817 });
818 final String commitSummary = "Update repository role of the token '" + appId +
819 "' for '" + projectName + '/' + repoName + '\'';
820 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, transformer);
821 }
822
823
824
825
826
827
828 public CompletableFuture<RepositoryRole> findRepositoryRole(String projectName, String repoName,
829 User user) {
830 requireNonNull(projectName, "projectName");
831 requireNonNull(repoName, "repoName");
832 requireNonNull(user, "user");
833 if (user.isSystemAdmin()) {
834 return CompletableFuture.completedFuture(RepositoryRole.ADMIN);
835 }
836 if (user instanceof UserWithToken) {
837 return findRepositoryRole(projectName, repoName, ((UserWithToken) user).token());
838 }
839 return findRepositoryRole0(projectName, repoName, user);
840 }
841
842
843
844
845
846
847 public CompletableFuture<RepositoryRole> findRepositoryRole(String projectName, String repoName,
848 Token token) {
849 requireNonNull(projectName, "projectName");
850 requireNonNull(repoName, "repoName");
851 requireNonNull(token, "token");
852
853 return getProject(projectName).thenApply(metadata -> {
854 final RepositoryMetadata repositoryMetadata = metadata.repo(repoName);
855 final Roles roles = repositoryMetadata.roles();
856 final String appId = token.appId();
857 final RepositoryRole tokenRepositoryRole = roles.tokens().get(appId);
858
859 final TokenRegistration projectTokenRegistration = metadata.tokens().get(appId);
860 final ProjectRole projectRole;
861 if (projectTokenRegistration != null) {
862 projectRole = projectTokenRegistration.role();
863 } else {
864
865 assert !token.isSystemAdmin();
866 if (token.allowGuestAccess()) {
867 projectRole = ProjectRole.GUEST;
868 } else {
869
870 return null;
871 }
872 }
873 return repositoryRole(roles, tokenRepositoryRole, projectRole);
874 });
875 }
876
877 private CompletableFuture<RepositoryRole> findRepositoryRole0(String projectName, String repoName,
878 User user) {
879 requireNonNull(projectName, "projectName");
880 requireNonNull(repoName, "repoName");
881 requireNonNull(user, "user");
882
883 return getProject(projectName).thenApply(metadata -> {
884 final RepositoryMetadata repositoryMetadata = metadata.repo(repoName);
885 final Roles roles = repositoryMetadata.roles();
886 final RepositoryRole userRepositoryRole = roles.users().get(user.id());
887
888 final Member projectUser = metadata.memberOrDefault(user.id(), null);
889 final ProjectRole projectRole = projectUser != null ? projectUser.role() : ProjectRole.GUEST;
890 return repositoryRole(roles, userRepositoryRole, projectRole);
891 });
892 }
893
894 @Nullable
895 private static RepositoryRole repositoryRole(Roles roles, @Nullable RepositoryRole repositoryRole,
896 ProjectRole projectRole) {
897 if (projectRole == ProjectRole.OWNER) {
898 return RepositoryRole.ADMIN;
899 }
900
901 final RepositoryRole memberOrGuestRole;
902 if (projectRole == ProjectRole.MEMBER) {
903 memberOrGuestRole = roles.projectRoles().member();
904 } else {
905 assert projectRole == ProjectRole.GUEST;
906 memberOrGuestRole = roles.projectRoles().guest();
907 }
908
909 if (repositoryRole == RepositoryRole.ADMIN || memberOrGuestRole == RepositoryRole.ADMIN) {
910 return RepositoryRole.ADMIN;
911 }
912
913 if (repositoryRole == RepositoryRole.WRITE || memberOrGuestRole == RepositoryRole.WRITE) {
914 return RepositoryRole.WRITE;
915 }
916
917 if (repositoryRole == RepositoryRole.READ || memberOrGuestRole == RepositoryRole.READ) {
918 return RepositoryRole.READ;
919 }
920
921 return null;
922 }
923
924
925
926
927 public CompletableFuture<ProjectRole> findProjectRole(String projectName, User user) {
928 requireNonNull(projectName, "projectName");
929 requireNonNull(user, "user");
930
931 if (user.isSystemAdmin()) {
932 return CompletableFuture.completedFuture(ProjectRole.OWNER);
933 }
934 return getProject(projectName).thenApply(project -> {
935 if (user instanceof UserWithToken) {
936 final TokenRegistration registration = project.tokens().getOrDefault(
937 ((UserWithToken) user).token().id(), null);
938 return registration != null ? registration.role() : ProjectRole.GUEST;
939 } else {
940 final Member member = project.memberOrDefault(user.id(), null);
941 return member != null ? member.role() : ProjectRole.GUEST;
942 }
943 });
944 }
945
946
947
948
949 public CompletableFuture<Tokens> fetchTokens() {
950 return tokenRepo.fetch(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, TOKEN_JSON)
951 .thenApply(HolderWithRevision::object);
952 }
953
954
955
956
957 public Tokens getTokens() {
958 return projectInitializer.tokens();
959 }
960
961
962
963
964
965 public CompletableFuture<Revision> createToken(Author author, String appId) {
966 return createToken(author, appId, false);
967 }
968
969
970
971
972
973 public CompletableFuture<Revision> createToken(Author author, String appId, boolean isSystemAdmin) {
974 return createToken(author, appId, SECRET_PREFIX + UUID.randomUUID(), isSystemAdmin);
975 }
976
977
978
979
980 public CompletableFuture<Revision> createToken(Author author, String appId, String secret) {
981 return createToken(author, appId, secret, false);
982 }
983
984
985
986
987 public CompletableFuture<Revision> createToken(Author author, String appId, String secret,
988 boolean isSystemAdmin) {
989 requireNonNull(author, "author");
990 requireNonNull(appId, "appId");
991 requireNonNull(secret, "secret");
992
993 checkArgument(secret.startsWith(SECRET_PREFIX), "secret must start with: " + SECRET_PREFIX);
994
995
996 final boolean allowGuestAccess = isSystemAdmin;
997 final Token newToken = new Token(appId, secret, isSystemAdmin, allowGuestAccess,
998 UserAndTimestamp.of(author));
999 final JsonPointer appIdPath = JsonPointer.compile("/appIds" + encodeSegment(newToken.id()));
1000 final String newTokenSecret = newToken.secret();
1001 assert newTokenSecret != null;
1002 final JsonPointer secretPath = JsonPointer.compile("/secrets" + encodeSegment(newTokenSecret));
1003 final Change<JsonNode> change =
1004 Change.ofJsonPatch(TOKEN_JSON,
1005 asJsonArray(JsonPatchOperation.testAbsence(appIdPath),
1006 JsonPatchOperation.testAbsence(secretPath),
1007 JsonPatchOperation.add(appIdPath, Jackson.valueToTree(newToken)),
1008 JsonPatchOperation.add(secretPath,
1009 Jackson.valueToTree(newToken.id()))));
1010 return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author,
1011 "Add a token: " + newToken.id(), change);
1012 }
1013
1014
1015
1016
1017 public CompletableFuture<Revision> destroyToken(Author author, String appId) {
1018 requireNonNull(author, "author");
1019 requireNonNull(appId, "appId");
1020
1021 final String commitSummary = "Destroy the token: " + appId;
1022 final UserAndTimestamp userAndTimestamp = UserAndTimestamp.of(author);
1023
1024 final TokensTransformer transformer = new TokensTransformer((headRevision, tokens) -> {
1025 final Token token = tokens.get(appId);
1026 if (token.deletion() != null) {
1027 throw new ChangeConflictException("The token is already destroyed: " + appId);
1028 }
1029
1030 final String secret = token.secret();
1031 assert secret != null;
1032 final Token newToken = new Token(token.appId(), secret, token.isSystemAdmin(),
1033 token.isSystemAdmin(), token.allowGuestAccess(),
1034 token.creation(), token.deactivation(), userAndTimestamp);
1035 return new Tokens(updateMap(tokens.appIds(), appId, newToken), tokens.secrets());
1036 });
1037 return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author, commitSummary, transformer);
1038 }
1039
1040
1041
1042
1043
1044
1045 public Revision purgeToken(Author author, String appId) {
1046 requireNonNull(author, "author");
1047 requireNonNull(appId, "appId");
1048
1049 final Collection<Project> projects = listProjectsWithoutInternal(projectManager.list(),
1050 User.SYSTEM_ADMIN).values();
1051
1052 for (Project project : projects) {
1053
1054 final ProjectMetadata projectMetadata = fetchMetadata(project.name()).join();
1055 final boolean containsTargetTokenInTheProject =
1056 projectMetadata.tokens().values()
1057 .stream()
1058 .anyMatch(token -> token.appId().equals(appId));
1059
1060 if (containsTargetTokenInTheProject) {
1061 removeToken(project.name(), author, appId, true).join();
1062 }
1063 }
1064
1065 final String commitSummary = "Remove the token: " + appId;
1066
1067 final TokensTransformer transformer = new TokensTransformer((headRevision, tokens) -> {
1068 final Token token = tokens.get(appId);
1069 final Map<String, Token> newAppIds = removeFromMap(tokens.appIds(), appId);
1070 final String secret = token.secret();
1071 assert secret != null;
1072 final Map<String, String> newSecrets = removeFromMap(tokens.secrets(), secret);
1073 return new Tokens(newAppIds, newSecrets);
1074 });
1075 return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author, commitSummary, transformer)
1076 .join();
1077 }
1078
1079
1080
1081
1082 public CompletableFuture<Revision> activateToken(Author author, String appId) {
1083 requireNonNull(author, "author");
1084 requireNonNull(appId, "appId");
1085
1086 final String commitSummary = "Enable the token: " + appId;
1087
1088 final TokensTransformer transformer = new TokensTransformer((headRevision, tokens) -> {
1089 final Token token = tokens.get(appId);
1090 if (token.deactivation() == null) {
1091 throw new RedundantChangeException(headRevision, "The token is already activated: " + appId);
1092 }
1093 final String secret = token.secret();
1094 assert secret != null;
1095 final Map<String, String> newSecrets =
1096 addToMap(tokens.secrets(), secret, appId);
1097 final Token newToken = new Token(token.appId(), secret, token.isSystemAdmin(),
1098 token.allowGuestAccess(), token.creation());
1099 return new Tokens(updateMap(tokens.appIds(), appId, newToken), newSecrets);
1100 });
1101 return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author, commitSummary, transformer);
1102 }
1103
1104
1105
1106
1107 public CompletableFuture<Revision> deactivateToken(Author author, String appId) {
1108 requireNonNull(author, "author");
1109 requireNonNull(appId, "appId");
1110
1111 final String commitSummary = "Deactivate the token: " + appId;
1112 final UserAndTimestamp userAndTimestamp = UserAndTimestamp.of(author);
1113
1114 final TokensTransformer transformer = new TokensTransformer((headRevision, tokens) -> {
1115 final Token token = tokens.get(appId);
1116 if (token.deactivation() != null) {
1117 throw new RedundantChangeException(headRevision, "The token is already deactivated: " + appId);
1118 }
1119 final String secret = token.secret();
1120 assert secret != null;
1121 final Token newToken = new Token(token.appId(), secret, token.isSystemAdmin(),
1122 token.isSystemAdmin(), token.allowGuestAccess(), token.creation(),
1123 userAndTimestamp, null);
1124 final Map<String, Token> newAppIds = updateMap(tokens.appIds(), appId, newToken);
1125 final Map<String, String> newSecrets =
1126 removeFromMap(tokens.secrets(), secret);
1127 return new Tokens(newAppIds, newSecrets);
1128 });
1129 return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author, commitSummary, transformer);
1130 }
1131
1132
1133
1134
1135 public CompletableFuture<Revision> updateTokenLevel(Author author, String appId, boolean toBeSystemAdmin) {
1136 requireNonNull(author, "author");
1137 requireNonNull(appId, "appId");
1138 final String commitSummary =
1139 "Update the token level: " + appId + " to " + (toBeSystemAdmin ? "admin" : "user");
1140 final TokensTransformer transformer = new TokensTransformer((headRevision, tokens) -> {
1141 final Token token = tokens.get(appId);
1142 if (toBeSystemAdmin == token.isSystemAdmin()) {
1143 throw new RedundantChangeException(
1144 headRevision,
1145 "The token is already " + (toBeSystemAdmin ? "admin" : "user"));
1146 }
1147
1148 final Token newToken = token.withSystemAdmin(toBeSystemAdmin);
1149 return new Tokens(updateMap(tokens.appIds(), appId, newToken), tokens.secrets());
1150 });
1151 return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author, commitSummary, transformer);
1152 }
1153
1154
1155
1156
1157 public Token findTokenByAppId(String appId) {
1158 requireNonNull(appId, "appId");
1159 return getTokens().get(appId);
1160 }
1161
1162
1163
1164
1165 public Token findTokenBySecret(String secret) {
1166 requireNonNull(secret, "secret");
1167 validateSecret(secret);
1168 return getTokens().findBySecret(secret);
1169 }
1170
1171
1172
1173
1174 private static void ensureProjectMember(ProjectMetadata project, User user) {
1175 requireNonNull(project, "project");
1176 requireNonNull(user, "user");
1177
1178 if (project.members().values().stream().noneMatch(member -> member.login().equals(user.id()))) {
1179 throw new MemberNotFoundException(user.id(), project.name());
1180 }
1181 }
1182
1183
1184
1185
1186 private static void ensureProjectToken(ProjectMetadata project, String appId) {
1187 requireNonNull(project, "project");
1188 requireNonNull(appId, "appId");
1189
1190 if (!project.tokens().containsKey(appId)) {
1191 throw new TokenNotFoundException(
1192 appId + " is not a token of the project '" + project.name() + '\'');
1193 }
1194 }
1195
1196 private static <T> ImmutableMap<String, T> addToMap(Map<String, T> map, String key, T value) {
1197 return ImmutableMap.<String, T>builderWithExpectedSize(map.size() + 1)
1198 .putAll(map)
1199 .put(key, value)
1200 .build();
1201 }
1202
1203 private static <T> Map<String, T> updateMap(Map<String, T> map, String key, T value) {
1204 final ImmutableMap.Builder<String, T> builder = ImmutableMap.builderWithExpectedSize(map.size());
1205 for (Entry<String, T> entry : map.entrySet()) {
1206 if (entry.getKey().equals(key)) {
1207 builder.put(key, value);
1208 } else {
1209 builder.put(entry);
1210 }
1211 }
1212 return builder.build();
1213 }
1214
1215 private static <T> ImmutableMap<String, T> removeFromMap(Map<String, T> map, String id) {
1216 return map.entrySet().stream()
1217 .filter(e -> !e.getKey().equals(id))
1218 .collect(toImmutableMap(Entry::getKey, Entry::getValue));
1219 }
1220
1221
1222
1223
1224 public CompletableFuture<Revision> updateRepositoryStatus(
1225 Author author, String projectName, String repoName, RepositoryStatus repositoryStatus) {
1226 requireNonNull(author, "author");
1227 requireNonNull(projectName, "projectName");
1228 requireNonNull(repoName, "repoName");
1229 requireNonNull(repositoryStatus, "repositoryStatus");
1230 final String newRepoName;
1231 if (Project.REPO_META.equals(repoName)) {
1232 newRepoName = Project.REPO_DOGMA;
1233 } else {
1234 newRepoName = repoName;
1235 }
1236
1237 final ProjectMetadataTransformer transformer;
1238 if (Project.REPO_DOGMA.equals(newRepoName)) {
1239
1240
1241 transformer = new ProjectMetadataTransformer((headRevision, projectMetadata) -> {
1242 final RepositoryMetadata repositoryMetadata = projectMetadata.repos().get(Project.REPO_DOGMA);
1243 if (repositoryMetadata != null) {
1244 throwIfRedundant(repositoryStatus, headRevision, repositoryMetadata, Project.REPO_DOGMA);
1245 }
1246 final RepositoryMetadata newRepositoryMetadata = RepositoryMetadata.ofDogma(repositoryStatus);
1247 final Builder<String, RepositoryMetadata> builder = ImmutableMap.builder();
1248 builder.put(Project.REPO_DOGMA, newRepositoryMetadata);
1249 projectMetadata.repos().forEach((name, metadata) -> {
1250 if (!Project.REPO_DOGMA.equals(name)) {
1251 builder.put(name, metadata);
1252 }
1253 });
1254 return new ProjectMetadata(projectMetadata.name(),
1255 builder.build(),
1256 projectMetadata.members(),
1257 projectMetadata.tokens(),
1258 projectMetadata.creation(),
1259 projectMetadata.removal());
1260 });
1261 } else {
1262 transformer = new RepositoryMetadataTransformer(
1263 newRepoName, (headRevision, repositoryMetadata) -> {
1264 throwIfRedundant(repositoryStatus, headRevision, repositoryMetadata, newRepoName);
1265
1266 return new RepositoryMetadata(repositoryMetadata.name(),
1267 repositoryMetadata.roles(),
1268 repositoryMetadata.creation(),
1269 repositoryMetadata.removal(),
1270 repositoryStatus);
1271 });
1272 }
1273
1274 final String commitSummary = "Update the status of '" + projectName + '/' + newRepoName +
1275 "'. status: " + repositoryStatus;
1276 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, transformer, true);
1277 }
1278
1279 private static void throwIfRedundant(RepositoryStatus repositoryStatus, Revision headRevision,
1280 RepositoryMetadata repositoryMetadata, String newRepoName) {
1281 if (repositoryMetadata.status() == repositoryStatus) {
1282 throw new RedundantChangeException(
1283 headRevision,
1284 "the status of '" + newRepoName + "' isn't changed. status: " + repositoryStatus);
1285 }
1286 }
1287 }