1use async_trait::async_trait;
16use base64::Engine;
17use chrono::Utc;
18use serde_json::{json, Map, Value};
19use std::sync::Arc;
20use tokio::sync::Mutex as AsyncMutex;
21
22use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
23use fakecloud_persistence::SnapshotStore;
24
25use crate::state::{FileEntry, Repo, SharedCodeCommitState};
26use crate::validate;
27
28use crate::codecommit_helpers::*;
29
30pub const CODECOMMIT_ACTIONS: &[&str] = &[
31 "AssociateApprovalRuleTemplateWithRepository",
32 "BatchAssociateApprovalRuleTemplateWithRepositories",
33 "BatchDescribeMergeConflicts",
34 "BatchDisassociateApprovalRuleTemplateFromRepositories",
35 "BatchGetCommits",
36 "BatchGetRepositories",
37 "CreateApprovalRuleTemplate",
38 "CreateBranch",
39 "CreateCommit",
40 "CreatePullRequest",
41 "CreatePullRequestApprovalRule",
42 "CreateRepository",
43 "CreateUnreferencedMergeCommit",
44 "DeleteApprovalRuleTemplate",
45 "DeleteBranch",
46 "DeleteCommentContent",
47 "DeleteFile",
48 "DeletePullRequestApprovalRule",
49 "DeleteRepository",
50 "DescribeMergeConflicts",
51 "DescribePullRequestEvents",
52 "DisassociateApprovalRuleTemplateFromRepository",
53 "EvaluatePullRequestApprovalRules",
54 "GetApprovalRuleTemplate",
55 "GetBlob",
56 "GetBranch",
57 "GetComment",
58 "GetCommentReactions",
59 "GetCommentsForComparedCommit",
60 "GetCommentsForPullRequest",
61 "GetCommit",
62 "GetDifferences",
63 "GetFile",
64 "GetFolder",
65 "GetMergeCommit",
66 "GetMergeConflicts",
67 "GetMergeOptions",
68 "GetPullRequest",
69 "GetPullRequestApprovalStates",
70 "GetPullRequestOverrideState",
71 "GetRepository",
72 "GetRepositoryTriggers",
73 "ListApprovalRuleTemplates",
74 "ListAssociatedApprovalRuleTemplatesForRepository",
75 "ListBranches",
76 "ListFileCommitHistory",
77 "ListPullRequests",
78 "ListRepositories",
79 "ListRepositoriesForApprovalRuleTemplate",
80 "ListTagsForResource",
81 "MergeBranchesByFastForward",
82 "MergeBranchesBySquash",
83 "MergeBranchesByThreeWay",
84 "MergePullRequestByFastForward",
85 "MergePullRequestBySquash",
86 "MergePullRequestByThreeWay",
87 "OverridePullRequestApprovalRules",
88 "PostCommentForComparedCommit",
89 "PostCommentForPullRequest",
90 "PostCommentReply",
91 "PutCommentReaction",
92 "PutFile",
93 "PutRepositoryTriggers",
94 "TagResource",
95 "TestRepositoryTriggers",
96 "UntagResource",
97 "UpdateApprovalRuleTemplateContent",
98 "UpdateApprovalRuleTemplateDescription",
99 "UpdateApprovalRuleTemplateName",
100 "UpdateComment",
101 "UpdateDefaultBranch",
102 "UpdatePullRequestApprovalRuleContent",
103 "UpdatePullRequestApprovalState",
104 "UpdatePullRequestDescription",
105 "UpdatePullRequestStatus",
106 "UpdatePullRequestTitle",
107 "UpdateRepositoryDescription",
108 "UpdateRepositoryEncryptionKey",
109 "UpdateRepositoryName",
110];
111
112pub struct CodeCommitService {
113 state: SharedCodeCommitState,
114 snapshot_store: Option<Arc<dyn SnapshotStore>>,
115 snapshot_lock: Arc<AsyncMutex<()>>,
116}
117
118impl CodeCommitService {
119 pub fn new(state: SharedCodeCommitState) -> Self {
120 Self {
121 state,
122 snapshot_store: None,
123 snapshot_lock: Arc::new(AsyncMutex::new(())),
124 }
125 }
126
127 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
128 self.snapshot_store = Some(store);
129 self
130 }
131
132 async fn save(&self) {
133 crate::persistence::save_snapshot(
134 &self.state,
135 self.snapshot_store.clone(),
136 &self.snapshot_lock,
137 )
138 .await;
139 }
140
141 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
143 let store = self.snapshot_store.clone()?;
144 let state = self.state.clone();
145 let lock = self.snapshot_lock.clone();
146 Some(Arc::new(move || {
147 let state = state.clone();
148 let store = store.clone();
149 let lock = lock.clone();
150 Box::pin(async move {
151 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
152 })
153 }))
154 }
155}
156
157fn is_mutator(action: &str) -> bool {
161 action.starts_with("Create")
162 || action.starts_with("Delete")
163 || action.starts_with("Update")
164 || action.starts_with("Put")
165 || action.starts_with("Merge")
166 || action.starts_with("Post")
167 || action.starts_with("Associate")
168 || action.starts_with("Disassociate")
169 || action.starts_with("BatchAssociate")
170 || action.starts_with("BatchDisassociate")
171 || matches!(
172 action,
173 "TagResource" | "UntagResource" | "OverridePullRequestApprovalRules"
174 )
175}
176
177#[async_trait]
178impl AwsService for CodeCommitService {
179 fn service_name(&self) -> &str {
180 "codecommit"
181 }
182
183 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
184 let action = req.action.clone();
185 let result = self.dispatch(&action, &req);
186 let should_save =
187 matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) && is_mutator(&action);
188 if should_save {
189 self.save().await;
190 }
191 result
192 }
193
194 fn supported_actions(&self) -> &[&str] {
195 CODECOMMIT_ACTIONS
196 }
197}
198
199impl CodeCommitService {
200 #[allow(clippy::too_many_lines)]
201 fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
202 match action {
203 "CreateRepository" => self.create_repository(req),
205 "GetRepository" => self.get_repository(req),
206 "DeleteRepository" => self.delete_repository(req),
207 "ListRepositories" => self.list_repositories(req),
208 "BatchGetRepositories" => self.batch_get_repositories(req),
209 "UpdateRepositoryDescription" => self.update_repository_description(req),
210 "UpdateRepositoryName" => self.update_repository_name(req),
211 "UpdateRepositoryEncryptionKey" => self.update_repository_encryption_key(req),
212 "CreateBranch" => self.create_branch(req),
214 "DeleteBranch" => self.delete_branch(req),
215 "GetBranch" => self.get_branch(req),
216 "ListBranches" => self.list_branches(req),
217 "UpdateDefaultBranch" => self.update_default_branch(req),
218 "PutFile" => self.put_file(req),
220 "DeleteFile" => self.delete_file(req),
221 "GetFile" => self.get_file(req),
222 "GetFolder" => self.get_folder(req),
223 "GetBlob" => self.get_blob(req),
224 "CreateCommit" => self.create_commit(req),
225 "GetCommit" => self.get_commit(req),
226 "BatchGetCommits" => self.batch_get_commits(req),
227 "GetDifferences" => self.get_differences(req),
228 "ListFileCommitHistory" => self.list_file_commit_history(req),
229 "MergeBranchesByFastForward" => self.merge_branches_fast_forward(req),
231 "MergeBranchesBySquash" => self.merge_branches_squash(req),
232 "MergeBranchesByThreeWay" => self.merge_branches_three_way(req),
233 "CreateUnreferencedMergeCommit" => self.create_unreferenced_merge_commit(req),
234 "GetMergeCommit" => self.get_merge_commit(req),
235 "GetMergeOptions" => self.get_merge_options(req),
236 "GetMergeConflicts" => self.get_merge_conflicts(req),
237 "DescribeMergeConflicts" => self.describe_merge_conflicts(req),
238 "BatchDescribeMergeConflicts" => self.batch_describe_merge_conflicts(req),
239 "CreatePullRequest" => self.create_pull_request(req),
241 "GetPullRequest" => self.get_pull_request(req),
242 "ListPullRequests" => self.list_pull_requests(req),
243 "UpdatePullRequestTitle" => self.update_pull_request_title(req),
244 "UpdatePullRequestDescription" => self.update_pull_request_description(req),
245 "UpdatePullRequestStatus" => self.update_pull_request_status(req),
246 "DescribePullRequestEvents" => self.describe_pull_request_events(req),
247 "MergePullRequestByFastForward" => self.merge_pull_request_fast_forward(req),
248 "MergePullRequestBySquash" => self.merge_pull_request_squash(req),
249 "MergePullRequestByThreeWay" => self.merge_pull_request_three_way(req),
250 "CreatePullRequestApprovalRule" => self.create_pull_request_approval_rule(req),
252 "DeletePullRequestApprovalRule" => self.delete_pull_request_approval_rule(req),
253 "UpdatePullRequestApprovalRuleContent" => {
254 self.update_pull_request_approval_rule_content(req)
255 }
256 "UpdatePullRequestApprovalState" => self.update_pull_request_approval_state(req),
257 "GetPullRequestApprovalStates" => self.get_pull_request_approval_states(req),
258 "EvaluatePullRequestApprovalRules" => self.evaluate_pull_request_approval_rules(req),
259 "OverridePullRequestApprovalRules" => self.override_pull_request_approval_rules(req),
260 "GetPullRequestOverrideState" => self.get_pull_request_override_state(req),
261 "CreateApprovalRuleTemplate" => self.create_approval_rule_template(req),
263 "GetApprovalRuleTemplate" => self.get_approval_rule_template(req),
264 "DeleteApprovalRuleTemplate" => self.delete_approval_rule_template(req),
265 "ListApprovalRuleTemplates" => self.list_approval_rule_templates(req),
266 "UpdateApprovalRuleTemplateContent" => self.update_approval_rule_template_content(req),
267 "UpdateApprovalRuleTemplateDescription" => {
268 self.update_approval_rule_template_description(req)
269 }
270 "UpdateApprovalRuleTemplateName" => self.update_approval_rule_template_name(req),
271 "AssociateApprovalRuleTemplateWithRepository" => {
272 self.associate_template_with_repository(req)
273 }
274 "DisassociateApprovalRuleTemplateFromRepository" => {
275 self.disassociate_template_from_repository(req)
276 }
277 "BatchAssociateApprovalRuleTemplateWithRepositories" => {
278 self.batch_associate_template(req)
279 }
280 "BatchDisassociateApprovalRuleTemplateFromRepositories" => {
281 self.batch_disassociate_template(req)
282 }
283 "ListAssociatedApprovalRuleTemplatesForRepository" => {
284 self.list_associated_templates_for_repository(req)
285 }
286 "ListRepositoriesForApprovalRuleTemplate" => self.list_repositories_for_template(req),
287 "PostCommentForComparedCommit" => self.post_comment_for_compared_commit(req),
289 "PostCommentForPullRequest" => self.post_comment_for_pull_request(req),
290 "PostCommentReply" => self.post_comment_reply(req),
291 "GetComment" => self.get_comment(req),
292 "GetCommentsForComparedCommit" => self.get_comments_for_compared_commit(req),
293 "GetCommentsForPullRequest" => self.get_comments_for_pull_request(req),
294 "UpdateComment" => self.update_comment(req),
295 "DeleteCommentContent" => self.delete_comment_content(req),
296 "PutCommentReaction" => self.put_comment_reaction(req),
297 "GetCommentReactions" => self.get_comment_reactions(req),
298 "PutRepositoryTriggers" => self.put_repository_triggers(req),
300 "GetRepositoryTriggers" => self.get_repository_triggers(req),
301 "TestRepositoryTriggers" => self.test_repository_triggers(req),
302 "TagResource" => self.tag_resource(req),
304 "UntagResource" => self.untag_resource(req),
305 "ListTagsForResource" => self.list_tags_for_resource(req),
306 _ => Err(AwsServiceError::action_not_implemented(
307 self.service_name(),
308 action,
309 )),
310 }
311 }
312}
313
314impl CodeCommitService {
318 fn account(&self, req: &AwsRequest) -> String {
319 req.account_id.clone()
320 }
321
322 fn create_repository(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
325 let b = body(req);
326 let name = require_repository_name(&b)?;
327 if let Some(d) = str_field(&b, "repositoryDescription") {
328 if d.chars().count() > 1000 {
329 return Err(err(
330 "InvalidRepositoryDescriptionException",
331 "The repository description is not valid. Length must not exceed 1000 characters.",
332 ));
333 }
334 }
335 if let Some(k) = str_field(&b, "kmsKeyId") {
336 if !validate::valid_kms_key_id(&k) {
337 return Err(err(
338 "EncryptionKeyInvalidIdException",
339 "The Amazon Web Services KMS key is not valid.",
340 ));
341 }
342 }
343 let account = self.account(req);
344 let arn = repo_arn(&req.region, &account, &name);
345 let now = Utc::now();
346 let mut guard = self.state.write();
347 let st = guard.get_or_create(&account);
348 if st.repositories.contains_key(&name) {
349 return Err(err(
350 "RepositoryNameExistsException",
351 format!("Repository named {name} already exists."),
352 ));
353 }
354 let kms = str_field(&b, "kmsKeyId").unwrap_or_else(|| {
355 format!("arn:aws:kms:{}:{}:key/{}", req.region, account, new_uuid())
356 });
357 let mut metadata = Map::new();
358 metadata.insert("accountId".into(), json!(account));
359 metadata.insert("repositoryId".into(), json!(new_uuid()));
360 metadata.insert("repositoryName".into(), json!(name));
361 if let Some(d) = str_field(&b, "repositoryDescription") {
362 metadata.insert("repositoryDescription".into(), json!(d));
363 }
364 metadata.insert("lastModifiedDate".into(), ts(now));
365 metadata.insert("creationDate".into(), ts(now));
366 metadata.insert(
367 "cloneUrlHttp".into(),
368 json!(format!(
369 "https://git-codecommit.{}.amazonaws.com/v1/repos/{}",
370 req.region, name
371 )),
372 );
373 metadata.insert(
374 "cloneUrlSsh".into(),
375 json!(format!(
376 "ssh://git-codecommit.{}.amazonaws.com/v1/repos/{}",
377 req.region, name
378 )),
379 );
380 metadata.insert("Arn".into(), json!(arn));
381 metadata.insert("kmsKeyId".into(), json!(kms));
382 let metadata = Value::Object(metadata);
383 let repo = Repo {
384 metadata: metadata.clone(),
385 ..Repo::default()
386 };
387 st.repositories.insert(name.clone(), repo);
388 st.repository_order.push(name.clone());
389 if let Some(tags) = b.get("tags").and_then(Value::as_object) {
390 let map = tags
391 .iter()
392 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
393 .collect();
394 st.tags.insert(arn, map);
395 }
396 ok(json!({ "repositoryMetadata": metadata }))
397 }
398
399 fn get_repository(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
400 let b = body(req);
401 let name = require_repository_name(&b)?;
402 let account = self.account(req);
403 let guard = self.state.read();
404 let st = guard.get(&account);
405 match st.and_then(|s| s.repositories.get(&name)) {
406 Some(repo) => ok(json!({ "repositoryMetadata": repo.metadata })),
407 None => Err(repo_not_found(&name)),
408 }
409 }
410
411 fn delete_repository(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
412 let b = body(req);
413 let name = require_repository_name(&b)?;
414 let account = self.account(req);
415 let arn = repo_arn(&req.region, &account, &name);
416 let mut guard = self.state.write();
417 let st = guard.get_or_create(&account);
418 let repository_id = st.repositories.remove(&name).and_then(|r| {
422 r.metadata
423 .get("repositoryId")
424 .and_then(Value::as_str)
425 .map(str::to_string)
426 });
427 st.repository_order.retain(|n| n != &name);
428 st.tags.remove(&arn);
429 match repository_id {
430 Some(id) => ok(json!({ "repositoryId": id })),
431 None => ok(json!({})),
432 }
433 }
434
435 fn list_repositories(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
436 let b = body(req);
437 check_enum(&b, "sortBy", validate::SORT_BY, "InvalidSortByException")?;
438 check_enum(&b, "order", validate::ORDER, "InvalidOrderException")?;
439 let account = self.account(req);
440 let guard = self.state.read();
441 let st = guard.get(&account);
442 let mut pairs = Vec::new();
443 if let Some(s) = st {
444 let mut names: Vec<String> = s.repository_order.clone();
445 if str_field(&b, "sortBy").as_deref() == Some("lastModifiedDate") {
446 names.sort_by_key(|n| {
447 s.repositories
448 .get(n)
449 .and_then(|r| r.metadata.get("lastModifiedDate"))
450 .and_then(Value::as_f64)
451 .map(|f| (f * 1000.0) as i64)
452 .unwrap_or(0)
453 });
454 } else {
455 names.sort();
456 }
457 if str_field(&b, "order").as_deref() == Some("descending") {
458 names.reverse();
459 }
460 for n in names {
461 if let Some(r) = s.repositories.get(&n) {
462 pairs.push(json!({
463 "repositoryName": n,
464 "repositoryId": r.metadata.get("repositoryId").cloned().unwrap_or(Value::Null),
465 }));
466 }
467 }
468 }
469 ok(json!({ "repositories": pairs }))
470 }
471
472 fn batch_get_repositories(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
473 let b = body(req);
474 let names = b
475 .get("repositoryNames")
476 .and_then(Value::as_array)
477 .ok_or_else(|| {
478 err(
479 "RepositoryNamesRequiredException",
480 "A repository names object is required.",
481 )
482 })?;
483 if names.len() > 100 {
484 return Err(err(
485 "MaximumRepositoryNamesExceededException",
486 "The maximum number of repository names for a request (100) has been exceeded.",
487 ));
488 }
489 let account = self.account(req);
490 let guard = self.state.read();
491 let st = guard.get(&account);
492 let mut found = Vec::new();
493 let mut not_found = Vec::new();
494 for n in names {
495 let Some(name) = n.as_str() else { continue };
496 match st.and_then(|s| s.repositories.get(name)) {
497 Some(r) => found.push(r.metadata.clone()),
498 None => not_found.push(json!(name)),
499 }
500 }
501 ok(json!({
502 "repositories": found,
503 "repositoriesNotFound": not_found,
504 "errors": [],
505 }))
506 }
507
508 fn update_repository_description(
509 &self,
510 req: &AwsRequest,
511 ) -> Result<AwsResponse, AwsServiceError> {
512 let b = body(req);
513 let name = require_repository_name(&b)?;
514 if let Some(d) = str_field(&b, "repositoryDescription") {
515 if d.chars().count() > 1000 {
516 return Err(err(
517 "InvalidRepositoryDescriptionException",
518 "The repository description is not valid.",
519 ));
520 }
521 }
522 let account = self.account(req);
523 let mut guard = self.state.write();
524 let st = guard.get_or_create(&account);
525 let repo = st
526 .repositories
527 .get_mut(&name)
528 .ok_or_else(|| repo_not_found(&name))?;
529 if let Some(obj) = repo.metadata.as_object_mut() {
530 match str_field(&b, "repositoryDescription") {
531 Some(d) => {
532 obj.insert("repositoryDescription".into(), json!(d));
533 }
534 None => {
535 obj.remove("repositoryDescription");
536 }
537 }
538 obj.insert("lastModifiedDate".into(), ts(Utc::now()));
539 }
540 ok(json!({}))
541 }
542
543 fn update_repository_name(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
544 let b = body(req);
545 let old = require(&b, "oldName", "RepositoryNameRequiredException")?;
546 let new = require(&b, "newName", "RepositoryNameRequiredException")?;
547 if !validate::valid_repository_name(&old) || !validate::valid_repository_name(&new) {
548 return Err(err(
549 "InvalidRepositoryNameException",
550 "The repository name is not valid.",
551 ));
552 }
553 let account = self.account(req);
554 let mut guard = self.state.write();
555 let st = guard.get_or_create(&account);
556 if !st.repositories.contains_key(&old) {
557 return Err(repo_not_found(&old));
558 }
559 if old != new && st.repositories.contains_key(&new) {
560 return Err(err(
561 "RepositoryNameExistsException",
562 format!("Repository named {new} already exists."),
563 ));
564 }
565 if old != new {
566 let mut repo = st.repositories.remove(&old).unwrap();
567 let arn = repo_arn(&req.region, &account, &new);
568 if let Some(obj) = repo.metadata.as_object_mut() {
569 obj.insert("repositoryName".into(), json!(new));
570 obj.insert("Arn".into(), json!(arn.clone()));
571 obj.insert(
572 "cloneUrlHttp".into(),
573 json!(format!(
574 "https://git-codecommit.{}.amazonaws.com/v1/repos/{}",
575 req.region, new
576 )),
577 );
578 obj.insert(
579 "cloneUrlSsh".into(),
580 json!(format!(
581 "ssh://git-codecommit.{}.amazonaws.com/v1/repos/{}",
582 req.region, new
583 )),
584 );
585 obj.insert("lastModifiedDate".into(), ts(Utc::now()));
586 }
587 let old_arn = repo_arn(&req.region, &account, &old);
588 if let Some(tags) = st.tags.remove(&old_arn) {
589 st.tags.insert(arn, tags);
590 }
591 st.repositories.insert(new.clone(), repo);
592 for n in &mut st.repository_order {
593 if n == &old {
594 *n = new.clone();
595 }
596 }
597 }
598 ok(json!({}))
599 }
600
601 fn update_repository_encryption_key(
602 &self,
603 req: &AwsRequest,
604 ) -> Result<AwsResponse, AwsServiceError> {
605 let b = body(req);
606 let name = require_repository_name(&b)?;
607 let kms = require(&b, "kmsKeyId", "EncryptionKeyRequiredException")?;
608 if !validate::valid_kms_key_id(&kms) {
609 return Err(err(
610 "EncryptionKeyInvalidIdException",
611 "The Amazon Web Services KMS key is not valid.",
612 ));
613 }
614 let account = self.account(req);
615 let mut guard = self.state.write();
616 let st = guard.get_or_create(&account);
617 let repo = st
618 .repositories
619 .get_mut(&name)
620 .ok_or_else(|| repo_not_found(&name))?;
621 let repository_id = repo
622 .metadata
623 .get("repositoryId")
624 .and_then(Value::as_str)
625 .unwrap_or_default()
626 .to_string();
627 let original = repo
628 .metadata
629 .get("kmsKeyId")
630 .and_then(Value::as_str)
631 .unwrap_or_default()
632 .to_string();
633 if let Some(obj) = repo.metadata.as_object_mut() {
634 obj.insert("kmsKeyId".into(), json!(kms));
635 obj.insert("lastModifiedDate".into(), ts(Utc::now()));
636 }
637 ok(json!({
638 "repositoryId": repository_id,
639 "kmsKeyId": kms,
640 "originalKmsKeyId": original,
641 }))
642 }
643
644 fn create_branch(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
647 let b = body(req);
648 let name = require_repository_name(&b)?;
649 let branch = require(&b, "branchName", "BranchNameRequiredException")?;
650 if !validate::valid_branch_name(&branch) {
651 return Err(err(
652 "InvalidBranchNameException",
653 "The branch name is not valid.",
654 ));
655 }
656 let commit = require(&b, "commitId", "CommitIdRequiredException")?;
657 if !is_object_id(&commit) {
658 return Err(err(
659 "InvalidCommitIdException",
660 "The commit ID is not valid.",
661 ));
662 }
663 let account = self.account(req);
664 let mut guard = self.state.write();
665 let st = guard.get_or_create(&account);
666 let repo = st
667 .repositories
668 .get_mut(&name)
669 .ok_or_else(|| repo_not_found(&name))?;
670 if repo.branches.contains_key(&branch) {
671 return Err(err(
672 "BranchNameExistsException",
673 format!("Branch name {branch} already exists."),
674 ));
675 }
676 if !repo.commits.contains_key(&commit) {
677 return Err(err(
678 "CommitDoesNotExistException",
679 "The specified commit does not exist.",
680 ));
681 }
682 repo.branches.insert(branch, commit);
683 ok(json!({}))
684 }
685
686 fn delete_branch(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
687 let b = body(req);
688 let name = require_repository_name(&b)?;
689 let branch = require(&b, "branchName", "BranchNameRequiredException")?;
690 if !validate::valid_branch_name(&branch) {
691 return Err(err(
692 "InvalidBranchNameException",
693 "The branch name is not valid.",
694 ));
695 }
696 let account = self.account(req);
697 let mut guard = self.state.write();
698 let st = guard.get_or_create(&account);
699 let repo = st
700 .repositories
701 .get_mut(&name)
702 .ok_or_else(|| repo_not_found(&name))?;
703 let default = repo
704 .metadata
705 .get("defaultBranch")
706 .and_then(Value::as_str)
707 .map(str::to_string);
708 if default.as_deref() == Some(branch.as_str()) {
709 return Err(err(
710 "DefaultBranchCannotBeDeletedException",
711 "The default branch for a repository cannot be deleted.",
712 ));
713 }
714 match repo.branches.remove(&branch) {
715 Some(commit) => ok(json!({
716 "deletedBranch": { "branchName": branch, "commitId": commit }
717 })),
718 None => ok(json!({})),
719 }
720 }
721
722 fn get_branch(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
723 let b = body(req);
724 let name = str_field(&b, "repositoryName").ok_or_else(|| {
725 err(
726 "RepositoryNameRequiredException",
727 "A repository name is required.",
728 )
729 })?;
730 if !validate::valid_repository_name(&name) {
731 return Err(err(
732 "InvalidRepositoryNameException",
733 "The repository name is not valid.",
734 ));
735 }
736 let branch = require(&b, "branchName", "BranchNameRequiredException")?;
737 if !validate::valid_branch_name(&branch) {
738 return Err(err(
739 "InvalidBranchNameException",
740 "The branch name is not valid.",
741 ));
742 }
743 let account = self.account(req);
744 let guard = self.state.read();
745 let st = guard.get(&account);
746 let repo = st
747 .and_then(|s| s.repositories.get(&name))
748 .ok_or_else(|| repo_not_found(&name))?;
749 match repo.branches.get(&branch) {
750 Some(commit) => ok(json!({
751 "branch": { "branchName": branch, "commitId": commit }
752 })),
753 None => Err(err(
754 "BranchDoesNotExistException",
755 "The specified branch does not exist.",
756 )),
757 }
758 }
759
760 fn list_branches(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
761 let b = body(req);
762 let name = require_repository_name(&b)?;
763 let account = self.account(req);
764 let guard = self.state.read();
765 let st = guard.get(&account);
766 let repo = st
767 .and_then(|s| s.repositories.get(&name))
768 .ok_or_else(|| repo_not_found(&name))?;
769 let branches: Vec<Value> = repo.branches.keys().map(|k| json!(k)).collect();
770 ok(json!({ "branches": branches }))
771 }
772
773 fn update_default_branch(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
774 let b = body(req);
775 let name = require_repository_name(&b)?;
776 let branch = require(&b, "defaultBranchName", "BranchNameRequiredException")?;
777 if !validate::valid_branch_name(&branch) {
778 return Err(err(
779 "InvalidBranchNameException",
780 "The branch name is not valid.",
781 ));
782 }
783 let account = self.account(req);
784 let mut guard = self.state.write();
785 let st = guard.get_or_create(&account);
786 let repo = st
787 .repositories
788 .get_mut(&name)
789 .ok_or_else(|| repo_not_found(&name))?;
790 if !repo.branches.contains_key(&branch) {
791 return Err(err(
792 "BranchDoesNotExistException",
793 "The specified branch does not exist.",
794 ));
795 }
796 if let Some(obj) = repo.metadata.as_object_mut() {
797 obj.insert("defaultBranch".into(), json!(branch));
798 obj.insert("lastModifiedDate".into(), ts(Utc::now()));
799 }
800 ok(json!({}))
801 }
802
803 fn put_file(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
806 let b = body(req);
807 let name = require_repository_name(&b)?;
808 let branch = require(&b, "branchName", "BranchNameRequiredException")?;
809 if !validate::valid_branch_name(&branch) {
810 return Err(err(
811 "InvalidBranchNameException",
812 "The branch name is not valid.",
813 ));
814 }
815 let path = require(&b, "filePath", "PathRequiredException")?;
816 let content_b64 = require(&b, "fileContent", "FileContentRequiredException")?;
817 check_enum(
818 &b,
819 "fileMode",
820 validate::FILE_MODE,
821 "InvalidFileModeException",
822 )?;
823 let mode = str_field(&b, "fileMode").unwrap_or_else(|| "NORMAL".to_string());
824 let content = base64::engine::general_purpose::STANDARD
825 .decode(content_b64.as_bytes())
826 .unwrap_or_else(|_| content_b64.clone().into_bytes());
827 let account = self.account(req);
828 let mut guard = self.state.write();
829 let st = guard.get_or_create(&account);
830 let repo = st
831 .repositories
832 .get_mut(&name)
833 .ok_or_else(|| repo_not_found(&name))?;
834
835 let parent_commit = str_field(&b, "parentCommitId");
836 let mut tree = if let Some(tip) = repo.branches.get(&branch).cloned() {
837 match &parent_commit {
839 None => {
840 return Err(err(
841 "ParentCommitIdRequiredException",
842 "A parent commit ID is required.",
843 ))
844 }
845 Some(p) if *p != tip => {
846 return Err(err(
847 "ParentCommitIdOutdatedException",
848 "The parent commit ID is not the tip of the branch.",
849 ))
850 }
851 _ => {}
852 }
853 repo.trees.get(&tip).cloned().unwrap_or_default()
854 } else {
855 std::collections::BTreeMap::new()
856 };
857
858 let bid = blob_id_for(&content);
859 if tree.get(&path).map(|e| &e.blob_id) == Some(&bid)
861 && tree.get(&path).map(|e| e.mode.as_str()) == Some(file_mode_to_git(&mode))
862 {
863 return Err(err(
864 "SameFileContentException",
865 "The file was not added or updated because the content of the file is exactly the same as the content of that file in the repository and branch that you specified.",
866 ));
867 }
868 repo.blobs.insert(bid.clone(), content_b64.clone());
869 tree.insert(
870 path.clone(),
871 FileEntry {
872 blob_id: bid.clone(),
873 mode: file_mode_to_git(&mode).to_string(),
874 },
875 );
876 let tree_id = tree_id_for(&tree);
877 let commit_id = fresh_object_id();
878 let parents: Vec<String> = parent_commit.into_iter().collect();
879 let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
880 repo.commits.insert(commit_id.clone(), commit);
881 repo.trees.insert(commit_id.clone(), tree);
882 repo.branches.insert(branch.clone(), commit_id.clone());
883 set_default_if_absent(repo);
884 refresh_source_revisions(st, &name, &branch);
886 ok(json!({ "commitId": commit_id, "blobId": bid, "treeId": tree_id }))
887 }
888
889 fn delete_file(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
890 let b = body(req);
891 let name = require_repository_name(&b)?;
892 let branch = require(&b, "branchName", "BranchNameRequiredException")?;
893 if !validate::valid_branch_name(&branch) {
894 return Err(err(
895 "InvalidBranchNameException",
896 "The branch name is not valid.",
897 ));
898 }
899 let path = require(&b, "filePath", "PathRequiredException")?;
900 let parent = require(&b, "parentCommitId", "ParentCommitIdRequiredException")?;
901 let account = self.account(req);
902 let mut guard = self.state.write();
903 let st = guard.get_or_create(&account);
904 let repo = st
905 .repositories
906 .get_mut(&name)
907 .ok_or_else(|| repo_not_found(&name))?;
908 let tip = repo.branches.get(&branch).cloned().ok_or_else(|| {
909 err(
910 "BranchDoesNotExistException",
911 "The specified branch does not exist.",
912 )
913 })?;
914 if parent != tip {
915 return Err(err(
916 "ParentCommitIdOutdatedException",
917 "The parent commit ID is not the tip of the branch.",
918 ));
919 }
920 let mut tree = repo.trees.get(&tip).cloned().unwrap_or_default();
921 if tree.remove(&path).is_none() {
922 return Err(err(
923 "FileDoesNotExistException",
924 "The specified file does not exist.",
925 ));
926 }
927 let tree_id = tree_id_for(&tree);
928 let commit_id = fresh_object_id();
929 let commit = build_commit(&commit_id, &tree_id, &[tip], &b, req);
930 repo.commits.insert(commit_id.clone(), commit);
931 repo.trees.insert(commit_id.clone(), tree);
932 repo.branches.insert(branch.clone(), commit_id.clone());
933 refresh_source_revisions(st, &name, &branch);
934 ok(json!({
935 "commitId": commit_id,
936 "blobId": blob_id_for(&[]),
937 "treeId": tree_id,
938 "filePath": path,
939 }))
940 }
941
942 fn create_commit(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
943 let b = body(req);
944 let name = require_repository_name(&b)?;
945 let branch = require(&b, "branchName", "BranchNameRequiredException")?;
946 if !validate::valid_branch_name(&branch) {
947 return Err(err(
948 "InvalidBranchNameException",
949 "The branch name is not valid.",
950 ));
951 }
952 let account = self.account(req);
953 let mut guard = self.state.write();
954 let st = guard.get_or_create(&account);
955 let repo = st
956 .repositories
957 .get_mut(&name)
958 .ok_or_else(|| repo_not_found(&name))?;
959
960 let parent_commit = str_field(&b, "parentCommitId");
961 let mut tree = if let Some(tip) = repo.branches.get(&branch).cloned() {
962 match &parent_commit {
963 None => {
964 return Err(err(
965 "ParentCommitIdRequiredException",
966 "A parent commit ID is required.",
967 ))
968 }
969 Some(p) if *p != tip => {
970 return Err(err(
971 "ParentCommitIdOutdatedException",
972 "The parent commit ID is not the tip of the branch.",
973 ))
974 }
975 _ => {}
976 }
977 repo.trees.get(&tip).cloned().unwrap_or_default()
978 } else {
979 std::collections::BTreeMap::new()
980 };
981
982 let mut files_added = Vec::new();
983 let mut files_updated = Vec::new();
984 let mut files_deleted = Vec::new();
985
986 if let Some(puts) = b.get("putFiles").and_then(Value::as_array) {
987 for pf in puts {
988 let Some(path) = pf.get("filePath").and_then(Value::as_str) else {
989 return Err(err("PathRequiredException", "A file path is required."));
990 };
991 let mode = pf
992 .get("fileMode")
993 .and_then(Value::as_str)
994 .unwrap_or("NORMAL");
995 let content_b64 = pf
996 .get("fileContent")
997 .and_then(Value::as_str)
998 .map(str::to_string)
999 .unwrap_or_default();
1000 let content = base64::engine::general_purpose::STANDARD
1001 .decode(content_b64.as_bytes())
1002 .unwrap_or_else(|_| content_b64.clone().into_bytes());
1003 let bid = blob_id_for(&content);
1004 repo.blobs.insert(bid.clone(), content_b64.clone());
1005 let existed = tree.contains_key(path);
1006 tree.insert(
1007 path.to_string(),
1008 FileEntry {
1009 blob_id: bid.clone(),
1010 mode: file_mode_to_git(mode).to_string(),
1011 },
1012 );
1013 let meta = json!({ "absolutePath": path, "blobId": bid, "fileMode": git_mode_to_file(file_mode_to_git(mode)) });
1014 if existed {
1015 files_updated.push(meta);
1016 } else {
1017 files_added.push(meta);
1018 }
1019 }
1020 }
1021 if let Some(dels) = b.get("deleteFiles").and_then(Value::as_array) {
1022 for df in dels {
1023 if let Some(path) = df.get("filePath").and_then(Value::as_str) {
1024 if tree.remove(path).is_some() {
1025 files_deleted.push(json!({ "absolutePath": path }));
1026 }
1027 }
1028 }
1029 }
1030 if let Some(modes) = b.get("setFileModes").and_then(Value::as_array) {
1031 for sm in modes {
1032 if let (Some(path), Some(mode)) = (
1033 sm.get("filePath").and_then(Value::as_str),
1034 sm.get("fileMode").and_then(Value::as_str),
1035 ) {
1036 if let Some(e) = tree.get_mut(path) {
1037 e.mode = file_mode_to_git(mode).to_string();
1038 }
1039 }
1040 }
1041 }
1042
1043 if files_added.is_empty()
1044 && files_updated.is_empty()
1045 && files_deleted.is_empty()
1046 && parent_commit.is_some()
1047 {
1048 return Err(err(
1049 "NoChangeException",
1050 "The commit cannot be created because no changes will be made to the repository as a result of this commit.",
1051 ));
1052 }
1053
1054 let tree_id = tree_id_for(&tree);
1055 let commit_id = fresh_object_id();
1056 let parents: Vec<String> = parent_commit.into_iter().collect();
1057 let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
1058 repo.commits.insert(commit_id.clone(), commit);
1059 repo.trees.insert(commit_id.clone(), tree);
1060 repo.branches.insert(branch.clone(), commit_id.clone());
1061 set_default_if_absent(repo);
1062 refresh_source_revisions(st, &name, &branch);
1063 ok(json!({
1064 "commitId": commit_id,
1065 "treeId": tree_id,
1066 "filesAdded": files_added,
1067 "filesUpdated": files_updated,
1068 "filesDeleted": files_deleted,
1069 }))
1070 }
1071
1072 fn get_commit(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1073 let b = body(req);
1074 let name = require_repository_name(&b)?;
1075 let commit = require(&b, "commitId", "CommitIdRequiredException")?;
1076 if !is_object_id(&commit) {
1077 return Err(err(
1078 "InvalidCommitIdException",
1079 "The commit ID is not valid.",
1080 ));
1081 }
1082 let account = self.account(req);
1083 let guard = self.state.read();
1084 let st = guard.get(&account);
1085 let repo = st
1086 .and_then(|s| s.repositories.get(&name))
1087 .ok_or_else(|| repo_not_found(&name))?;
1088 match repo.commits.get(&commit) {
1089 Some(c) => ok(json!({ "commit": c })),
1090 None => Err(err(
1091 "CommitIdDoesNotExistException",
1092 "The specified commit ID does not exist.",
1093 )),
1094 }
1095 }
1096
1097 fn batch_get_commits(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1098 let b = body(req);
1099 let name = require_repository_name(&b)?;
1100 let ids = b
1101 .get("commitIds")
1102 .and_then(Value::as_array)
1103 .ok_or_else(|| {
1104 err(
1105 "CommitIdsListRequiredException",
1106 "A list of commit IDs is required.",
1107 )
1108 })?;
1109 let account = self.account(req);
1110 let guard = self.state.read();
1111 let st = guard.get(&account);
1112 let repo = st
1113 .and_then(|s| s.repositories.get(&name))
1114 .ok_or_else(|| repo_not_found(&name))?;
1115 let mut commits = Vec::new();
1116 let mut errors = Vec::new();
1117 for id in ids {
1118 let Some(cid) = id.as_str() else { continue };
1119 match repo.commits.get(cid) {
1120 Some(c) => commits.push(c.clone()),
1121 None => errors.push(json!({
1122 "commitId": cid,
1123 "errorCode": "CommitDoesNotExist",
1124 "errorMessage": "The specified commit does not exist.",
1125 })),
1126 }
1127 }
1128 ok(json!({ "commits": commits, "errors": errors }))
1129 }
1130
1131 fn get_blob(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1132 let b = body(req);
1133 let name = require_repository_name(&b)?;
1134 let blob = require(&b, "blobId", "BlobIdRequiredException")?;
1135 if !is_object_id(&blob) {
1136 return Err(err("InvalidBlobIdException", "The blob ID is not valid."));
1137 }
1138 let account = self.account(req);
1139 let guard = self.state.read();
1140 let st = guard.get(&account);
1141 let repo = st
1142 .and_then(|s| s.repositories.get(&name))
1143 .ok_or_else(|| repo_not_found(&name))?;
1144 match repo.blobs.get(&blob) {
1145 Some(content_b64) => ok(json!({ "content": content_b64 })),
1146 None => Err(err(
1147 "BlobIdDoesNotExistException",
1148 "The specified blob does not exist.",
1149 )),
1150 }
1151 }
1152
1153 fn get_file(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1154 let b = body(req);
1155 let name = require_repository_name(&b)?;
1156 let path = require(&b, "filePath", "PathRequiredException")?;
1157 let account = self.account(req);
1158 let guard = self.state.read();
1159 let st = guard.get(&account);
1160 let repo = st
1161 .and_then(|s| s.repositories.get(&name))
1162 .ok_or_else(|| repo_not_found(&name))?;
1163 let commit_id = resolve_commit(repo, &b, "commitSpecifier")?;
1164 let tree = repo.trees.get(&commit_id).cloned().unwrap_or_default();
1165 let entry = tree.get(&path).ok_or_else(|| {
1166 err(
1167 "FileDoesNotExistException",
1168 "The specified file does not exist.",
1169 )
1170 })?;
1171 let content_b64 = repo.blobs.get(&entry.blob_id).cloned().unwrap_or_default();
1172 let size = base64::engine::general_purpose::STANDARD
1173 .decode(content_b64.as_bytes())
1174 .map(|d| d.len())
1175 .unwrap_or(content_b64.len());
1176 ok(json!({
1177 "commitId": commit_id,
1178 "blobId": entry.blob_id,
1179 "filePath": path,
1180 "fileMode": git_mode_to_file(&entry.mode),
1181 "fileSize": size,
1182 "fileContent": content_b64,
1183 }))
1184 }
1185
1186 fn get_folder(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1187 let b = body(req);
1188 let name = require_repository_name(&b)?;
1189 let folder = require(&b, "folderPath", "PathRequiredException")?;
1190 let account = self.account(req);
1191 let guard = self.state.read();
1192 let st = guard.get(&account);
1193 let repo = st
1194 .and_then(|s| s.repositories.get(&name))
1195 .ok_or_else(|| repo_not_found(&name))?;
1196 let commit_id = resolve_commit(repo, &b, "commitSpecifier")?;
1197 let tree = repo.trees.get(&commit_id).cloned().unwrap_or_default();
1198 let prefix = if folder == "/" || folder.is_empty() {
1199 String::new()
1200 } else {
1201 format!("{}/", folder.trim_end_matches('/'))
1202 };
1203 let tree_id = repo
1204 .commits
1205 .get(&commit_id)
1206 .and_then(|c| c.get("treeId"))
1207 .and_then(Value::as_str)
1208 .unwrap_or_default()
1209 .to_string();
1210 let mut files = Vec::new();
1211 let mut subfolders = std::collections::BTreeSet::new();
1212 let mut symlinks = Vec::new();
1213 for (path, entry) in &tree {
1214 if !path.starts_with(&prefix) {
1215 continue;
1216 }
1217 let rel = &path[prefix.len()..];
1218 if let Some(idx) = rel.find('/') {
1219 subfolders.insert(rel[..idx].to_string());
1220 } else {
1221 let file = json!({
1222 "blobId": entry.blob_id,
1223 "absolutePath": path,
1224 "relativePath": rel,
1225 "fileMode": git_mode_to_file(&entry.mode),
1226 });
1227 if entry.mode == "120000" {
1228 symlinks.push(file);
1229 } else {
1230 files.push(file);
1231 }
1232 }
1233 }
1234 let sub: Vec<Value> = subfolders
1235 .into_iter()
1236 .map(|s| {
1237 json!({
1238 "treeId": tree_id,
1239 "absolutePath": format!("{prefix}{s}"),
1240 "relativePath": s,
1241 })
1242 })
1243 .collect();
1244 ok(json!({
1245 "commitId": commit_id,
1246 "folderPath": folder,
1247 "treeId": tree_id,
1248 "subFolders": sub,
1249 "files": files,
1250 "symbolicLinks": symlinks,
1251 "subModules": [],
1252 }))
1253 }
1254
1255 fn get_differences(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1256 let b = body(req);
1257 let name = require_repository_name(&b)?;
1258 let account = self.account(req);
1259 let guard = self.state.read();
1260 let st = guard.get(&account);
1261 let repo = st
1262 .and_then(|s| s.repositories.get(&name))
1263 .ok_or_else(|| repo_not_found(&name))?;
1264 let after = resolve_commit(repo, &b, "afterCommitSpecifier")?;
1265 let after_tree = repo.trees.get(&after).cloned().unwrap_or_default();
1266 let before_tree = if b.get("beforeCommitSpecifier").is_some() {
1267 let before = resolve_commit(repo, &b, "beforeCommitSpecifier")?;
1268 repo.trees.get(&before).cloned().unwrap_or_default()
1269 } else {
1270 std::collections::BTreeMap::new()
1271 };
1272 let mut diffs = Vec::new();
1273 for (path, entry) in &after_tree {
1274 match before_tree.get(path) {
1275 None => diffs.push(json!({
1276 "afterBlob": { "blobId": entry.blob_id, "path": path, "mode": entry.mode },
1277 "changeType": "A",
1278 })),
1279 Some(bentry) if bentry.blob_id != entry.blob_id => diffs.push(json!({
1280 "beforeBlob": { "blobId": bentry.blob_id, "path": path, "mode": bentry.mode },
1281 "afterBlob": { "blobId": entry.blob_id, "path": path, "mode": entry.mode },
1282 "changeType": "M",
1283 })),
1284 _ => {}
1285 }
1286 }
1287 for (path, bentry) in &before_tree {
1288 if !after_tree.contains_key(path) {
1289 diffs.push(json!({
1290 "beforeBlob": { "blobId": bentry.blob_id, "path": path, "mode": bentry.mode },
1291 "changeType": "D",
1292 }));
1293 }
1294 }
1295 ok(json!({ "differences": diffs }))
1296 }
1297
1298 fn list_file_commit_history(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1299 let b = body(req);
1300 let name = require_repository_name(&b)?;
1301 let _path = require(&b, "filePath", "PathRequiredException")?;
1302 let account = self.account(req);
1303 let guard = self.state.read();
1304 let st = guard.get(&account);
1305 let repo = st
1306 .and_then(|s| s.repositories.get(&name))
1307 .ok_or_else(|| repo_not_found(&name))?;
1308 let commit_id = resolve_commit(repo, &b, "commitSpecifier")?;
1309 let mut dag = Vec::new();
1311 let mut cur = Some(commit_id);
1312 while let Some(cid) = cur {
1313 let Some(c) = repo.commits.get(&cid) else {
1314 break;
1315 };
1316 let parents = c
1317 .get("parents")
1318 .and_then(Value::as_array)
1319 .cloned()
1320 .unwrap_or_default();
1321 dag.push(json!({
1322 "commit": cid,
1323 "parents": parents.iter().filter_map(|p| p.as_str()).collect::<Vec<_>>(),
1324 }));
1325 cur = parents.first().and_then(Value::as_str).map(str::to_string);
1326 }
1327 ok(json!({ "revisionDag": dag }))
1328 }
1329
1330 fn merge_branches_fast_forward(
1333 &self,
1334 req: &AwsRequest,
1335 ) -> Result<AwsResponse, AwsServiceError> {
1336 let b = body(req);
1337 let name = require_repository_name(&b)?;
1338 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1339 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1340 let account = self.account(req);
1341 let mut guard = self.state.write();
1342 let st = guard.get_or_create(&account);
1343 let repo = st
1344 .repositories
1345 .get_mut(&name)
1346 .ok_or_else(|| repo_not_found(&name))?;
1347 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1348 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1349 if !is_ancestor(repo, &dest, &source) {
1350 return Err(err(
1351 "ManualMergeRequiredException",
1352 "The fast-forward merge cannot be performed because the destination is not an ancestor of the source.",
1353 ));
1354 }
1355 let tree_id = repo
1356 .commits
1357 .get(&source)
1358 .and_then(|c| c.get("treeId"))
1359 .and_then(Value::as_str)
1360 .unwrap_or_default()
1361 .to_string();
1362 if let Some(target) = str_field(&b, "targetBranch") {
1363 repo.branches.insert(target.clone(), source.clone());
1364 refresh_source_revisions(st, &name, &target);
1365 }
1366 ok(json!({ "commitId": source, "treeId": tree_id }))
1367 }
1368
1369 fn merge_branches_squash(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1370 self.merge_branches_create(req, "SQUASH_MERGE")
1371 }
1372
1373 fn merge_branches_three_way(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1374 self.merge_branches_create(req, "THREE_WAY_MERGE")
1375 }
1376
1377 fn merge_branches_create(
1378 &self,
1379 req: &AwsRequest,
1380 merge_option: &str,
1381 ) -> Result<AwsResponse, AwsServiceError> {
1382 let b = body(req);
1383 let name = require_repository_name(&b)?;
1384 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1385 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1386 validate_merge_conflict_enums(&b)?;
1387 let account = self.account(req);
1388 let mut guard = self.state.write();
1389 let st = guard.get_or_create(&account);
1390 let repo = st
1391 .repositories
1392 .get_mut(&name)
1393 .ok_or_else(|| repo_not_found(&name))?;
1394 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1395 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1396 let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
1397 let merged = merge_trees(repo, &source, &dest, &strategy).ok_or_else(|| {
1398 err(
1399 "ManualMergeRequiredException",
1400 "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
1401 )
1402 })?;
1403 let tree_id = tree_id_for(&merged);
1404 let commit_id = fresh_object_id();
1405 let parents = merge_parents(merge_option, &dest, &source);
1406 let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
1407 repo.commits.insert(commit_id.clone(), commit);
1408 repo.trees.insert(commit_id.clone(), merged);
1409 if let Some(target) = str_field(&b, "targetBranch") {
1410 repo.branches.insert(target.clone(), commit_id.clone());
1411 refresh_source_revisions(st, &name, &target);
1412 }
1413 ok(json!({ "commitId": commit_id, "treeId": tree_id }))
1414 }
1415
1416 fn create_unreferenced_merge_commit(
1417 &self,
1418 req: &AwsRequest,
1419 ) -> Result<AwsResponse, AwsServiceError> {
1420 let b = body(req);
1421 let name = require_repository_name(&b)?;
1422 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1423 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1424 let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1425 if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1426 return Err(err(
1427 "InvalidMergeOptionException",
1428 "The merge option is not valid.",
1429 ));
1430 }
1431 validate_merge_conflict_enums(&b)?;
1432 let account = self.account(req);
1433 let mut guard = self.state.write();
1434 let st = guard.get_or_create(&account);
1435 let repo = st
1436 .repositories
1437 .get_mut(&name)
1438 .ok_or_else(|| repo_not_found(&name))?;
1439 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1440 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1441 let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
1442 let merged = merge_trees(repo, &source, &dest, &strategy).ok_or_else(|| {
1443 err(
1444 "ManualMergeRequiredException",
1445 "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
1446 )
1447 })?;
1448 let tree_id = tree_id_for(&merged);
1449 let commit_id = fresh_object_id();
1450 let parents = merge_parents(&merge_option, &dest, &source);
1451 let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
1452 repo.commits.insert(commit_id.clone(), commit);
1453 repo.trees.insert(commit_id.clone(), merged);
1454 ok(json!({ "commitId": commit_id, "treeId": tree_id }))
1455 }
1456
1457 fn get_merge_commit(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1458 let b = body(req);
1459 let name = require_repository_name(&b)?;
1460 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1461 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1462 let account = self.account(req);
1463 let guard = self.state.read();
1464 let st = guard.get(&account);
1465 let repo = st
1466 .and_then(|s| s.repositories.get(&name))
1467 .ok_or_else(|| repo_not_found(&name))?;
1468 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1469 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1470 let base = merge_base(repo, &source, &dest);
1471 ok(json!({
1472 "sourceCommitId": source,
1473 "destinationCommitId": dest,
1474 "baseCommitId": base,
1475 }))
1476 }
1477
1478 fn get_merge_options(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1479 let b = body(req);
1480 let name = require_repository_name(&b)?;
1481 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1482 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1483 let account = self.account(req);
1484 let guard = self.state.read();
1485 let st = guard.get(&account);
1486 let repo = st
1487 .and_then(|s| s.repositories.get(&name))
1488 .ok_or_else(|| repo_not_found(&name))?;
1489 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1490 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1491 let base = merge_base(repo, &source, &dest);
1492 let mut options = vec![json!("SQUASH_MERGE"), json!("THREE_WAY_MERGE")];
1493 if is_ancestor(repo, &dest, &source) {
1494 options.insert(0, json!("FAST_FORWARD_MERGE"));
1495 }
1496 ok(json!({
1497 "mergeOptions": options,
1498 "sourceCommitId": source,
1499 "destinationCommitId": dest,
1500 "baseCommitId": base,
1501 }))
1502 }
1503
1504 fn get_merge_conflicts(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1505 let b = body(req);
1506 let name = require_repository_name(&b)?;
1507 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1508 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1509 let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1510 if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1511 return Err(err(
1512 "InvalidMergeOptionException",
1513 "The merge option is not valid.",
1514 ));
1515 }
1516 let account = self.account(req);
1517 let guard = self.state.read();
1518 let st = guard.get(&account);
1519 let repo = st
1520 .and_then(|s| s.repositories.get(&name))
1521 .ok_or_else(|| repo_not_found(&name))?;
1522 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1523 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1524 let empty = std::collections::BTreeMap::new();
1525 let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1526 let conflicts = conflicting_paths_in(base_tree, src_tree, dst_tree);
1527 let mergeable = conflicts.is_empty();
1528 let metadata: Vec<Value> = conflicts
1529 .iter()
1530 .map(|p| conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, p).0)
1531 .collect();
1532 ok(json!({
1533 "mergeable": mergeable,
1534 "destinationCommitId": dest,
1535 "sourceCommitId": source,
1536 "baseCommitId": base,
1537 "conflictMetadataList": metadata,
1538 }))
1539 }
1540
1541 fn describe_merge_conflicts(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1542 let b = body(req);
1543 let name = require_repository_name(&b)?;
1544 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1545 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1546 let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1547 if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1548 return Err(err(
1549 "InvalidMergeOptionException",
1550 "The merge option is not valid.",
1551 ));
1552 }
1553 let path = require(&b, "filePath", "PathRequiredException")?;
1554 let account = self.account(req);
1555 let guard = self.state.read();
1556 let st = guard.get(&account);
1557 let repo = st
1558 .and_then(|s| s.repositories.get(&name))
1559 .ok_or_else(|| repo_not_found(&name))?;
1560 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1561 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1562 let empty = std::collections::BTreeMap::new();
1563 let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1564 if base_tree.get(&path).is_none()
1565 && src_tree.get(&path).is_none()
1566 && dst_tree.get(&path).is_none()
1567 {
1568 return Err(err(
1569 "FileDoesNotExistException",
1570 "The specified file does not exist.",
1571 ));
1572 }
1573 let (metadata, hunks) =
1574 conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, &path);
1575 ok(json!({
1576 "conflictMetadata": metadata,
1577 "mergeHunks": hunks,
1578 "destinationCommitId": dest,
1579 "sourceCommitId": source,
1580 "baseCommitId": base,
1581 }))
1582 }
1583
1584 fn batch_describe_merge_conflicts(
1585 &self,
1586 req: &AwsRequest,
1587 ) -> Result<AwsResponse, AwsServiceError> {
1588 let b = body(req);
1589 let name = require_repository_name(&b)?;
1590 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1591 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1592 let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1593 if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1594 return Err(err(
1595 "InvalidMergeOptionException",
1596 "The merge option is not valid.",
1597 ));
1598 }
1599 let account = self.account(req);
1600 let guard = self.state.read();
1601 let st = guard.get(&account);
1602 let repo = st
1603 .and_then(|s| s.repositories.get(&name))
1604 .ok_or_else(|| repo_not_found(&name))?;
1605 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1606 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1607 let max_files = b
1608 .get("maxConflictFiles")
1609 .and_then(Value::as_i64)
1610 .filter(|n| *n >= 0)
1611 .map_or(usize::MAX, |n| n as usize);
1612 let max_hunks = b
1613 .get("maxMergeHunks")
1614 .and_then(Value::as_i64)
1615 .filter(|n| *n >= 0)
1616 .map_or(usize::MAX, |n| n as usize);
1617 let empty = std::collections::BTreeMap::new();
1618 let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1619 let mut conflicts = Vec::new();
1620 for path in conflicting_paths_in(base_tree, src_tree, dst_tree) {
1621 if conflicts.len() >= max_files {
1622 break;
1623 }
1624 let (metadata, mut hunks) =
1625 conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, &path);
1626 hunks.truncate(max_hunks);
1627 conflicts.push(json!({
1628 "conflictMetadata": metadata,
1629 "mergeHunks": hunks,
1630 }));
1631 }
1632 ok(json!({
1633 "conflicts": conflicts,
1634 "errors": [],
1635 "destinationCommitId": dest,
1636 "sourceCommitId": source,
1637 "baseCommitId": base,
1638 }))
1639 }
1640
1641 fn create_pull_request(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1644 let b = body(req);
1645 let title = match str_field(&b, "title") {
1646 Some(t) => t,
1647 None => {
1648 return Err(err(
1649 "TitleRequiredException",
1650 "A pull request title is required.",
1651 ))
1652 }
1653 };
1654 if title.chars().count() > 150 {
1655 return Err(err("InvalidTitleException", "The title is not valid."));
1656 }
1657 let targets = b
1658 .get("targets")
1659 .and_then(Value::as_array)
1660 .filter(|t| !t.is_empty())
1661 .ok_or_else(|| {
1662 err(
1663 "TargetsRequiredException",
1664 "Pull request targets are required.",
1665 )
1666 })?
1667 .clone();
1668 if let Some(d) = str_field(&b, "description") {
1669 if d.chars().count() > 10240 {
1670 return Err(err(
1671 "InvalidDescriptionException",
1672 "The description is not valid.",
1673 ));
1674 }
1675 }
1676 let account = self.account(req);
1677 let author = caller_arn(req);
1678 let mut guard = self.state.write();
1679 let st = guard.get_or_create(&account);
1680 let now = Utc::now();
1681 let mut pr_targets = Vec::new();
1682 let mut approval_rules = Vec::new();
1683 for t in &targets {
1684 let repo_name = t
1685 .get("repositoryName")
1686 .and_then(Value::as_str)
1687 .ok_or_else(|| {
1688 err(
1689 "RepositoryNameRequiredException",
1690 "A repository name is required.",
1691 )
1692 })?;
1693 let repo = st
1694 .repositories
1695 .get(repo_name)
1696 .ok_or_else(|| repo_not_found(repo_name))?;
1697 let source_ref = t
1698 .get("sourceReference")
1699 .and_then(Value::as_str)
1700 .ok_or_else(|| {
1701 err(
1702 "ReferenceNameRequiredException",
1703 "A source reference is required.",
1704 )
1705 })?;
1706 let dest_ref = t
1707 .get("destinationReference")
1708 .and_then(Value::as_str)
1709 .map(str::to_string)
1710 .or_else(|| {
1711 repo.metadata
1712 .get("defaultBranch")
1713 .and_then(Value::as_str)
1714 .map(str::to_string)
1715 })
1716 .unwrap_or_else(|| "main".to_string());
1717 if source_ref == dest_ref {
1718 return Err(err(
1719 "SourceAndDestinationAreSameException",
1720 "The source reference and destination reference are the same. You must specify different references for the source and destination.",
1721 ));
1722 }
1723 let source_commit = repo.branches.get(source_ref).cloned().ok_or_else(|| {
1724 err(
1725 "ReferenceDoesNotExistException",
1726 "The specified reference does not exist.",
1727 )
1728 })?;
1729 let dest_commit = repo.branches.get(&dest_ref).cloned().ok_or_else(|| {
1731 err(
1732 "ReferenceDoesNotExistException",
1733 "The specified reference does not exist.",
1734 )
1735 })?;
1736 let base = merge_base(repo, &source_commit, &dest_commit);
1737 pr_targets.push(json!({
1738 "repositoryName": repo_name,
1739 "sourceReference": source_ref,
1740 "destinationReference": dest_ref,
1741 "sourceCommit": source_commit,
1742 "destinationCommit": dest_commit,
1743 "mergeBase": base,
1744 "mergeMetadata": { "isMerged": false },
1745 }));
1746 for tmpl_name in &repo.associated_templates {
1749 if approval_rules.iter().any(|r: &Value| {
1750 r.get("approvalRuleName").and_then(Value::as_str) == Some(tmpl_name.as_str())
1751 }) {
1752 continue;
1753 }
1754 if let Some(tmpl) = st.templates.get(tmpl_name) {
1755 let content = tmpl
1756 .get("approvalRuleTemplateContent")
1757 .and_then(Value::as_str)
1758 .unwrap_or_default()
1759 .to_string();
1760 approval_rules.push(json!({
1761 "approvalRuleId": new_uuid(),
1762 "approvalRuleName": tmpl_name,
1763 "approvalRuleContent": content,
1764 "ruleContentSha256": sha256_hex(content.as_bytes()),
1765 "lastModifiedDate": ts(now),
1766 "creationDate": ts(now),
1767 "lastModifiedUser": author,
1768 "originApprovalRuleTemplate": {
1769 "approvalRuleTemplateId": tmpl
1770 .get("approvalRuleTemplateId")
1771 .cloned()
1772 .unwrap_or(Value::Null),
1773 "approvalRuleTemplateName": tmpl_name,
1774 },
1775 }));
1776 }
1777 }
1778 }
1779 st.pull_request_counter += 1;
1780 let pr_id = st.pull_request_counter.to_string();
1781 let revision_id = fresh_object_id();
1782 let pr = json!({
1783 "pullRequestId": pr_id,
1784 "title": title,
1785 "description": str_field(&b, "description").unwrap_or_default(),
1786 "lastActivityDate": ts(now),
1787 "creationDate": ts(now),
1788 "pullRequestStatus": "OPEN",
1789 "authorArn": author,
1790 "pullRequestTargets": pr_targets,
1791 "clientRequestToken": str_field(&b, "clientRequestToken").unwrap_or_default(),
1792 "revisionId": revision_id,
1793 "approvalRules": approval_rules,
1794 });
1795 st.pull_requests.insert(pr_id.clone(), pr.clone());
1796 st.pull_request_order.push(pr_id.clone());
1797 st.pull_request_events.insert(
1798 pr_id.clone(),
1799 vec![pr_event(
1800 &pr_id,
1801 "PULL_REQUEST_CREATED",
1802 &author,
1803 now,
1804 json!({}),
1805 )],
1806 );
1807 ok(json!({ "pullRequest": pr }))
1808 }
1809
1810 fn get_pull_request(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1811 let b = body(req);
1812 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1813 let account = self.account(req);
1814 let guard = self.state.read();
1815 let st = guard.get(&account);
1816 match st.and_then(|s| s.pull_requests.get(&id).map(|pr| refresh_pr_targets(s, pr))) {
1819 Some(pr) => ok(json!({ "pullRequest": pr })),
1820 None => Err(pr_not_found()),
1821 }
1822 }
1823
1824 fn list_pull_requests(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1825 let b = body(req);
1826 let name = require_repository_name(&b)?;
1827 check_enum(
1828 &b,
1829 "pullRequestStatus",
1830 validate::PULL_REQUEST_STATUS,
1831 "InvalidPullRequestStatusException",
1832 )?;
1833 let author_filter = str_field(&b, "authorArn");
1834 let status_filter = str_field(&b, "pullRequestStatus");
1835 let account = self.account(req);
1836 let guard = self.state.read();
1837 let st = guard.get(&account);
1838 let s = st.ok_or_else(|| repo_not_found(&name))?;
1839 if !s.repositories.contains_key(&name) {
1840 return Err(repo_not_found(&name));
1841 }
1842 let mut ids = Vec::new();
1843 for pid in &s.pull_request_order {
1844 let Some(pr) = s.pull_requests.get(pid) else {
1845 continue;
1846 };
1847 let in_repo = pr
1848 .get("pullRequestTargets")
1849 .and_then(Value::as_array)
1850 .map(|ts| {
1851 ts.iter().any(|t| {
1852 t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str())
1853 })
1854 })
1855 .unwrap_or(false);
1856 if !in_repo {
1857 continue;
1858 }
1859 if let Some(a) = &author_filter {
1860 if pr.get("authorArn").and_then(Value::as_str) != Some(a.as_str()) {
1861 continue;
1862 }
1863 }
1864 if let Some(s2) = &status_filter {
1865 if pr.get("pullRequestStatus").and_then(Value::as_str) != Some(s2.as_str()) {
1866 continue;
1867 }
1868 }
1869 ids.push(json!(pid));
1870 }
1871 ok(json!({ "pullRequestIds": ids }))
1872 }
1873
1874 fn update_pull_request_title(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1875 let b = body(req);
1876 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1877 let title = match str_field(&b, "title") {
1878 Some(t) => t,
1879 None => {
1880 return Err(err(
1881 "TitleRequiredException",
1882 "A pull request title is required.",
1883 ))
1884 }
1885 };
1886 if title.chars().count() > 150 {
1887 return Err(err("InvalidTitleException", "The title is not valid."));
1888 }
1889 self.mutate_pr(req, &id, |pr| {
1890 pr["title"] = json!(title);
1891 pr["lastActivityDate"] = ts(Utc::now());
1892 Ok(())
1893 })
1894 }
1895
1896 fn update_pull_request_description(
1897 &self,
1898 req: &AwsRequest,
1899 ) -> Result<AwsResponse, AwsServiceError> {
1900 let b = body(req);
1901 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1902 let desc = str_field(&b, "description").unwrap_or_default();
1903 if desc.chars().count() > 10240 {
1904 return Err(err(
1905 "InvalidDescriptionException",
1906 "The description is not valid.",
1907 ));
1908 }
1909 self.mutate_pr(req, &id, |pr| {
1910 pr["description"] = json!(desc);
1911 pr["lastActivityDate"] = ts(Utc::now());
1912 Ok(())
1913 })
1914 }
1915
1916 fn update_pull_request_status(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1917 let b = body(req);
1918 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1919 let status = require(
1920 &b,
1921 "pullRequestStatus",
1922 "PullRequestStatusRequiredException",
1923 )?;
1924 if !validate::is_enum(validate::PULL_REQUEST_STATUS, &status) {
1925 return Err(err(
1926 "InvalidPullRequestStatusException",
1927 "The pull request status is not valid.",
1928 ));
1929 }
1930 self.mutate_pr(req, &id, |pr| {
1931 let current = pr.get("pullRequestStatus").and_then(Value::as_str).unwrap_or("OPEN");
1932 if !(current == "OPEN" && status == "CLOSED") {
1936 return Err(err(
1937 "InvalidPullRequestStatusUpdateException",
1938 "The pull request status update is not valid. The only valid update is from OPEN to CLOSED.",
1939 ));
1940 }
1941 pr["pullRequestStatus"] = json!(status);
1942 pr["lastActivityDate"] = ts(Utc::now());
1943 Ok(())
1944 })
1945 }
1946
1947 fn describe_pull_request_events(
1948 &self,
1949 req: &AwsRequest,
1950 ) -> Result<AwsResponse, AwsServiceError> {
1951 let b = body(req);
1952 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1953 check_enum(
1954 &b,
1955 "pullRequestEventType",
1956 validate::PULL_REQUEST_EVENT_TYPE,
1957 "InvalidPullRequestEventTypeException",
1958 )?;
1959 let account = self.account(req);
1960 let guard = self.state.read();
1961 let st = guard.get(&account);
1962 let s = st.ok_or_else(pr_not_found)?;
1963 if !s.pull_requests.contains_key(&id) {
1964 return Err(pr_not_found());
1965 }
1966 let events = s.pull_request_events.get(&id).cloned().unwrap_or_default();
1967 ok(json!({ "pullRequestEvents": events }))
1968 }
1969
1970 fn merge_pull_request_fast_forward(
1971 &self,
1972 req: &AwsRequest,
1973 ) -> Result<AwsResponse, AwsServiceError> {
1974 self.merge_pull_request(req, "FAST_FORWARD_MERGE")
1975 }
1976
1977 fn merge_pull_request_squash(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1978 self.merge_pull_request(req, "SQUASH_MERGE")
1979 }
1980
1981 fn merge_pull_request_three_way(
1982 &self,
1983 req: &AwsRequest,
1984 ) -> Result<AwsResponse, AwsServiceError> {
1985 self.merge_pull_request(req, "THREE_WAY_MERGE")
1986 }
1987
1988 fn merge_pull_request(
1989 &self,
1990 req: &AwsRequest,
1991 merge_option: &str,
1992 ) -> Result<AwsResponse, AwsServiceError> {
1993 let b = body(req);
1994 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1995 let name = require_repository_name(&b)?;
1996 validate_merge_conflict_enums(&b)?;
1997 let author = caller_arn(req);
1998 let account = self.account(req);
1999 let mut guard = self.state.write();
2000 let st = guard.get_or_create(&account);
2001 if !st.repositories.contains_key(&name) {
2002 return Err(repo_not_found(&name));
2003 }
2004 let now = Utc::now();
2005
2006 let (source_ref, dest_ref, revision, rules) = {
2009 let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2010 if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2011 return Err(err(
2012 "PullRequestAlreadyClosedException",
2013 "The pull request status cannot be updated because it is already closed.",
2014 ));
2015 }
2016 let target = pr
2017 .get("pullRequestTargets")
2018 .and_then(Value::as_array)
2019 .and_then(|ts| {
2020 ts.iter().find(|t| {
2021 t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str())
2022 })
2023 })
2024 .ok_or_else(|| {
2025 err(
2026 "RepositoryNotAssociatedWithPullRequestException",
2027 "The repository is not associated with the pull request.",
2028 )
2029 })?;
2030 let source_ref = target
2031 .get("sourceReference")
2032 .and_then(Value::as_str)
2033 .unwrap_or_default()
2034 .to_string();
2035 let dest_ref = target
2036 .get("destinationReference")
2037 .and_then(Value::as_str)
2038 .unwrap_or_default()
2039 .to_string();
2040 let revision = pr
2041 .get("revisionId")
2042 .and_then(Value::as_str)
2043 .unwrap_or_default()
2044 .to_string();
2045 let rules = pr
2046 .get("approvalRules")
2047 .and_then(Value::as_array)
2048 .cloned()
2049 .unwrap_or_default();
2050 (source_ref, dest_ref, revision, rules)
2051 };
2052
2053 let overridden = st
2056 .pull_request_overrides
2057 .get(&id)
2058 .and_then(|m| m.get(&revision))
2059 .is_some();
2060 if !overridden && !rules.is_empty() {
2061 let approvers: std::collections::BTreeSet<String> = st
2062 .pull_request_approvals
2063 .get(&id)
2064 .and_then(|m| m.get(&revision))
2065 .map(|m| {
2066 m.iter()
2067 .filter(|(_, v)| v.as_str() == "APPROVE")
2068 .map(|(arn, _)| arn.clone())
2069 .collect()
2070 })
2071 .unwrap_or_default();
2072 let all_satisfied = rules.iter().all(|r| {
2073 let content = r
2074 .get("approvalRuleContent")
2075 .and_then(Value::as_str)
2076 .unwrap_or_default();
2077 rule_satisfied(content, &approvers)
2078 });
2079 if !all_satisfied {
2080 return Err(err(
2081 "PullRequestApprovalRulesNotSatisfiedException",
2082 "The pull request cannot be merged because one or more approval rules have not been satisfied.",
2083 ));
2084 }
2085 }
2086
2087 let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
2090 let repo = st.repositories.get_mut(&name).expect("repo checked above");
2091 let source_tip = repo.branches.get(&source_ref).cloned().ok_or_else(|| {
2092 err(
2093 "ReferenceDoesNotExistException",
2094 "The specified reference does not exist.",
2095 )
2096 })?;
2097 let dest_tip = repo.branches.get(&dest_ref).cloned().ok_or_else(|| {
2098 err(
2099 "ReferenceDoesNotExistException",
2100 "The specified reference does not exist.",
2101 )
2102 })?;
2103 if let Some(pinned) = str_field(&b, "sourceCommitId") {
2105 if pinned != source_tip {
2106 return Err(err(
2107 "TipOfSourceReferenceIsDifferentException",
2108 "The tip of the source reference is different from the specified commit ID.",
2109 ));
2110 }
2111 }
2112 let merge_commit_id = match merge_option {
2113 "FAST_FORWARD_MERGE" => {
2114 if !is_ancestor(repo, &dest_tip, &source_tip) {
2115 return Err(err(
2116 "ManualMergeRequiredException",
2117 "The fast-forward merge cannot be performed because the destination is not an ancestor of the source.",
2118 ));
2119 }
2120 repo.branches.insert(dest_ref.clone(), source_tip.clone());
2121 source_tip.clone()
2122 }
2123 _ => {
2124 let merged =
2125 merge_trees(repo, &source_tip, &dest_tip, &strategy).ok_or_else(|| {
2126 err(
2127 "ManualMergeRequiredException",
2128 "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
2129 )
2130 })?;
2131 let tree_id = tree_id_for(&merged);
2132 let commit_id = fresh_object_id();
2133 let parents = merge_parents(merge_option, &dest_tip, &source_tip);
2134 let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
2135 repo.commits.insert(commit_id.clone(), commit);
2136 repo.trees.insert(commit_id.clone(), merged);
2137 repo.branches.insert(dest_ref.clone(), commit_id.clone());
2138 commit_id
2139 }
2140 };
2141
2142 refresh_source_revisions(st, &name, &dest_ref);
2145
2146 let pr = st.pull_requests.get_mut(&id).expect("pr checked above");
2148 pr["pullRequestStatus"] = json!("CLOSED");
2149 pr["lastActivityDate"] = ts(now);
2150 if let Some(targets) = pr
2151 .get_mut("pullRequestTargets")
2152 .and_then(Value::as_array_mut)
2153 {
2154 for t in targets.iter_mut() {
2155 if t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str()) {
2156 t["mergeMetadata"] = json!({
2157 "isMerged": true,
2158 "mergedBy": author,
2159 "mergeCommitId": merge_commit_id,
2160 "mergeOption": merge_option,
2161 });
2162 }
2163 }
2164 }
2165 let pr = pr.clone();
2166 st.pull_request_events
2167 .entry(id.clone())
2168 .or_default()
2169 .push(pr_event(
2170 &id,
2171 "PULL_REQUEST_MERGE_STATE_CHANGED",
2172 &author,
2173 now,
2174 json!({}),
2175 ));
2176 ok(json!({ "pullRequest": pr }))
2177 }
2178
2179 fn create_pull_request_approval_rule(
2182 &self,
2183 req: &AwsRequest,
2184 ) -> Result<AwsResponse, AwsServiceError> {
2185 let b = body(req);
2186 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2187 let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2188 let content = require(
2189 &b,
2190 "approvalRuleContent",
2191 "ApprovalRuleContentRequiredException",
2192 )?;
2193 if rule_name.chars().count() > 100 {
2194 return Err(err(
2195 "InvalidApprovalRuleNameException",
2196 "The approval rule name is not valid.",
2197 ));
2198 }
2199 if content.chars().count() > 3000 {
2200 return Err(err(
2201 "InvalidApprovalRuleContentException",
2202 "The approval rule content is not valid.",
2203 ));
2204 }
2205 let content = normalize_rule_content(&content);
2206 let now = Utc::now();
2207 let user = caller_arn(req);
2208 let account = self.account(req);
2209 let mut guard = self.state.write();
2210 let st = guard.get_or_create(&account);
2211 let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2212 if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2213 return Err(err(
2214 "PullRequestAlreadyClosedException",
2215 "The pull request is already closed.",
2216 ));
2217 }
2218 let rules = pr
2219 .get_mut("approvalRules")
2220 .and_then(Value::as_array_mut)
2221 .ok_or_else(pr_not_found)?;
2222 if rules
2223 .iter()
2224 .any(|r| r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str()))
2225 {
2226 return Err(err(
2227 "ApprovalRuleNameAlreadyExistsException",
2228 "An approval rule with that name already exists.",
2229 ));
2230 }
2231 let rule = json!({
2232 "approvalRuleId": new_uuid(),
2233 "approvalRuleName": rule_name,
2234 "approvalRuleContent": content,
2235 "ruleContentSha256": sha256_hex(content.as_bytes()),
2236 "lastModifiedDate": ts(now),
2237 "creationDate": ts(now),
2238 "lastModifiedUser": user,
2239 });
2240 rules.push(rule.clone());
2241 ok(json!({ "approvalRule": rule }))
2242 }
2243
2244 fn delete_pull_request_approval_rule(
2245 &self,
2246 req: &AwsRequest,
2247 ) -> Result<AwsResponse, AwsServiceError> {
2248 let b = body(req);
2249 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2250 let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2251 let account = self.account(req);
2252 let mut guard = self.state.write();
2253 let st = guard.get_or_create(&account);
2254 let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2255 let mut deleted_id = String::new();
2256 if let Some(rules) = pr.get_mut("approvalRules").and_then(Value::as_array_mut) {
2257 if let Some(pos) = rules.iter().position(|r| {
2258 r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str())
2259 }) {
2260 deleted_id = rules[pos]
2261 .get("approvalRuleId")
2262 .and_then(Value::as_str)
2263 .unwrap_or_default()
2264 .to_string();
2265 rules.remove(pos);
2266 }
2267 }
2268 ok(json!({ "approvalRuleId": deleted_id }))
2269 }
2270
2271 fn update_pull_request_approval_rule_content(
2272 &self,
2273 req: &AwsRequest,
2274 ) -> Result<AwsResponse, AwsServiceError> {
2275 let b = body(req);
2276 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2277 let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2278 let content = require(&b, "newRuleContent", "ApprovalRuleContentRequiredException")?;
2279 if content.chars().count() > 3000 {
2280 return Err(err(
2281 "InvalidApprovalRuleContentException",
2282 "The approval rule content is not valid.",
2283 ));
2284 }
2285 let content = normalize_rule_content(&content);
2286 let now = Utc::now();
2287 let user = caller_arn(req);
2288 let account = self.account(req);
2289 let mut guard = self.state.write();
2290 let st = guard.get_or_create(&account);
2291 let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2292 let rules = pr
2293 .get_mut("approvalRules")
2294 .and_then(Value::as_array_mut)
2295 .ok_or_else(pr_not_found)?;
2296 let rule = rules
2297 .iter_mut()
2298 .find(|r| r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str()))
2299 .ok_or_else(|| {
2300 err(
2301 "ApprovalRuleDoesNotExistException",
2302 "The specified approval rule does not exist.",
2303 )
2304 })?;
2305 rule["approvalRuleContent"] = json!(content);
2306 rule["ruleContentSha256"] = json!(sha256_hex(content.as_bytes()));
2307 rule["lastModifiedDate"] = ts(now);
2308 rule["lastModifiedUser"] = json!(user);
2309 let rule = rule.clone();
2310 ok(json!({ "approvalRule": rule }))
2311 }
2312
2313 fn update_pull_request_approval_state(
2314 &self,
2315 req: &AwsRequest,
2316 ) -> Result<AwsResponse, AwsServiceError> {
2317 let b = body(req);
2318 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2319 let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2320 let state = require(&b, "approvalState", "ApprovalStateRequiredException")?;
2321 if !validate::is_enum(validate::APPROVAL_STATE, &state) {
2322 return Err(err(
2323 "InvalidApprovalStateException",
2324 "The approval state is not valid.",
2325 ));
2326 }
2327 let user = caller_arn(req);
2328 let account = self.account(req);
2329 let mut guard = self.state.write();
2330 let st = guard.get_or_create(&account);
2331 let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2332 if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2333 return Err(err(
2334 "PullRequestAlreadyClosedException",
2335 "The pull request is already closed.",
2336 ));
2337 }
2338 let current_revision = pr
2339 .get("revisionId")
2340 .and_then(Value::as_str)
2341 .unwrap_or_default()
2342 .to_string();
2343 if revision != current_revision {
2344 return Err(err(
2345 "RevisionNotCurrentException",
2346 "The revision ID is not the current revision of the pull request.",
2347 ));
2348 }
2349 if pr.get("authorArn").and_then(Value::as_str) == Some(user.as_str()) {
2350 return Err(err(
2351 "PullRequestCannotBeApprovedByAuthorException",
2352 "The pull request cannot be approved by the author of the pull request.",
2353 ));
2354 }
2355 st.pull_request_approvals
2356 .entry(id)
2357 .or_default()
2358 .entry(revision)
2359 .or_default()
2360 .insert(user, state);
2361 ok(json!({}))
2362 }
2363
2364 fn get_pull_request_approval_states(
2365 &self,
2366 req: &AwsRequest,
2367 ) -> Result<AwsResponse, AwsServiceError> {
2368 let b = body(req);
2369 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2370 let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2371 let account = self.account(req);
2372 let guard = self.state.read();
2373 let st = guard.get(&account);
2374 let s = st.ok_or_else(pr_not_found)?;
2375 if !s.pull_requests.contains_key(&id) {
2376 return Err(pr_not_found());
2377 }
2378 let approvals: Vec<Value> = s
2379 .pull_request_approvals
2380 .get(&id)
2381 .and_then(|m| m.get(&revision))
2382 .map(|m| {
2383 m.iter()
2384 .filter(|(_, v)| v.as_str() == "APPROVE")
2385 .map(|(arn, v)| json!({ "userArn": arn, "approvalState": v }))
2386 .collect()
2387 })
2388 .unwrap_or_default();
2389 ok(json!({ "approvals": approvals }))
2390 }
2391
2392 fn evaluate_pull_request_approval_rules(
2393 &self,
2394 req: &AwsRequest,
2395 ) -> Result<AwsResponse, AwsServiceError> {
2396 let b = body(req);
2397 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2398 let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2399 let account = self.account(req);
2400 let guard = self.state.read();
2401 let st = guard.get(&account);
2402 let s = st.ok_or_else(pr_not_found)?;
2403 let pr = s.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2404 let current_revision = pr
2405 .get("revisionId")
2406 .and_then(Value::as_str)
2407 .unwrap_or_default();
2408 if revision != current_revision {
2409 return Err(err(
2410 "RevisionNotCurrentException",
2411 "The revision ID is not the current revision of the pull request.",
2412 ));
2413 }
2414 let overridden = s
2415 .pull_request_overrides
2416 .get(&id)
2417 .and_then(|m| m.get(&revision))
2418 .is_some();
2419 let rules = pr
2420 .get("approvalRules")
2421 .and_then(Value::as_array)
2422 .cloned()
2423 .unwrap_or_default();
2424 let approvers: std::collections::BTreeSet<String> = s
2426 .pull_request_approvals
2427 .get(&id)
2428 .and_then(|m| m.get(&revision))
2429 .map(|m| {
2430 m.iter()
2431 .filter(|(_, v)| v.as_str() == "APPROVE")
2432 .map(|(arn, _)| arn.clone())
2433 .collect()
2434 })
2435 .unwrap_or_default();
2436 let mut satisfied_names = Vec::new();
2437 let mut not_satisfied_names = Vec::new();
2438 for r in &rules {
2439 let Some(name) = r.get("approvalRuleName").cloned() else {
2440 continue;
2441 };
2442 let content = r
2443 .get("approvalRuleContent")
2444 .and_then(Value::as_str)
2445 .unwrap_or_default();
2446 if overridden || rule_satisfied(content, &approvers) {
2449 satisfied_names.push(name);
2450 } else {
2451 not_satisfied_names.push(name);
2452 }
2453 }
2454 let approved = not_satisfied_names.is_empty();
2455 ok(json!({
2456 "evaluation": {
2457 "approved": approved,
2458 "overridden": overridden,
2459 "approvalRulesSatisfied": satisfied_names,
2460 "approvalRulesNotSatisfied": not_satisfied_names,
2461 }
2462 }))
2463 }
2464
2465 fn override_pull_request_approval_rules(
2466 &self,
2467 req: &AwsRequest,
2468 ) -> Result<AwsResponse, AwsServiceError> {
2469 let b = body(req);
2470 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2471 let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2472 let status = require(&b, "overrideStatus", "OverrideStatusRequiredException")?;
2473 if !validate::is_enum(validate::OVERRIDE_STATUS, &status) {
2474 return Err(err(
2475 "InvalidOverrideStatusException",
2476 "The override status is not valid.",
2477 ));
2478 }
2479 let user = caller_arn(req);
2480 let account = self.account(req);
2481 let mut guard = self.state.write();
2482 let st = guard.get_or_create(&account);
2483 {
2484 let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2485 let current_revision = pr
2486 .get("revisionId")
2487 .and_then(Value::as_str)
2488 .unwrap_or_default();
2489 if revision != current_revision {
2490 return Err(err(
2491 "RevisionNotCurrentException",
2492 "The revision ID is not the current revision of the pull request.",
2493 ));
2494 }
2495 }
2496 let overrides = st.pull_request_overrides.entry(id).or_default();
2497 if status == "OVERRIDE" {
2498 if overrides.contains_key(&revision) {
2499 return Err(err(
2500 "OverrideAlreadySetException",
2501 "The override status is already set.",
2502 ));
2503 }
2504 overrides.insert(revision, user);
2505 } else {
2506 overrides.remove(&revision);
2507 }
2508 ok(json!({}))
2509 }
2510
2511 fn get_pull_request_override_state(
2512 &self,
2513 req: &AwsRequest,
2514 ) -> Result<AwsResponse, AwsServiceError> {
2515 let b = body(req);
2516 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2517 let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2518 let account = self.account(req);
2519 let guard = self.state.read();
2520 let st = guard.get(&account);
2521 let s = st.ok_or_else(pr_not_found)?;
2522 if !s.pull_requests.contains_key(&id) {
2523 return Err(pr_not_found());
2524 }
2525 match s
2526 .pull_request_overrides
2527 .get(&id)
2528 .and_then(|m| m.get(&revision))
2529 {
2530 Some(arn) => ok(json!({ "overridden": true, "overrider": arn })),
2531 None => ok(json!({ "overridden": false })),
2532 }
2533 }
2534
2535 fn create_approval_rule_template(
2538 &self,
2539 req: &AwsRequest,
2540 ) -> Result<AwsResponse, AwsServiceError> {
2541 let b = body(req);
2542 let name = require(
2543 &b,
2544 "approvalRuleTemplateName",
2545 "ApprovalRuleTemplateNameRequiredException",
2546 )?;
2547 let content = require(
2548 &b,
2549 "approvalRuleTemplateContent",
2550 "ApprovalRuleTemplateContentRequiredException",
2551 )?;
2552 if name.chars().count() > 100 {
2553 return Err(err(
2554 "InvalidApprovalRuleTemplateNameException",
2555 "The approval rule template name is not valid.",
2556 ));
2557 }
2558 if content.chars().count() > 3000 {
2559 return Err(err(
2560 "InvalidApprovalRuleTemplateContentException",
2561 "The approval rule template content is not valid.",
2562 ));
2563 }
2564 let content = normalize_rule_content(&content);
2565 if let Some(d) = str_field(&b, "approvalRuleTemplateDescription") {
2566 if d.chars().count() > 1000 {
2567 return Err(err(
2568 "InvalidApprovalRuleTemplateDescriptionException",
2569 "The approval rule template description is not valid.",
2570 ));
2571 }
2572 }
2573 let now = Utc::now();
2574 let user = caller_arn(req);
2575 let account = self.account(req);
2576 let mut guard = self.state.write();
2577 let st = guard.get_or_create(&account);
2578 if st.templates.contains_key(&name) {
2579 return Err(err(
2580 "ApprovalRuleTemplateNameAlreadyExistsException",
2581 format!("An approval rule template with the name {name} already exists."),
2582 ));
2583 }
2584 let mut tmpl = Map::new();
2585 tmpl.insert("approvalRuleTemplateId".into(), json!(new_uuid()));
2586 tmpl.insert("approvalRuleTemplateName".into(), json!(name));
2587 if let Some(d) = str_field(&b, "approvalRuleTemplateDescription") {
2588 tmpl.insert("approvalRuleTemplateDescription".into(), json!(d));
2589 }
2590 tmpl.insert("approvalRuleTemplateContent".into(), json!(content));
2591 tmpl.insert(
2592 "ruleContentSha256".into(),
2593 json!(sha256_hex(content.as_bytes())),
2594 );
2595 tmpl.insert("lastModifiedDate".into(), ts(now));
2596 tmpl.insert("creationDate".into(), ts(now));
2597 tmpl.insert("lastModifiedUser".into(), json!(user));
2598 let tmpl = Value::Object(tmpl);
2599 st.templates.insert(name.clone(), tmpl.clone());
2600 st.template_order.push(name);
2601 ok(json!({ "approvalRuleTemplate": tmpl }))
2602 }
2603
2604 fn get_approval_rule_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2605 let b = body(req);
2606 let name = require(
2607 &b,
2608 "approvalRuleTemplateName",
2609 "ApprovalRuleTemplateNameRequiredException",
2610 )?;
2611 let account = self.account(req);
2612 let guard = self.state.read();
2613 let st = guard.get(&account);
2614 match st.and_then(|s| s.templates.get(&name)) {
2615 Some(t) => ok(json!({ "approvalRuleTemplate": t })),
2616 None => Err(template_not_found()),
2617 }
2618 }
2619
2620 fn delete_approval_rule_template(
2621 &self,
2622 req: &AwsRequest,
2623 ) -> Result<AwsResponse, AwsServiceError> {
2624 let b = body(req);
2625 let name = require(
2626 &b,
2627 "approvalRuleTemplateName",
2628 "ApprovalRuleTemplateNameRequiredException",
2629 )?;
2630 if name.chars().count() > 100 {
2631 return Err(err(
2632 "InvalidApprovalRuleTemplateNameException",
2633 "The approval rule template name is not valid.",
2634 ));
2635 }
2636 let account = self.account(req);
2637 let mut guard = self.state.write();
2638 let st = guard.get_or_create(&account);
2639 let id = st
2640 .templates
2641 .remove(&name)
2642 .and_then(|t| {
2643 t.get("approvalRuleTemplateId")
2644 .and_then(Value::as_str)
2645 .map(str::to_string)
2646 })
2647 .unwrap_or_default();
2648 st.template_order.retain(|n| n != &name);
2649 for repo in st.repositories.values_mut() {
2650 repo.associated_templates.retain(|t| t != &name);
2651 }
2652 ok(json!({ "approvalRuleTemplateId": id }))
2653 }
2654
2655 fn list_approval_rule_templates(
2656 &self,
2657 req: &AwsRequest,
2658 ) -> Result<AwsResponse, AwsServiceError> {
2659 let account = self.account(req);
2660 let guard = self.state.read();
2661 let st = guard.get(&account);
2662 let names: Vec<Value> = st
2663 .map(|s| s.template_order.iter().map(|n| json!(n)).collect())
2664 .unwrap_or_default();
2665 ok(json!({ "approvalRuleTemplateNames": names }))
2666 }
2667
2668 fn update_approval_rule_template_content(
2669 &self,
2670 req: &AwsRequest,
2671 ) -> Result<AwsResponse, AwsServiceError> {
2672 let b = body(req);
2673 let name = require(
2674 &b,
2675 "approvalRuleTemplateName",
2676 "ApprovalRuleTemplateNameRequiredException",
2677 )?;
2678 let content = require(
2679 &b,
2680 "newRuleContent",
2681 "ApprovalRuleTemplateContentRequiredException",
2682 )?;
2683 if content.chars().count() > 3000 {
2684 return Err(err(
2685 "InvalidApprovalRuleTemplateContentException",
2686 "The approval rule template content is not valid.",
2687 ));
2688 }
2689 let content = normalize_rule_content(&content);
2690 self.mutate_template(req, &name, |t| {
2691 t["approvalRuleTemplateContent"] = json!(content);
2692 t["ruleContentSha256"] = json!(sha256_hex(content.as_bytes()));
2693 t["lastModifiedDate"] = ts(Utc::now());
2694 Ok(())
2695 })
2696 }
2697
2698 fn update_approval_rule_template_description(
2699 &self,
2700 req: &AwsRequest,
2701 ) -> Result<AwsResponse, AwsServiceError> {
2702 let b = body(req);
2703 let name = require(
2704 &b,
2705 "approvalRuleTemplateName",
2706 "ApprovalRuleTemplateNameRequiredException",
2707 )?;
2708 let desc = require(
2709 &b,
2710 "approvalRuleTemplateDescription",
2711 "InvalidApprovalRuleTemplateDescriptionException",
2712 )?;
2713 if desc.chars().count() > 1000 {
2714 return Err(err(
2715 "InvalidApprovalRuleTemplateDescriptionException",
2716 "The approval rule template description is not valid.",
2717 ));
2718 }
2719 self.mutate_template(req, &name, |t| {
2720 t["approvalRuleTemplateDescription"] = json!(desc);
2721 t["lastModifiedDate"] = ts(Utc::now());
2722 Ok(())
2723 })
2724 }
2725
2726 fn update_approval_rule_template_name(
2727 &self,
2728 req: &AwsRequest,
2729 ) -> Result<AwsResponse, AwsServiceError> {
2730 let b = body(req);
2731 let old = require(
2732 &b,
2733 "oldApprovalRuleTemplateName",
2734 "ApprovalRuleTemplateNameRequiredException",
2735 )?;
2736 let new = require(
2737 &b,
2738 "newApprovalRuleTemplateName",
2739 "ApprovalRuleTemplateNameRequiredException",
2740 )?;
2741 if new.chars().count() > 100 {
2742 return Err(err(
2743 "InvalidApprovalRuleTemplateNameException",
2744 "The approval rule template name is not valid.",
2745 ));
2746 }
2747 let account = self.account(req);
2748 let mut guard = self.state.write();
2749 let st = guard.get_or_create(&account);
2750 if !st.templates.contains_key(&old) {
2751 return Err(template_not_found());
2752 }
2753 if old != new && st.templates.contains_key(&new) {
2754 return Err(err(
2755 "ApprovalRuleTemplateNameAlreadyExistsException",
2756 format!("An approval rule template with the name {new} already exists."),
2757 ));
2758 }
2759 let mut tmpl = st.templates.remove(&old).unwrap();
2760 tmpl["approvalRuleTemplateName"] = json!(new);
2761 tmpl["lastModifiedDate"] = ts(Utc::now());
2762 for n in &mut st.template_order {
2763 if n == &old {
2764 *n = new.clone();
2765 }
2766 }
2767 for repo in st.repositories.values_mut() {
2768 for t in &mut repo.associated_templates {
2769 if t == &old {
2770 *t = new.clone();
2771 }
2772 }
2773 }
2774 st.templates.insert(new, tmpl.clone());
2775 ok(json!({ "approvalRuleTemplate": tmpl }))
2776 }
2777
2778 fn associate_template_with_repository(
2779 &self,
2780 req: &AwsRequest,
2781 ) -> Result<AwsResponse, AwsServiceError> {
2782 let b = body(req);
2783 let tmpl = require(
2784 &b,
2785 "approvalRuleTemplateName",
2786 "ApprovalRuleTemplateNameRequiredException",
2787 )?;
2788 let name = require_repository_name(&b)?;
2789 let account = self.account(req);
2790 let mut guard = self.state.write();
2791 let st = guard.get_or_create(&account);
2792 if !st.templates.contains_key(&tmpl) {
2793 return Err(template_not_found());
2794 }
2795 let repo = st
2796 .repositories
2797 .get_mut(&name)
2798 .ok_or_else(|| repo_not_found(&name))?;
2799 if !repo.associated_templates.contains(&tmpl) {
2800 repo.associated_templates.push(tmpl);
2801 }
2802 ok(json!({}))
2803 }
2804
2805 fn disassociate_template_from_repository(
2806 &self,
2807 req: &AwsRequest,
2808 ) -> Result<AwsResponse, AwsServiceError> {
2809 let b = body(req);
2810 let tmpl = require(
2811 &b,
2812 "approvalRuleTemplateName",
2813 "ApprovalRuleTemplateNameRequiredException",
2814 )?;
2815 let name = require_repository_name(&b)?;
2816 let account = self.account(req);
2817 let mut guard = self.state.write();
2818 let st = guard.get_or_create(&account);
2819 if !st.templates.contains_key(&tmpl) {
2820 return Err(template_not_found());
2821 }
2822 let repo = st
2823 .repositories
2824 .get_mut(&name)
2825 .ok_or_else(|| repo_not_found(&name))?;
2826 repo.associated_templates.retain(|t| t != &tmpl);
2827 ok(json!({}))
2828 }
2829
2830 fn batch_associate_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2831 let b = body(req);
2832 let tmpl = require(
2833 &b,
2834 "approvalRuleTemplateName",
2835 "ApprovalRuleTemplateNameRequiredException",
2836 )?;
2837 let names = b
2838 .get("repositoryNames")
2839 .and_then(Value::as_array)
2840 .ok_or_else(|| {
2841 err(
2842 "RepositoryNamesRequiredException",
2843 "Repository names are required.",
2844 )
2845 })?
2846 .clone();
2847 let account = self.account(req);
2848 let mut guard = self.state.write();
2849 let st = guard.get_or_create(&account);
2850 if !st.templates.contains_key(&tmpl) {
2851 return Err(template_not_found());
2852 }
2853 let mut associated = Vec::new();
2854 let mut errors = Vec::new();
2855 for n in &names {
2856 let Some(rn) = n.as_str() else { continue };
2857 match st.repositories.get_mut(rn) {
2858 Some(repo) => {
2859 if !repo.associated_templates.contains(&tmpl) {
2860 repo.associated_templates.push(tmpl.clone());
2861 }
2862 associated.push(json!(rn));
2863 }
2864 None => errors.push(json!({
2865 "repositoryName": rn,
2866 "errorCode": "RepositoryDoesNotExist",
2867 "errorMessage": format!("The repository {rn} does not exist."),
2868 })),
2869 }
2870 }
2871 ok(json!({ "associatedRepositoryNames": associated, "errors": errors }))
2872 }
2873
2874 fn batch_disassociate_template(
2875 &self,
2876 req: &AwsRequest,
2877 ) -> Result<AwsResponse, AwsServiceError> {
2878 let b = body(req);
2879 let tmpl = require(
2880 &b,
2881 "approvalRuleTemplateName",
2882 "ApprovalRuleTemplateNameRequiredException",
2883 )?;
2884 let names = b
2885 .get("repositoryNames")
2886 .and_then(Value::as_array)
2887 .ok_or_else(|| {
2888 err(
2889 "RepositoryNamesRequiredException",
2890 "Repository names are required.",
2891 )
2892 })?
2893 .clone();
2894 let account = self.account(req);
2895 let mut guard = self.state.write();
2896 let st = guard.get_or_create(&account);
2897 if !st.templates.contains_key(&tmpl) {
2898 return Err(template_not_found());
2899 }
2900 let mut disassociated = Vec::new();
2901 let mut errors = Vec::new();
2902 for n in &names {
2903 let Some(rn) = n.as_str() else { continue };
2904 match st.repositories.get_mut(rn) {
2905 Some(repo) => {
2906 repo.associated_templates.retain(|t| t != &tmpl);
2907 disassociated.push(json!(rn));
2908 }
2909 None => errors.push(json!({
2910 "repositoryName": rn,
2911 "errorCode": "RepositoryDoesNotExist",
2912 "errorMessage": format!("The repository {rn} does not exist."),
2913 })),
2914 }
2915 }
2916 ok(json!({ "disassociatedRepositoryNames": disassociated, "errors": errors }))
2917 }
2918
2919 fn list_associated_templates_for_repository(
2920 &self,
2921 req: &AwsRequest,
2922 ) -> Result<AwsResponse, AwsServiceError> {
2923 let b = body(req);
2924 let name = require_repository_name(&b)?;
2925 let account = self.account(req);
2926 let guard = self.state.read();
2927 let st = guard.get(&account);
2928 let repo = st
2929 .and_then(|s| s.repositories.get(&name))
2930 .ok_or_else(|| repo_not_found(&name))?;
2931 let names: Vec<Value> = repo.associated_templates.iter().map(|t| json!(t)).collect();
2932 ok(json!({ "approvalRuleTemplateNames": names }))
2933 }
2934
2935 fn list_repositories_for_template(
2936 &self,
2937 req: &AwsRequest,
2938 ) -> Result<AwsResponse, AwsServiceError> {
2939 let b = body(req);
2940 let tmpl = require(
2941 &b,
2942 "approvalRuleTemplateName",
2943 "ApprovalRuleTemplateNameRequiredException",
2944 )?;
2945 let account = self.account(req);
2946 let guard = self.state.read();
2947 let st = guard.get(&account);
2948 let s = st.ok_or_else(template_not_found)?;
2949 if !s.templates.contains_key(&tmpl) {
2950 return Err(template_not_found());
2951 }
2952 let names: Vec<Value> = s
2953 .repository_order
2954 .iter()
2955 .filter(|n| {
2956 s.repositories
2957 .get(*n)
2958 .map(|r| r.associated_templates.contains(&tmpl))
2959 .unwrap_or(false)
2960 })
2961 .map(|n| json!(n))
2962 .collect();
2963 ok(json!({ "repositoryNames": names }))
2964 }
2965
2966 fn post_comment_for_compared_commit(
2969 &self,
2970 req: &AwsRequest,
2971 ) -> Result<AwsResponse, AwsServiceError> {
2972 let b = body(req);
2973 let name = require_repository_name(&b)?;
2974 let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
2975 let content = require(&b, "content", "CommentContentRequiredException")?;
2976 if content.chars().count() > 10240 {
2977 return Err(err(
2978 "CommentContentSizeLimitExceededException",
2979 "The comment is too large. Comments are limited to 10,240 characters.",
2980 ));
2981 }
2982 let before = str_field(&b, "beforeCommitId");
2983 if before.as_deref() == Some(after.as_str()) {
2984 return Err(err(
2985 "BeforeCommitIdAndAfterCommitIdAreSameException",
2986 "The before commit ID and the after commit ID are the same.",
2987 ));
2988 }
2989 let account = self.account(req);
2990 let mut guard = self.state.write();
2991 let st = guard.get_or_create(&account);
2992 if !st.repositories.contains_key(&name) {
2993 return Err(repo_not_found(&name));
2994 }
2995 let now = Utc::now();
2996 let author = caller_arn(req);
2997 let comment_id = new_uuid();
2998 let location = b.get("location").cloned();
2999 let mut stored = comment_value(&comment_id, &content, &author, now, None);
3000 if let Some(obj) = stored.as_object_mut() {
3001 obj.insert("_ctxRepositoryName".into(), json!(name));
3002 obj.insert("_ctxAfterCommitId".into(), json!(after));
3003 if let Some(bc) = &before {
3004 obj.insert("_ctxBeforeCommitId".into(), json!(bc));
3005 }
3006 if let Some(loc) = &location {
3007 obj.insert("_ctxLocation".into(), loc.clone());
3008 }
3009 }
3010 st.comments.insert(comment_id.clone(), stored.clone());
3011 st.comment_order.push(comment_id);
3012 let public = public_comment(&stored);
3013 let mut out = Map::new();
3014 out.insert("repositoryName".into(), json!(name));
3015 if let Some(bc) = before {
3016 out.insert("beforeCommitId".into(), json!(bc));
3017 }
3018 out.insert("afterCommitId".into(), json!(after));
3019 if let Some(loc) = location {
3020 out.insert("location".into(), loc);
3021 }
3022 out.insert("comment".into(), public);
3023 ok(Value::Object(out))
3024 }
3025
3026 fn post_comment_for_pull_request(
3027 &self,
3028 req: &AwsRequest,
3029 ) -> Result<AwsResponse, AwsServiceError> {
3030 let b = body(req);
3031 let pr_id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
3032 let name = require_repository_name(&b)?;
3033 let before = require(&b, "beforeCommitId", "CommitIdRequiredException")?;
3034 let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
3035 let content = require(&b, "content", "CommentContentRequiredException")?;
3036 if content.chars().count() > 10240 {
3037 return Err(err(
3038 "CommentContentSizeLimitExceededException",
3039 "The comment is too large.",
3040 ));
3041 }
3042 let account = self.account(req);
3043 let mut guard = self.state.write();
3044 let st = guard.get_or_create(&account);
3045 if !st.repositories.contains_key(&name) {
3046 return Err(repo_not_found(&name));
3047 }
3048 if !st.pull_requests.contains_key(&pr_id) {
3049 return Err(pr_not_found());
3050 }
3051 let now = Utc::now();
3052 let author = caller_arn(req);
3053 let comment_id = new_uuid();
3054 let location = b.get("location").cloned();
3055 let mut stored = comment_value(&comment_id, &content, &author, now, None);
3056 if let Some(obj) = stored.as_object_mut() {
3057 obj.insert("_ctxRepositoryName".into(), json!(name));
3058 obj.insert("_ctxPullRequestId".into(), json!(pr_id));
3059 obj.insert("_ctxBeforeCommitId".into(), json!(before));
3060 obj.insert("_ctxAfterCommitId".into(), json!(after));
3061 if let Some(loc) = &location {
3062 obj.insert("_ctxLocation".into(), loc.clone());
3063 }
3064 }
3065 st.comments.insert(comment_id.clone(), stored.clone());
3066 st.comment_order.push(comment_id);
3067 let public = public_comment(&stored);
3068 let mut out = Map::new();
3069 out.insert("repositoryName".into(), json!(name));
3070 out.insert("pullRequestId".into(), json!(pr_id));
3071 out.insert("beforeCommitId".into(), json!(before));
3072 out.insert("afterCommitId".into(), json!(after));
3073 if let Some(loc) = location {
3074 out.insert("location".into(), loc);
3075 }
3076 out.insert("comment".into(), public);
3077 ok(Value::Object(out))
3078 }
3079
3080 fn post_comment_reply(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3081 let b = body(req);
3082 let in_reply_to = require(&b, "inReplyTo", "CommentIdRequiredException")?;
3083 let content = require(&b, "content", "CommentContentRequiredException")?;
3084 if content.chars().count() > 10240 {
3085 return Err(err(
3086 "CommentContentSizeLimitExceededException",
3087 "The comment is too large.",
3088 ));
3089 }
3090 let account = self.account(req);
3091 let mut guard = self.state.write();
3092 let st = guard.get_or_create(&account);
3093 let parent = st.comments.get(&in_reply_to).cloned().ok_or_else(|| {
3094 err(
3095 "CommentDoesNotExistException",
3096 "The specified comment does not exist.",
3097 )
3098 })?;
3099 let now = Utc::now();
3100 let author = caller_arn(req);
3101 let comment_id = new_uuid();
3102 let mut stored = comment_value(&comment_id, &content, &author, now, Some(&in_reply_to));
3103 if let (Some(obj), Some(pobj)) = (stored.as_object_mut(), parent.as_object()) {
3105 for (k, v) in pobj {
3106 if k.starts_with("_ctx") {
3107 obj.insert(k.clone(), v.clone());
3108 }
3109 }
3110 }
3111 st.comments.insert(comment_id.clone(), stored.clone());
3112 st.comment_order.push(comment_id);
3113 ok(json!({ "comment": public_comment(&stored) }))
3114 }
3115
3116 fn get_comment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3117 let b = body(req);
3118 let id = require(&b, "commentId", "CommentIdRequiredException")?;
3119 let account = self.account(req);
3120 let guard = self.state.read();
3121 let st = guard.get(&account);
3122 let comment = st.and_then(|s| s.comments.get(&id)).ok_or_else(|| {
3123 err(
3124 "CommentDoesNotExistException",
3125 "The specified comment does not exist.",
3126 )
3127 })?;
3128 if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3129 return Err(err(
3130 "CommentDeletedException",
3131 "This comment has already been deleted.",
3132 ));
3133 }
3134 ok(json!({ "comment": public_comment(comment) }))
3135 }
3136
3137 fn get_comments_for_compared_commit(
3138 &self,
3139 req: &AwsRequest,
3140 ) -> Result<AwsResponse, AwsServiceError> {
3141 let b = body(req);
3142 let name = require_repository_name(&b)?;
3143 let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
3144 let before = str_field(&b, "beforeCommitId");
3145 let account = self.account(req);
3146 let guard = self.state.read();
3147 let st = guard.get(&account);
3148 let s = st.ok_or_else(|| repo_not_found(&name))?;
3149 if !s.repositories.contains_key(&name) {
3150 return Err(repo_not_found(&name));
3151 }
3152 let mut threads: Vec<Value> = Vec::new();
3153 for cid in &s.comment_order {
3154 let Some(c) = s.comments.get(cid) else {
3155 continue;
3156 };
3157 if c.get("_ctxPullRequestId").is_some() {
3158 continue;
3159 }
3160 if c.get("_ctxRepositoryName").and_then(Value::as_str) != Some(name.as_str()) {
3161 continue;
3162 }
3163 if c.get("_ctxAfterCommitId").and_then(Value::as_str) != Some(after.as_str()) {
3164 continue;
3165 }
3166 if let Some(bc) = &before {
3167 if c.get("_ctxBeforeCommitId").and_then(Value::as_str) != Some(bc.as_str()) {
3168 continue;
3169 }
3170 }
3171 threads.push(json!({
3172 "repositoryName": name,
3173 "beforeCommitId": before,
3174 "afterCommitId": after,
3175 "location": c.get("_ctxLocation").cloned().unwrap_or(Value::Null),
3176 "comments": [public_comment(c)],
3177 }));
3178 }
3179 ok(json!({ "commentsForComparedCommitData": threads }))
3180 }
3181
3182 fn get_comments_for_pull_request(
3183 &self,
3184 req: &AwsRequest,
3185 ) -> Result<AwsResponse, AwsServiceError> {
3186 let b = body(req);
3187 let pr_id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
3188 let account = self.account(req);
3189 let guard = self.state.read();
3190 let st = guard.get(&account);
3191 let s = st.ok_or_else(pr_not_found)?;
3192 if !s.pull_requests.contains_key(&pr_id) {
3193 return Err(pr_not_found());
3194 }
3195 let mut threads: Vec<Value> = Vec::new();
3196 for cid in &s.comment_order {
3197 let Some(c) = s.comments.get(cid) else {
3198 continue;
3199 };
3200 if c.get("_ctxPullRequestId").and_then(Value::as_str) != Some(pr_id.as_str()) {
3201 continue;
3202 }
3203 threads.push(json!({
3204 "pullRequestId": pr_id,
3205 "repositoryName": c.get("_ctxRepositoryName").cloned().unwrap_or(Value::Null),
3206 "beforeCommitId": c.get("_ctxBeforeCommitId").cloned().unwrap_or(Value::Null),
3207 "afterCommitId": c.get("_ctxAfterCommitId").cloned().unwrap_or(Value::Null),
3208 "location": c.get("_ctxLocation").cloned().unwrap_or(Value::Null),
3209 "comments": [public_comment(c)],
3210 }));
3211 }
3212 ok(json!({ "commentsForPullRequestData": threads }))
3213 }
3214
3215 fn update_comment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3216 let b = body(req);
3217 let id = require(&b, "commentId", "CommentIdRequiredException")?;
3218 let content = require(&b, "content", "CommentContentRequiredException")?;
3219 if content.chars().count() > 10240 {
3220 return Err(err(
3221 "CommentContentSizeLimitExceededException",
3222 "The comment is too large.",
3223 ));
3224 }
3225 let caller = caller_arn(req);
3226 let account = self.account(req);
3227 let mut guard = self.state.write();
3228 let st = guard.get_or_create(&account);
3229 let comment = st.comments.get_mut(&id).ok_or_else(|| {
3230 err(
3231 "CommentDoesNotExistException",
3232 "The specified comment does not exist.",
3233 )
3234 })?;
3235 if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3236 return Err(err(
3237 "CommentDeletedException",
3238 "This comment has already been deleted.",
3239 ));
3240 }
3241 if comment.get("authorArn").and_then(Value::as_str) != Some(caller.as_str()) {
3242 return Err(err(
3243 "CommentNotCreatedByCallerException",
3244 "You cannot modify a comment that was not created by you.",
3245 ));
3246 }
3247 comment["content"] = json!(content);
3248 comment["lastModifiedDate"] = ts(Utc::now());
3249 ok(json!({ "comment": public_comment(comment) }))
3250 }
3251
3252 fn delete_comment_content(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3253 let b = body(req);
3254 let id = require(&b, "commentId", "CommentIdRequiredException")?;
3255 let account = self.account(req);
3256 let mut guard = self.state.write();
3257 let st = guard.get_or_create(&account);
3258 let comment = st.comments.get_mut(&id).ok_or_else(|| {
3259 err(
3260 "CommentDoesNotExistException",
3261 "The specified comment does not exist.",
3262 )
3263 })?;
3264 comment["deleted"] = json!(true);
3265 comment["content"] = json!("");
3266 comment["lastModifiedDate"] = ts(Utc::now());
3267 ok(json!({ "comment": public_comment(comment) }))
3268 }
3269
3270 fn put_comment_reaction(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3271 let b = body(req);
3272 let id = require(&b, "commentId", "CommentIdRequiredException")?;
3273 let reaction = require(&b, "reactionValue", "ReactionValueRequiredException")?;
3274 if !validate::REACTION_EMOJIS.contains(&reaction.as_str())
3275 && !reaction.starts_with(":")
3276 && !reaction.starts_with("\\u")
3277 {
3278 return Err(err(
3279 "InvalidReactionValueException",
3280 "The reaction value is not valid.",
3281 ));
3282 }
3283 let user = caller_arn(req);
3284 let account = self.account(req);
3285 let mut guard = self.state.write();
3286 let st = guard.get_or_create(&account);
3287 let comment = st.comments.get_mut(&id).ok_or_else(|| {
3288 err(
3289 "CommentDoesNotExistException",
3290 "The specified comment does not exist.",
3291 )
3292 })?;
3293 if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3294 return Err(err(
3295 "CommentDeletedException",
3296 "This comment has already been deleted.",
3297 ));
3298 }
3299 let reactions = comment
3300 .as_object_mut()
3301 .unwrap()
3302 .entry("_ctxReactions".to_string())
3303 .or_insert_with(|| json!({}));
3304 if let Some(obj) = reactions.as_object_mut() {
3305 let users = obj.entry(reaction).or_insert_with(|| json!([]));
3306 if let Some(arr) = users.as_array_mut() {
3307 if !arr.iter().any(|u| u.as_str() == Some(user.as_str())) {
3308 arr.push(json!(user));
3309 }
3310 }
3311 }
3312 ok(json!({}))
3313 }
3314
3315 fn get_comment_reactions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3316 let b = body(req);
3317 let id = require(&b, "commentId", "CommentIdRequiredException")?;
3318 let account = self.account(req);
3319 let guard = self.state.read();
3320 let st = guard.get(&account);
3321 let comment = st.and_then(|s| s.comments.get(&id)).ok_or_else(|| {
3322 err(
3323 "CommentDoesNotExistException",
3324 "The specified comment does not exist.",
3325 )
3326 })?;
3327 let mut reactions = Vec::new();
3328 if let Some(obj) = comment.get("_ctxReactions").and_then(Value::as_object) {
3329 for (emoji, users) in obj {
3330 let user_list: Vec<&str> = users
3331 .as_array()
3332 .map(|a| a.iter().filter_map(Value::as_str).collect())
3333 .unwrap_or_default();
3334 reactions.push(json!({
3335 "reaction": {
3336 "emoji": emoji,
3337 "shortCode": emoji,
3338 "unicode": "",
3339 },
3340 "reactionUsers": user_list,
3341 "reactionsFromDeletedUsersCount": 0,
3342 }));
3343 }
3344 }
3345 ok(json!({ "reactionsForComment": reactions }))
3346 }
3347
3348 fn put_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3351 let b = body(req);
3352 let name = require_repository_name(&b)?;
3353 let triggers = b
3354 .get("triggers")
3355 .and_then(Value::as_array)
3356 .ok_or_else(|| {
3357 err(
3358 "RepositoryTriggersListRequiredException",
3359 "The list of triggers for the repository is required.",
3360 )
3361 })?
3362 .clone();
3363 for t in &triggers {
3364 let tname = t.get("name").and_then(Value::as_str);
3365 if tname.map(str::is_empty).unwrap_or(true) {
3366 return Err(err(
3367 "RepositoryTriggerNameRequiredException",
3368 "A name for the trigger is required.",
3369 ));
3370 }
3371 let dest = t.get("destinationArn").and_then(Value::as_str);
3372 if dest.map(str::is_empty).unwrap_or(true) {
3373 return Err(err(
3374 "RepositoryTriggerDestinationArnRequiredException",
3375 "A destination ARN for the target service for the trigger is required.",
3376 ));
3377 }
3378 let events = t.get("events").and_then(Value::as_array);
3379 match events {
3380 None => {
3381 return Err(err(
3382 "RepositoryTriggerEventsListRequiredException",
3383 "At least one event for the trigger is required.",
3384 ))
3385 }
3386 Some(evs) => {
3387 for e in evs {
3388 if let Some(ev) = e.as_str() {
3389 if !validate::is_enum(validate::REPOSITORY_TRIGGER_EVENT, ev) {
3390 return Err(err(
3391 "InvalidRepositoryTriggerEventsException",
3392 "One or more of the trigger events is not valid.",
3393 ));
3394 }
3395 }
3396 }
3397 }
3398 }
3399 }
3400 let account = self.account(req);
3401 let mut guard = self.state.write();
3402 let st = guard.get_or_create(&account);
3403 let repo = st
3404 .repositories
3405 .get_mut(&name)
3406 .ok_or_else(|| repo_not_found(&name))?;
3407 let config_id = new_uuid();
3408 repo.triggers = triggers;
3409 repo.triggers_config_id = config_id.clone();
3410 ok(json!({ "configurationId": config_id }))
3411 }
3412
3413 fn get_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3414 let b = body(req);
3415 let name = require_repository_name(&b)?;
3416 let account = self.account(req);
3417 let guard = self.state.read();
3418 let st = guard.get(&account);
3419 let repo = st
3420 .and_then(|s| s.repositories.get(&name))
3421 .ok_or_else(|| repo_not_found(&name))?;
3422 let config_id = if repo.triggers_config_id.is_empty() {
3423 new_uuid()
3424 } else {
3425 repo.triggers_config_id.clone()
3426 };
3427 ok(json!({
3428 "configurationId": config_id,
3429 "triggers": repo.triggers,
3430 }))
3431 }
3432
3433 fn test_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3434 let b = body(req);
3435 let name = require_repository_name(&b)?;
3436 let triggers = b
3437 .get("triggers")
3438 .and_then(Value::as_array)
3439 .ok_or_else(|| {
3440 err(
3441 "RepositoryTriggersListRequiredException",
3442 "The list of triggers is required.",
3443 )
3444 })?
3445 .clone();
3446 let account = self.account(req);
3447 let guard = self.state.read();
3448 let st = guard.get(&account);
3449 let repo = st
3450 .and_then(|s| s.repositories.get(&name))
3451 .ok_or_else(|| repo_not_found(&name))?;
3452 for t in &triggers {
3454 if t.get("name")
3455 .and_then(Value::as_str)
3456 .map(str::is_empty)
3457 .unwrap_or(true)
3458 {
3459 return Err(err(
3460 "RepositoryTriggerNameRequiredException",
3461 "A name for the trigger is required.",
3462 ));
3463 }
3464 if t.get("destinationArn")
3465 .and_then(Value::as_str)
3466 .map(str::is_empty)
3467 .unwrap_or(true)
3468 {
3469 return Err(err(
3470 "RepositoryTriggerDestinationArnRequiredException",
3471 "A destination ARN for the target service for the trigger is required.",
3472 ));
3473 }
3474 if t.get("events")
3475 .and_then(Value::as_array)
3476 .map(|e| e.is_empty())
3477 .unwrap_or(true)
3478 {
3479 return Err(err(
3480 "RepositoryTriggerEventsListRequiredException",
3481 "At least one event for the trigger is required.",
3482 ));
3483 }
3484 }
3485 let mut successful = Vec::new();
3491 let mut failed = Vec::new();
3492 for t in &triggers {
3493 let tname = t.get("name").and_then(Value::as_str).unwrap_or_default();
3494 let dest = t
3495 .get("destinationArn")
3496 .and_then(Value::as_str)
3497 .unwrap_or_default();
3498 let mut failure: Option<String> = None;
3499 if !trigger_destination_ok(dest, &account, &req.region) {
3500 failure = Some(format!(
3501 "Unable to deliver to destination {dest}. A repository trigger destination must be an Amazon SNS topic or an AWS Lambda function in the same account ({account}) and region ({}).",
3502 req.region
3503 ));
3504 }
3505 if failure.is_none() {
3506 if let Some(evs) = t.get("events").and_then(Value::as_array) {
3507 for e in evs {
3508 if let Some(ev) = e.as_str() {
3509 if !validate::is_enum(validate::REPOSITORY_TRIGGER_EVENT, ev) {
3510 failure = Some(format!("The trigger event {ev} is not valid."));
3511 break;
3512 }
3513 }
3514 }
3515 }
3516 }
3517 if failure.is_none() {
3518 if let Some(branches) = t.get("branches").and_then(Value::as_array) {
3519 for br in branches {
3520 if let Some(bn) = br.as_str() {
3521 if !repo.branches.contains_key(bn) {
3522 failure = Some(format!(
3523 "The branch {bn} specified for the trigger does not exist in the repository."
3524 ));
3525 break;
3526 }
3527 }
3528 }
3529 }
3530 }
3531 match failure {
3532 Some(msg) => {
3533 failed.push(json!({ "trigger": tname, "failureMessage": msg }));
3534 }
3535 None => successful.push(json!(tname)),
3536 }
3537 }
3538 ok(json!({
3539 "successfulExecutions": successful,
3540 "failedExecutions": failed,
3541 }))
3542 }
3543
3544 fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3547 let b = body(req);
3548 let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3549 if !is_codecommit_arn(&arn) {
3550 return Err(err(
3551 "InvalidResourceArnException",
3552 "The resource ARN is not valid.",
3553 ));
3554 }
3555 let tags = b
3556 .get("tags")
3557 .and_then(Value::as_object)
3558 .ok_or_else(|| err("TagsMapRequiredException", "A map of tags is required."))?;
3559 let account = self.account(req);
3560 let mut guard = self.state.write();
3561 let st = guard.get_or_create(&account);
3562 if let Some(rn) = repo_name_from_arn(&arn) {
3563 if !st.repositories.contains_key(&rn) {
3564 return Err(repo_not_found(&rn));
3565 }
3566 }
3567 let entry = st.tags.entry(arn).or_default();
3568 for (k, v) in tags {
3569 if let Some(v) = v.as_str() {
3570 entry.insert(k.clone(), v.to_string());
3571 }
3572 }
3573 ok(json!({}))
3574 }
3575
3576 fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3577 let b = body(req);
3578 let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3579 if !is_codecommit_arn(&arn) {
3580 return Err(err(
3581 "InvalidResourceArnException",
3582 "The resource ARN is not valid.",
3583 ));
3584 }
3585 let keys = b
3586 .get("tagKeys")
3587 .and_then(Value::as_array)
3588 .ok_or_else(|| {
3589 err(
3590 "TagKeysListRequiredException",
3591 "A list of tag keys is required.",
3592 )
3593 })?
3594 .clone();
3595 let account = self.account(req);
3596 let mut guard = self.state.write();
3597 let st = guard.get_or_create(&account);
3598 if let Some(rn) = repo_name_from_arn(&arn) {
3599 if !st.repositories.contains_key(&rn) {
3600 return Err(repo_not_found(&rn));
3601 }
3602 }
3603 if let Some(entry) = st.tags.get_mut(&arn) {
3604 for k in &keys {
3605 if let Some(k) = k.as_str() {
3606 entry.remove(k);
3607 }
3608 }
3609 }
3610 ok(json!({}))
3611 }
3612
3613 fn list_tags_for_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3614 let b = body(req);
3615 let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3616 if !is_codecommit_arn(&arn) {
3617 return Err(err(
3618 "InvalidResourceArnException",
3619 "The resource ARN is not valid.",
3620 ));
3621 }
3622 let account = self.account(req);
3623 let guard = self.state.read();
3624 let st = guard.get(&account);
3625 if let Some(rn) = repo_name_from_arn(&arn) {
3626 if !st
3627 .map(|s| s.repositories.contains_key(&rn))
3628 .unwrap_or(false)
3629 {
3630 return Err(repo_not_found(&rn));
3631 }
3632 }
3633 let tags = st
3634 .and_then(|s| s.tags.get(&arn))
3635 .map(|m| {
3636 let mut obj = Map::new();
3637 for (k, v) in m {
3638 obj.insert(k.clone(), json!(v));
3639 }
3640 Value::Object(obj)
3641 })
3642 .unwrap_or_else(|| json!({}));
3643 ok(json!({ "tags": tags }))
3644 }
3645
3646 fn mutate_pr(
3649 &self,
3650 req: &AwsRequest,
3651 id: &str,
3652 f: impl FnOnce(&mut Value) -> Result<(), AwsServiceError>,
3653 ) -> Result<AwsResponse, AwsServiceError> {
3654 let account = self.account(req);
3655 let mut guard = self.state.write();
3656 let st = guard.get_or_create(&account);
3657 let pr = st.pull_requests.get_mut(id).ok_or_else(pr_not_found)?;
3658 f(pr)?;
3659 let pr = pr.clone();
3660 ok(json!({ "pullRequest": pr }))
3661 }
3662
3663 fn mutate_template(
3664 &self,
3665 req: &AwsRequest,
3666 name: &str,
3667 f: impl FnOnce(&mut Value) -> Result<(), AwsServiceError>,
3668 ) -> Result<AwsResponse, AwsServiceError> {
3669 let account = self.account(req);
3670 let mut guard = self.state.write();
3671 let st = guard.get_or_create(&account);
3672 let t = st.templates.get_mut(name).ok_or_else(template_not_found)?;
3673 f(t)?;
3674 let t = t.clone();
3675 ok(json!({ "approvalRuleTemplate": t }))
3676 }
3677}
3678
3679#[cfg(test)]
3684mod normalize_tests {
3685 use crate::codecommit_helpers::normalize_rule_content;
3686
3687 #[test]
3688 fn compacts_and_preserves_member_order() {
3689 let input = " {\n\t \"Version\": \"2018-11-08\",\n\t \"DestinationReferences\": [\"refs/heads/master\"],\n\t \"Statements\": []\n }\n";
3690 let out = normalize_rule_content(input);
3691 assert_eq!(
3692 out,
3693 "{\"Version\":\"2018-11-08\",\"DestinationReferences\":[\"refs/heads/master\"],\"Statements\":[]}"
3694 );
3695 }
3696}
3697
3698#[cfg(test)]
3699mod engine_tests {
3700 use super::*;
3701
3702 fn entry(id: &str) -> FileEntry {
3703 FileEntry {
3704 blob_id: id.to_string(),
3705 mode: "100644".to_string(),
3706 }
3707 }
3708
3709 fn commit(parents: &[&str]) -> Value {
3710 json!({ "parents": parents, "treeId": "t" })
3711 }
3712
3713 #[test]
3715 fn merge_base_is_lowest_common_ancestor() {
3716 let mut repo = Repo::default();
3721 repo.commits.insert("root".into(), commit(&[]));
3722 repo.commits.insert("X".into(), commit(&["root"]));
3723 repo.commits.insert("A".into(), commit(&["X"]));
3724 repo.commits.insert("B".into(), commit(&["X", "root"]));
3725 assert_eq!(merge_base(&repo, "A", "B"), Some("X".to_string()));
3726 assert_eq!(merge_base(&repo, "B", "A"), Some("X".to_string()));
3727 }
3728
3729 #[test]
3730 fn merge_base_of_a_diamond_is_the_fork_point() {
3731 let mut repo = Repo::default();
3733 repo.commits.insert("root".into(), commit(&[]));
3734 repo.commits.insert("L".into(), commit(&["root"]));
3735 repo.commits.insert("R".into(), commit(&["root"]));
3736 repo.commits.insert("A".into(), commit(&["L"]));
3737 repo.commits.insert("B".into(), commit(&["R"]));
3738 assert_eq!(merge_base(&repo, "A", "B"), Some("root".to_string()));
3739 }
3740
3741 #[test]
3743 fn three_way_merge_propagates_source_deletion() {
3744 let mut repo = Repo::default();
3745 repo.commits.insert("base".into(), commit(&[]));
3746 repo.commits.insert("src".into(), commit(&["base"]));
3747 repo.commits.insert("dst".into(), commit(&["base"]));
3748 let mut base = Tree::new();
3750 base.insert("a.txt".into(), entry("a1"));
3751 base.insert("b.txt".into(), entry("b1"));
3752 repo.trees.insert("base".into(), base);
3753 let mut src = Tree::new();
3755 src.insert("a.txt".into(), entry("a1"));
3756 repo.trees.insert("src".into(), src);
3757 let mut dst = Tree::new();
3759 dst.insert("a.txt".into(), entry("a1"));
3760 dst.insert("b.txt".into(), entry("b1"));
3761 dst.insert("c.txt".into(), entry("c1"));
3762 repo.trees.insert("dst".into(), dst);
3763
3764 let merged = merge_trees(&repo, "src", "dst", "").expect("mergeable");
3765 assert!(!merged.contains_key("b.txt"), "source deletion must win");
3766 assert!(merged.contains_key("a.txt"));
3767 assert!(merged.contains_key("c.txt"));
3768 }
3769
3770 #[test]
3771 fn three_way_merge_flags_true_conflict() {
3772 let mut repo = Repo::default();
3773 repo.commits.insert("base".into(), commit(&[]));
3774 repo.commits.insert("src".into(), commit(&["base"]));
3775 repo.commits.insert("dst".into(), commit(&["base"]));
3776 let mut base = Tree::new();
3777 base.insert("a.txt".into(), entry("a1"));
3778 repo.trees.insert("base".into(), base);
3779 let mut src = Tree::new();
3780 src.insert("a.txt".into(), entry("a-src"));
3781 repo.trees.insert("src".into(), src);
3782 let mut dst = Tree::new();
3783 dst.insert("a.txt".into(), entry("a-dst"));
3784 repo.trees.insert("dst".into(), dst);
3785 assert!(merge_trees(&repo, "src", "dst", "").is_none());
3787 let empty = Tree::new();
3788 let (_, bt, st, dt) = merge_inputs(&repo, "src", "dst", &empty);
3789 assert_eq!(conflicting_paths_in(bt, st, dt), vec!["a.txt"]);
3790 let merged = merge_trees(&repo, "src", "dst", "ACCEPT_SOURCE").expect("resolved");
3792 assert_eq!(merged.get("a.txt").unwrap().blob_id, "a-src");
3793 }
3794}
3795
3796#[cfg(test)]
3797mod handler_tests {
3798 use super::*;
3799 use bytes::Bytes;
3800 use fakecloud_core::auth::{Principal, PrincipalType};
3801 use fakecloud_core::multi_account::MultiAccountState;
3802 use http::{HeaderMap, Method};
3803 use parking_lot::RwLock;
3804 use std::collections::HashMap;
3805
3806 fn svc() -> CodeCommitService {
3807 CodeCommitService::new(Arc::new(RwLock::new(MultiAccountState::new(
3808 "000000000000",
3809 "us-east-1",
3810 "",
3811 ))))
3812 }
3813
3814 fn req_as(action: &str, body: Value, arn: Option<&str>) -> AwsRequest {
3815 let principal = arn.map(|a| Principal {
3816 arn: a.to_string(),
3817 user_id: "AIDATEST".to_string(),
3818 account_id: "000000000000".to_string(),
3819 principal_type: PrincipalType::User,
3820 source_identity: None,
3821 tags: None,
3822 });
3823 AwsRequest {
3824 service: "codecommit".into(),
3825 action: action.into(),
3826 region: "us-east-1".into(),
3827 account_id: "000000000000".into(),
3828 request_id: "req".into(),
3829 headers: HeaderMap::new(),
3830 query_params: HashMap::new(),
3831 body: Bytes::from(serde_json::to_vec(&body).unwrap()),
3832 body_stream: parking_lot::Mutex::new(None),
3833 path_segments: vec![],
3834 raw_path: String::new(),
3835 raw_query: String::new(),
3836 method: Method::POST,
3837 is_query_protocol: false,
3838 access_key_id: None,
3839 principal,
3840 }
3841 }
3842
3843 fn call(s: &CodeCommitService, action: &str, body: Value) -> Value {
3844 let resp = s
3845 .dispatch(action, &req_as(action, body, None))
3846 .expect("op ok");
3847 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
3848 }
3849
3850 fn call_as(s: &CodeCommitService, action: &str, body: Value, arn: &str) -> Value {
3851 let resp = s
3852 .dispatch(action, &req_as(action, body, Some(arn)))
3853 .expect("op ok");
3854 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
3855 }
3856
3857 fn call_err(s: &CodeCommitService, action: &str, body: Value) -> AwsServiceError {
3858 s.dispatch(action, &req_as(action, body, None))
3859 .err()
3860 .expect("op err")
3861 }
3862
3863 fn b64(s: &str) -> String {
3864 base64::engine::general_purpose::STANDARD.encode(s.as_bytes())
3865 }
3866
3867 fn make_repo(s: &CodeCommitService) {
3869 call(s, "CreateRepository", json!({ "repositoryName": "repo" }));
3870 }
3871
3872 fn put(
3874 s: &CodeCommitService,
3875 branch: &str,
3876 path: &str,
3877 content: &str,
3878 parent: Option<&str>,
3879 ) -> String {
3880 let mut body = json!({
3881 "repositoryName": "repo",
3882 "branchName": branch,
3883 "filePath": path,
3884 "fileContent": b64(content),
3885 });
3886 if let Some(p) = parent {
3887 body["parentCommitId"] = json!(p);
3888 }
3889 call(s, "PutFile", body)["commitId"]
3890 .as_str()
3891 .unwrap()
3892 .to_string()
3893 }
3894
3895 fn diverged_repo(s: &CodeCommitService) -> (String, String) {
3898 make_repo(s);
3899 let c1 = put(s, "main", "a.txt", "base", None);
3900 call(
3901 s,
3902 "CreateBranch",
3903 json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c1 }),
3904 );
3905 let feature_tip = put(s, "feature", "b.txt", "from-feature", Some(&c1));
3906 let main_tip = put(s, "main", "c.txt", "from-main", Some(&c1));
3907 (feature_tip, main_tip)
3908 }
3909
3910 fn create_pr(s: &CodeCommitService, dest: &str) -> Value {
3911 call(
3912 s,
3913 "CreatePullRequest",
3914 json!({
3915 "title": "PR",
3916 "targets": [{
3917 "repositoryName": "repo",
3918 "sourceReference": "feature",
3919 "destinationReference": dest,
3920 }],
3921 }),
3922 )["pullRequest"]
3923 .clone()
3924 }
3925
3926 #[test]
3929 fn merge_pr_three_way_advances_branch_and_writes_commit() {
3930 let s = svc();
3931 diverged_repo(&s);
3932 let pr = create_pr(&s, "main");
3933 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
3934
3935 let out = call(
3936 &s,
3937 "MergePullRequestByThreeWay",
3938 json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
3939 );
3940 let target = &out["pullRequest"]["pullRequestTargets"][0];
3941 assert_eq!(target["mergeMetadata"]["isMerged"], json!(true));
3942 let merge_commit = target["mergeMetadata"]["mergeCommitId"]
3943 .as_str()
3944 .unwrap()
3945 .to_string();
3946 assert!(is_object_id(&merge_commit));
3947 assert_eq!(out["pullRequest"]["pullRequestStatus"], json!("CLOSED"));
3948
3949 let branch = call(
3951 &s,
3952 "GetBranch",
3953 json!({ "repositoryName": "repo", "branchName": "main" }),
3954 );
3955 assert_eq!(branch["branch"]["commitId"], json!(merge_commit));
3956
3957 let got = call(
3959 &s,
3960 "GetCommit",
3961 json!({ "repositoryName": "repo", "commitId": merge_commit }),
3962 );
3963 assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 2);
3964 }
3965
3966 #[test]
3969 fn merge_pr_squash_deletes_file_and_has_one_parent() {
3970 let s = svc();
3971 make_repo(&s);
3972 let c1 = put(&s, "main", "a.txt", "a", None);
3973 let c2 = put(&s, "main", "b.txt", "b", Some(&c1));
3974 call(
3975 &s,
3976 "CreateBranch",
3977 json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c2 }),
3978 );
3979 call(
3981 &s,
3982 "DeleteFile",
3983 json!({ "repositoryName": "repo", "branchName": "feature", "filePath": "b.txt", "parentCommitId": c2 }),
3984 );
3985 put(&s, "main", "c.txt", "c", Some(&c2));
3986
3987 let pr = create_pr(&s, "main");
3988 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
3989 let out = call(
3990 &s,
3991 "MergePullRequestBySquash",
3992 json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
3993 );
3994 let merge_commit = out["pullRequest"]["pullRequestTargets"][0]["mergeMetadata"]
3995 ["mergeCommitId"]
3996 .as_str()
3997 .unwrap()
3998 .to_string();
3999 let got = call(
4001 &s,
4002 "GetCommit",
4003 json!({ "repositoryName": "repo", "commitId": merge_commit }),
4004 );
4005 assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4006 let err = call_err(
4008 &s,
4009 "GetFile",
4010 json!({ "repositoryName": "repo", "commitSpecifier": "main", "filePath": "b.txt" }),
4011 );
4012 assert_eq!(err.code(), "FileDoesNotExistException");
4013 }
4014
4015 #[test]
4017 fn merge_branches_squash_single_parent() {
4018 let s = svc();
4019 let (feature_tip, main_tip) = diverged_repo(&s);
4020 let out = call(
4021 &s,
4022 "MergeBranchesBySquash",
4023 json!({
4024 "repositoryName": "repo",
4025 "sourceCommitSpecifier": feature_tip,
4026 "destinationCommitSpecifier": main_tip,
4027 "targetBranch": "main",
4028 }),
4029 );
4030 let cid = out["commitId"].as_str().unwrap().to_string();
4031 let got = call(
4032 &s,
4033 "GetCommit",
4034 json!({ "repositoryName": "repo", "commitId": cid }),
4035 );
4036 assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4037 }
4038
4039 #[test]
4042 fn template_applied_and_two_approvals_needed() {
4043 let s = svc();
4044 let content = "{\"Version\":\"2018-11-08\",\"Statements\":[{\"Type\":\"Approvers\",\"NumberOfApprovalsNeeded\":2,\"ApprovalPoolMembers\":[]}]}";
4045 call(
4046 &s,
4047 "CreateApprovalRuleTemplate",
4048 json!({ "approvalRuleTemplateName": "two", "approvalRuleTemplateContent": content }),
4049 );
4050 diverged_repo(&s);
4051 call(
4052 &s,
4053 "AssociateApprovalRuleTemplateWithRepository",
4054 json!({ "approvalRuleTemplateName": "two", "repositoryName": "repo" }),
4055 );
4056 let pr = create_pr(&s, "main");
4057 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4058 let revision = pr["revisionId"].as_str().unwrap().to_string();
4059
4060 let rules = pr["approvalRules"].as_array().unwrap();
4062 assert_eq!(rules.len(), 1);
4063 assert_eq!(rules[0]["approvalRuleName"], json!("two"));
4064 assert!(rules[0]["originApprovalRuleTemplate"].is_object());
4065
4066 call_as(
4068 &s,
4069 "UpdatePullRequestApprovalState",
4070 json!({ "pullRequestId": pr_id, "revisionId": revision, "approvalState": "APPROVE" }),
4071 "arn:aws:iam::000000000000:user/reviewer",
4072 );
4073 let eval = call(
4074 &s,
4075 "EvaluatePullRequestApprovalRules",
4076 json!({ "pullRequestId": pr_id, "revisionId": revision }),
4077 );
4078 assert_eq!(eval["evaluation"]["approved"], json!(false));
4079 assert_eq!(
4080 eval["evaluation"]["approvalRulesNotSatisfied"],
4081 json!(["two"])
4082 );
4083 }
4084
4085 #[test]
4087 fn closed_pr_cannot_reopen() {
4088 let s = svc();
4089 diverged_repo(&s);
4090 let pr = create_pr(&s, "main");
4091 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4092 call(
4093 &s,
4094 "UpdatePullRequestStatus",
4095 json!({ "pullRequestId": pr_id, "pullRequestStatus": "CLOSED" }),
4096 );
4097 let err = call_err(
4098 &s,
4099 "UpdatePullRequestStatus",
4100 json!({ "pullRequestId": pr_id, "pullRequestStatus": "OPEN" }),
4101 );
4102 assert_eq!(err.code(), "InvalidPullRequestStatusUpdateException");
4103 }
4104
4105 #[test]
4107 fn create_pr_rejects_missing_destination() {
4108 let s = svc();
4109 diverged_repo(&s);
4110 let err = call_err(
4111 &s,
4112 "CreatePullRequest",
4113 json!({
4114 "title": "PR",
4115 "targets": [{
4116 "repositoryName": "repo",
4117 "sourceReference": "feature",
4118 "destinationReference": "does-not-exist",
4119 }],
4120 }),
4121 );
4122 assert_eq!(err.code(), "ReferenceDoesNotExistException");
4123 }
4124
4125 #[test]
4127 fn describe_and_batch_report_conflict() {
4128 let s = svc();
4129 make_repo(&s);
4130 let c1 = put(&s, "main", "a.txt", "base-content", None);
4131 call(
4132 &s,
4133 "CreateBranch",
4134 json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c1 }),
4135 );
4136 put(&s, "feature", "a.txt", "feature-side", Some(&c1));
4137 put(&s, "main", "a.txt", "main-side", Some(&c1));
4138
4139 let base_body = json!({
4140 "repositoryName": "repo",
4141 "sourceCommitSpecifier": "feature",
4142 "destinationCommitSpecifier": "main",
4143 "mergeOption": "THREE_WAY_MERGE",
4144 });
4145
4146 let mut d = base_body.clone();
4147 d["filePath"] = json!("a.txt");
4148 let desc = call(&s, "DescribeMergeConflicts", d);
4149 assert_eq!(desc["conflictMetadata"]["numberOfConflicts"], json!(1));
4150 assert_eq!(desc["conflictMetadata"]["contentConflict"], json!(true));
4151 assert_eq!(desc["mergeHunks"][0]["isConflict"], json!(true));
4152
4153 let batch = call(&s, "BatchDescribeMergeConflicts", base_body.clone());
4154 let conflicts = batch["conflicts"].as_array().unwrap();
4155 assert_eq!(conflicts.len(), 1);
4156 assert_eq!(conflicts[0]["conflictMetadata"]["filePath"], json!("a.txt"));
4157
4158 let gmc = call(&s, "GetMergeConflicts", base_body);
4160 assert_eq!(gmc["mergeable"], json!(false));
4161 assert_eq!(gmc["conflictMetadataList"][0]["filePath"], json!("a.txt"));
4162 }
4163
4164 #[test]
4167 fn merge_pr_fast_forward_non_ff_rejected() {
4168 let s = svc();
4169 diverged_repo(&s);
4170 let pr = create_pr(&s, "main");
4171 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4172 let err = call_err(
4173 &s,
4174 "MergePullRequestByFastForward",
4175 json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
4176 );
4177 assert_eq!(err.code(), "ManualMergeRequiredException");
4178 let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4180 assert_eq!(got["pullRequest"]["pullRequestStatus"], json!("OPEN"));
4181 }
4182
4183 fn branch_tip(s: &CodeCommitService, branch: &str) -> String {
4184 call(
4185 s,
4186 "GetBranch",
4187 json!({ "repositoryName": "repo", "branchName": branch }),
4188 )["branch"]["commitId"]
4189 .as_str()
4190 .unwrap()
4191 .to_string()
4192 }
4193
4194 fn one_approval_rule() -> &'static str {
4195 "{\"Version\":\"2018-11-08\",\"Statements\":[{\"Type\":\"Approvers\",\"NumberOfApprovalsNeeded\":1,\"ApprovalPoolMembers\":[]}]}"
4196 }
4197
4198 #[test]
4201 fn new_source_commit_bumps_revision_and_invalidates_approvals() {
4202 let s = svc();
4203 let (feature_tip, _) = diverged_repo(&s);
4204 let pr = create_pr(&s, "main");
4205 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4206 let rev1 = pr["revisionId"].as_str().unwrap().to_string();
4207 call(
4208 &s,
4209 "CreatePullRequestApprovalRule",
4210 json!({
4211 "pullRequestId": pr_id,
4212 "approvalRuleName": "r",
4213 "approvalRuleContent": one_approval_rule(),
4214 }),
4215 );
4216 call_as(
4218 &s,
4219 "UpdatePullRequestApprovalState",
4220 json!({ "pullRequestId": pr_id, "revisionId": rev1, "approvalState": "APPROVE" }),
4221 "arn:aws:iam::000000000000:user/reviewer",
4222 );
4223 let eval1 = call(
4224 &s,
4225 "EvaluatePullRequestApprovalRules",
4226 json!({ "pullRequestId": pr_id, "revisionId": rev1 }),
4227 );
4228 assert_eq!(eval1["evaluation"]["approved"], json!(true));
4229
4230 put(&s, "feature", "d.txt", "more", Some(&feature_tip));
4232 let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4233 let rev2 = got["pullRequest"]["revisionId"]
4234 .as_str()
4235 .unwrap()
4236 .to_string();
4237 assert_ne!(rev1, rev2, "source advance must re-mint the revisionId");
4238
4239 let eval2 = call(
4241 &s,
4242 "EvaluatePullRequestApprovalRules",
4243 json!({ "pullRequestId": pr_id, "revisionId": rev2 }),
4244 );
4245 assert_eq!(eval2["evaluation"]["approved"], json!(false));
4246 }
4247
4248 #[test]
4250 fn get_pull_request_reflects_advanced_source_tip() {
4251 let s = svc();
4252 let (feature_tip, _) = diverged_repo(&s);
4253 let pr = create_pr(&s, "main");
4254 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4255 assert_eq!(
4256 pr["pullRequestTargets"][0]["sourceCommit"],
4257 json!(feature_tip)
4258 );
4259 put(&s, "feature", "e.txt", "x", Some(&feature_tip));
4260 let new_tip = branch_tip(&s, "feature");
4261 let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4262 assert_eq!(
4263 got["pullRequest"]["pullRequestTargets"][0]["sourceCommit"],
4264 json!(new_tip)
4265 );
4266 }
4267
4268 #[test]
4270 fn create_unreferenced_squash_single_parent() {
4271 let s = svc();
4272 diverged_repo(&s);
4273 let out = call(
4274 &s,
4275 "CreateUnreferencedMergeCommit",
4276 json!({
4277 "repositoryName": "repo",
4278 "sourceCommitSpecifier": "feature",
4279 "destinationCommitSpecifier": "main",
4280 "mergeOption": "SQUASH_MERGE",
4281 }),
4282 );
4283 let cid = out["commitId"].as_str().unwrap().to_string();
4284 let got = call(
4285 &s,
4286 "GetCommit",
4287 json!({ "repositoryName": "repo", "commitId": cid }),
4288 );
4289 assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4290 }
4291
4292 #[test]
4295 fn invalid_conflict_strategy_rejected() {
4296 let s = svc();
4297 diverged_repo(&s);
4298 let pr = create_pr(&s, "main");
4299 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4300 let e1 = call_err(
4301 &s,
4302 "MergePullRequestByThreeWay",
4303 json!({
4304 "pullRequestId": pr_id,
4305 "repositoryName": "repo",
4306 "conflictResolutionStrategy": "BOGUS",
4307 }),
4308 );
4309 assert_eq!(e1.code(), "InvalidConflictResolutionStrategyException");
4310 let e2 = call_err(
4311 &s,
4312 "CreateUnreferencedMergeCommit",
4313 json!({
4314 "repositoryName": "repo",
4315 "sourceCommitSpecifier": "feature",
4316 "destinationCommitSpecifier": "main",
4317 "mergeOption": "THREE_WAY_MERGE",
4318 "conflictResolutionStrategy": "BOGUS",
4319 }),
4320 );
4321 assert_eq!(e2.code(), "InvalidConflictResolutionStrategyException");
4322 }
4323
4324 #[test]
4326 fn open_to_open_status_rejected() {
4327 let s = svc();
4328 diverged_repo(&s);
4329 let pr = create_pr(&s, "main");
4330 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4331 let err = call_err(
4332 &s,
4333 "UpdatePullRequestStatus",
4334 json!({ "pullRequestId": pr_id, "pullRequestStatus": "OPEN" }),
4335 );
4336 assert_eq!(err.code(), "InvalidPullRequestStatusUpdateException");
4337 }
4338
4339 #[test]
4342 fn test_triggers_reports_unresolvable_destination() {
4343 let s = svc();
4344 make_repo(&s);
4345 let out = call(
4346 &s,
4347 "TestRepositoryTriggers",
4348 json!({
4349 "repositoryName": "repo",
4350 "triggers": [
4351 {
4352 "name": "ok",
4353 "destinationArn": "arn:aws:sns:us-east-1:000000000000:topic",
4354 "events": ["all"],
4355 },
4356 {
4357 "name": "bad",
4358 "destinationArn": "arn:aws:s3:::some-bucket",
4359 "events": ["all"],
4360 },
4361 ],
4362 }),
4363 );
4364 assert_eq!(out["successfulExecutions"], json!(["ok"]));
4365 let failed = out["failedExecutions"].as_array().unwrap();
4366 assert_eq!(failed.len(), 1);
4367 assert_eq!(failed[0]["trigger"], json!("bad"));
4368 assert!(failed[0]["failureMessage"].as_str().unwrap().contains("s3"));
4369 }
4370
4371 #[test]
4373 fn delete_repository_clears_template_association() {
4374 let s = svc();
4375 make_repo(&s);
4376 call(
4377 &s,
4378 "CreateApprovalRuleTemplate",
4379 json!({
4380 "approvalRuleTemplateName": "t",
4381 "approvalRuleTemplateContent": one_approval_rule(),
4382 }),
4383 );
4384 call(
4385 &s,
4386 "AssociateApprovalRuleTemplateWithRepository",
4387 json!({ "approvalRuleTemplateName": "t", "repositoryName": "repo" }),
4388 );
4389 let before = call(
4390 &s,
4391 "ListRepositoriesForApprovalRuleTemplate",
4392 json!({ "approvalRuleTemplateName": "t" }),
4393 );
4394 assert_eq!(before["repositoryNames"], json!(["repo"]));
4395 call(&s, "DeleteRepository", json!({ "repositoryName": "repo" }));
4396 let after = call(
4397 &s,
4398 "ListRepositoriesForApprovalRuleTemplate",
4399 json!({ "approvalRuleTemplateName": "t" }),
4400 );
4401 assert_eq!(after["repositoryNames"], json!([]));
4402 }
4403}