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")?.to_string();
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 blob_at = |cid: &str| -> Option<String> {
1311 repo.trees
1312 .get(cid)
1313 .and_then(|t| t.get(&path))
1314 .map(|e| e.blob_id.clone())
1315 };
1316 let mut dag = Vec::new();
1321 let mut cur = Some(commit_id);
1322 while let Some(cid) = cur {
1323 let Some(c) = repo.commits.get(&cid) else {
1324 break;
1325 };
1326 let parents = c
1327 .get("parents")
1328 .and_then(Value::as_array)
1329 .cloned()
1330 .unwrap_or_default();
1331 let first_parent = parents.first().and_then(Value::as_str);
1332 let cur_blob = blob_at(&cid);
1333 let parent_blob = first_parent.and_then(blob_at);
1334 let touched = match first_parent {
1335 None => cur_blob.is_some(),
1337 Some(_) => cur_blob != parent_blob,
1339 };
1340 if touched {
1341 dag.push(json!({
1342 "commit": cid,
1343 "parents": parents.iter().filter_map(|p| p.as_str()).collect::<Vec<_>>(),
1344 }));
1345 }
1346 cur = first_parent.map(str::to_string);
1347 }
1348 ok(json!({ "revisionDag": dag }))
1349 }
1350
1351 fn merge_branches_fast_forward(
1354 &self,
1355 req: &AwsRequest,
1356 ) -> Result<AwsResponse, AwsServiceError> {
1357 let b = body(req);
1358 let name = require_repository_name(&b)?;
1359 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1360 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1361 let account = self.account(req);
1362 let mut guard = self.state.write();
1363 let st = guard.get_or_create(&account);
1364 let repo = st
1365 .repositories
1366 .get_mut(&name)
1367 .ok_or_else(|| repo_not_found(&name))?;
1368 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1369 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1370 if !is_ancestor(repo, &dest, &source) {
1371 return Err(err(
1372 "ManualMergeRequiredException",
1373 "The fast-forward merge cannot be performed because the destination is not an ancestor of the source.",
1374 ));
1375 }
1376 let tree_id = repo
1377 .commits
1378 .get(&source)
1379 .and_then(|c| c.get("treeId"))
1380 .and_then(Value::as_str)
1381 .unwrap_or_default()
1382 .to_string();
1383 if let Some(target) = str_field(&b, "targetBranch") {
1384 repo.branches.insert(target.clone(), source.clone());
1385 refresh_source_revisions(st, &name, &target);
1386 }
1387 ok(json!({ "commitId": source, "treeId": tree_id }))
1388 }
1389
1390 fn merge_branches_squash(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1391 self.merge_branches_create(req, "SQUASH_MERGE")
1392 }
1393
1394 fn merge_branches_three_way(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1395 self.merge_branches_create(req, "THREE_WAY_MERGE")
1396 }
1397
1398 fn merge_branches_create(
1399 &self,
1400 req: &AwsRequest,
1401 merge_option: &str,
1402 ) -> Result<AwsResponse, AwsServiceError> {
1403 let b = body(req);
1404 let name = require_repository_name(&b)?;
1405 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1406 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1407 validate_merge_conflict_enums(&b)?;
1408 let account = self.account(req);
1409 let mut guard = self.state.write();
1410 let st = guard.get_or_create(&account);
1411 let repo = st
1412 .repositories
1413 .get_mut(&name)
1414 .ok_or_else(|| repo_not_found(&name))?;
1415 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1416 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1417 let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
1418 let merged = merge_trees(repo, &source, &dest, &strategy).ok_or_else(|| {
1419 err(
1420 "ManualMergeRequiredException",
1421 "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
1422 )
1423 })?;
1424 let tree_id = tree_id_for(&merged);
1425 let commit_id = fresh_object_id();
1426 let parents = merge_parents(merge_option, &dest, &source);
1427 let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
1428 repo.commits.insert(commit_id.clone(), commit);
1429 repo.trees.insert(commit_id.clone(), merged);
1430 if let Some(target) = str_field(&b, "targetBranch") {
1431 repo.branches.insert(target.clone(), commit_id.clone());
1432 refresh_source_revisions(st, &name, &target);
1433 }
1434 ok(json!({ "commitId": commit_id, "treeId": tree_id }))
1435 }
1436
1437 fn create_unreferenced_merge_commit(
1438 &self,
1439 req: &AwsRequest,
1440 ) -> Result<AwsResponse, AwsServiceError> {
1441 let b = body(req);
1442 let name = require_repository_name(&b)?;
1443 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1444 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1445 let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1446 if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1447 return Err(err(
1448 "InvalidMergeOptionException",
1449 "The merge option is not valid.",
1450 ));
1451 }
1452 validate_merge_conflict_enums(&b)?;
1453 let account = self.account(req);
1454 let mut guard = self.state.write();
1455 let st = guard.get_or_create(&account);
1456 let repo = st
1457 .repositories
1458 .get_mut(&name)
1459 .ok_or_else(|| repo_not_found(&name))?;
1460 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1461 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1462 let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
1463 let merged = merge_trees(repo, &source, &dest, &strategy).ok_or_else(|| {
1464 err(
1465 "ManualMergeRequiredException",
1466 "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
1467 )
1468 })?;
1469 let tree_id = tree_id_for(&merged);
1470 let commit_id = fresh_object_id();
1471 let parents = merge_parents(&merge_option, &dest, &source);
1472 let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
1473 repo.commits.insert(commit_id.clone(), commit);
1474 repo.trees.insert(commit_id.clone(), merged);
1475 ok(json!({ "commitId": commit_id, "treeId": tree_id }))
1476 }
1477
1478 fn get_merge_commit(&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 ok(json!({
1493 "sourceCommitId": source,
1494 "destinationCommitId": dest,
1495 "baseCommitId": base,
1496 }))
1497 }
1498
1499 fn get_merge_options(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1500 let b = body(req);
1501 let name = require_repository_name(&b)?;
1502 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1503 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1504 let account = self.account(req);
1505 let guard = self.state.read();
1506 let st = guard.get(&account);
1507 let repo = st
1508 .and_then(|s| s.repositories.get(&name))
1509 .ok_or_else(|| repo_not_found(&name))?;
1510 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1511 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1512 let base = merge_base(repo, &source, &dest);
1513 let mut options = vec![json!("SQUASH_MERGE"), json!("THREE_WAY_MERGE")];
1514 if is_ancestor(repo, &dest, &source) {
1515 options.insert(0, json!("FAST_FORWARD_MERGE"));
1516 }
1517 ok(json!({
1518 "mergeOptions": options,
1519 "sourceCommitId": source,
1520 "destinationCommitId": dest,
1521 "baseCommitId": base,
1522 }))
1523 }
1524
1525 fn get_merge_conflicts(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1526 let b = body(req);
1527 let name = require_repository_name(&b)?;
1528 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1529 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1530 let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1531 if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1532 return Err(err(
1533 "InvalidMergeOptionException",
1534 "The merge option is not valid.",
1535 ));
1536 }
1537 let account = self.account(req);
1538 let guard = self.state.read();
1539 let st = guard.get(&account);
1540 let repo = st
1541 .and_then(|s| s.repositories.get(&name))
1542 .ok_or_else(|| repo_not_found(&name))?;
1543 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1544 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1545 let empty = std::collections::BTreeMap::new();
1546 let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1547 let conflicts = conflicting_paths_in(base_tree, src_tree, dst_tree);
1548 let mergeable = conflicts.is_empty();
1549 let metadata: Vec<Value> = conflicts
1550 .iter()
1551 .map(|p| conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, p).0)
1552 .collect();
1553 ok(json!({
1554 "mergeable": mergeable,
1555 "destinationCommitId": dest,
1556 "sourceCommitId": source,
1557 "baseCommitId": base,
1558 "conflictMetadataList": metadata,
1559 }))
1560 }
1561
1562 fn describe_merge_conflicts(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1563 let b = body(req);
1564 let name = require_repository_name(&b)?;
1565 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1566 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1567 let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1568 if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1569 return Err(err(
1570 "InvalidMergeOptionException",
1571 "The merge option is not valid.",
1572 ));
1573 }
1574 let path = require(&b, "filePath", "PathRequiredException")?;
1575 let account = self.account(req);
1576 let guard = self.state.read();
1577 let st = guard.get(&account);
1578 let repo = st
1579 .and_then(|s| s.repositories.get(&name))
1580 .ok_or_else(|| repo_not_found(&name))?;
1581 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1582 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1583 let empty = std::collections::BTreeMap::new();
1584 let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1585 if base_tree.get(&path).is_none()
1586 && src_tree.get(&path).is_none()
1587 && dst_tree.get(&path).is_none()
1588 {
1589 return Err(err(
1590 "FileDoesNotExistException",
1591 "The specified file does not exist.",
1592 ));
1593 }
1594 let (metadata, hunks) =
1595 conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, &path);
1596 ok(json!({
1597 "conflictMetadata": metadata,
1598 "mergeHunks": hunks,
1599 "destinationCommitId": dest,
1600 "sourceCommitId": source,
1601 "baseCommitId": base,
1602 }))
1603 }
1604
1605 fn batch_describe_merge_conflicts(
1606 &self,
1607 req: &AwsRequest,
1608 ) -> Result<AwsResponse, AwsServiceError> {
1609 let b = body(req);
1610 let name = require_repository_name(&b)?;
1611 require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1612 require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1613 let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1614 if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1615 return Err(err(
1616 "InvalidMergeOptionException",
1617 "The merge option is not valid.",
1618 ));
1619 }
1620 let account = self.account(req);
1621 let guard = self.state.read();
1622 let st = guard.get(&account);
1623 let repo = st
1624 .and_then(|s| s.repositories.get(&name))
1625 .ok_or_else(|| repo_not_found(&name))?;
1626 let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1627 let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1628 let max_files = b
1629 .get("maxConflictFiles")
1630 .and_then(Value::as_i64)
1631 .filter(|n| *n >= 0)
1632 .map_or(usize::MAX, |n| n as usize);
1633 let max_hunks = b
1634 .get("maxMergeHunks")
1635 .and_then(Value::as_i64)
1636 .filter(|n| *n >= 0)
1637 .map_or(usize::MAX, |n| n as usize);
1638 let empty = std::collections::BTreeMap::new();
1639 let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1640 let mut conflicts = Vec::new();
1641 for path in conflicting_paths_in(base_tree, src_tree, dst_tree) {
1642 if conflicts.len() >= max_files {
1643 break;
1644 }
1645 let (metadata, mut hunks) =
1646 conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, &path);
1647 hunks.truncate(max_hunks);
1648 conflicts.push(json!({
1649 "conflictMetadata": metadata,
1650 "mergeHunks": hunks,
1651 }));
1652 }
1653 ok(json!({
1654 "conflicts": conflicts,
1655 "errors": [],
1656 "destinationCommitId": dest,
1657 "sourceCommitId": source,
1658 "baseCommitId": base,
1659 }))
1660 }
1661
1662 fn create_pull_request(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1665 let b = body(req);
1666 let title = match str_field(&b, "title") {
1667 Some(t) => t,
1668 None => {
1669 return Err(err(
1670 "TitleRequiredException",
1671 "A pull request title is required.",
1672 ))
1673 }
1674 };
1675 if title.chars().count() > 150 {
1676 return Err(err("InvalidTitleException", "The title is not valid."));
1677 }
1678 let targets = b
1679 .get("targets")
1680 .and_then(Value::as_array)
1681 .filter(|t| !t.is_empty())
1682 .ok_or_else(|| {
1683 err(
1684 "TargetsRequiredException",
1685 "Pull request targets are required.",
1686 )
1687 })?
1688 .clone();
1689 if let Some(d) = str_field(&b, "description") {
1690 if d.chars().count() > 10240 {
1691 return Err(err(
1692 "InvalidDescriptionException",
1693 "The description is not valid.",
1694 ));
1695 }
1696 }
1697 let account = self.account(req);
1698 let author = caller_arn(req);
1699 let mut guard = self.state.write();
1700 let st = guard.get_or_create(&account);
1701 let now = Utc::now();
1702 let mut pr_targets = Vec::new();
1703 let mut approval_rules = Vec::new();
1704 for t in &targets {
1705 let repo_name = t
1706 .get("repositoryName")
1707 .and_then(Value::as_str)
1708 .ok_or_else(|| {
1709 err(
1710 "RepositoryNameRequiredException",
1711 "A repository name is required.",
1712 )
1713 })?;
1714 let repo = st
1715 .repositories
1716 .get(repo_name)
1717 .ok_or_else(|| repo_not_found(repo_name))?;
1718 let source_ref = t
1719 .get("sourceReference")
1720 .and_then(Value::as_str)
1721 .ok_or_else(|| {
1722 err(
1723 "ReferenceNameRequiredException",
1724 "A source reference is required.",
1725 )
1726 })?;
1727 let dest_ref = t
1728 .get("destinationReference")
1729 .and_then(Value::as_str)
1730 .map(str::to_string)
1731 .or_else(|| {
1732 repo.metadata
1733 .get("defaultBranch")
1734 .and_then(Value::as_str)
1735 .map(str::to_string)
1736 })
1737 .unwrap_or_else(|| "main".to_string());
1738 if source_ref == dest_ref {
1739 return Err(err(
1740 "SourceAndDestinationAreSameException",
1741 "The source reference and destination reference are the same. You must specify different references for the source and destination.",
1742 ));
1743 }
1744 let source_commit = repo.branches.get(source_ref).cloned().ok_or_else(|| {
1745 err(
1746 "ReferenceDoesNotExistException",
1747 "The specified reference does not exist.",
1748 )
1749 })?;
1750 let dest_commit = repo.branches.get(&dest_ref).cloned().ok_or_else(|| {
1752 err(
1753 "ReferenceDoesNotExistException",
1754 "The specified reference does not exist.",
1755 )
1756 })?;
1757 let base = merge_base(repo, &source_commit, &dest_commit);
1758 pr_targets.push(json!({
1759 "repositoryName": repo_name,
1760 "sourceReference": source_ref,
1761 "destinationReference": dest_ref,
1762 "sourceCommit": source_commit,
1763 "destinationCommit": dest_commit,
1764 "mergeBase": base,
1765 "mergeMetadata": { "isMerged": false },
1766 }));
1767 for tmpl_name in &repo.associated_templates {
1770 if approval_rules.iter().any(|r: &Value| {
1771 r.get("approvalRuleName").and_then(Value::as_str) == Some(tmpl_name.as_str())
1772 }) {
1773 continue;
1774 }
1775 if let Some(tmpl) = st.templates.get(tmpl_name) {
1776 let content = tmpl
1777 .get("approvalRuleTemplateContent")
1778 .and_then(Value::as_str)
1779 .unwrap_or_default()
1780 .to_string();
1781 approval_rules.push(json!({
1782 "approvalRuleId": new_uuid(),
1783 "approvalRuleName": tmpl_name,
1784 "approvalRuleContent": content,
1785 "ruleContentSha256": sha256_hex(content.as_bytes()),
1786 "lastModifiedDate": ts(now),
1787 "creationDate": ts(now),
1788 "lastModifiedUser": author,
1789 "originApprovalRuleTemplate": {
1790 "approvalRuleTemplateId": tmpl
1791 .get("approvalRuleTemplateId")
1792 .cloned()
1793 .unwrap_or(Value::Null),
1794 "approvalRuleTemplateName": tmpl_name,
1795 },
1796 }));
1797 }
1798 }
1799 }
1800 st.pull_request_counter += 1;
1801 let pr_id = st.pull_request_counter.to_string();
1802 let revision_id = fresh_object_id();
1803 let pr = json!({
1804 "pullRequestId": pr_id,
1805 "title": title,
1806 "description": str_field(&b, "description").unwrap_or_default(),
1807 "lastActivityDate": ts(now),
1808 "creationDate": ts(now),
1809 "pullRequestStatus": "OPEN",
1810 "authorArn": author,
1811 "pullRequestTargets": pr_targets,
1812 "clientRequestToken": str_field(&b, "clientRequestToken").unwrap_or_default(),
1813 "revisionId": revision_id,
1814 "approvalRules": approval_rules,
1815 });
1816 st.pull_requests.insert(pr_id.clone(), pr.clone());
1817 st.pull_request_order.push(pr_id.clone());
1818 st.pull_request_events.insert(
1819 pr_id.clone(),
1820 vec![pr_event(
1821 &pr_id,
1822 "PULL_REQUEST_CREATED",
1823 &author,
1824 now,
1825 json!({}),
1826 )],
1827 );
1828 ok(json!({ "pullRequest": pr }))
1829 }
1830
1831 fn get_pull_request(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1832 let b = body(req);
1833 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1834 let account = self.account(req);
1835 let guard = self.state.read();
1836 let st = guard.get(&account);
1837 match st.and_then(|s| s.pull_requests.get(&id).map(|pr| refresh_pr_targets(s, pr))) {
1840 Some(pr) => ok(json!({ "pullRequest": pr })),
1841 None => Err(pr_not_found()),
1842 }
1843 }
1844
1845 fn list_pull_requests(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1846 let b = body(req);
1847 let name = require_repository_name(&b)?;
1848 check_enum(
1849 &b,
1850 "pullRequestStatus",
1851 validate::PULL_REQUEST_STATUS,
1852 "InvalidPullRequestStatusException",
1853 )?;
1854 let author_filter = str_field(&b, "authorArn");
1855 let status_filter = str_field(&b, "pullRequestStatus");
1856 let account = self.account(req);
1857 let guard = self.state.read();
1858 let st = guard.get(&account);
1859 let s = st.ok_or_else(|| repo_not_found(&name))?;
1860 if !s.repositories.contains_key(&name) {
1861 return Err(repo_not_found(&name));
1862 }
1863 let mut ids = Vec::new();
1864 for pid in &s.pull_request_order {
1865 let Some(pr) = s.pull_requests.get(pid) else {
1866 continue;
1867 };
1868 let in_repo = pr
1869 .get("pullRequestTargets")
1870 .and_then(Value::as_array)
1871 .map(|ts| {
1872 ts.iter().any(|t| {
1873 t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str())
1874 })
1875 })
1876 .unwrap_or(false);
1877 if !in_repo {
1878 continue;
1879 }
1880 if let Some(a) = &author_filter {
1881 if pr.get("authorArn").and_then(Value::as_str) != Some(a.as_str()) {
1882 continue;
1883 }
1884 }
1885 if let Some(s2) = &status_filter {
1886 if pr.get("pullRequestStatus").and_then(Value::as_str) != Some(s2.as_str()) {
1887 continue;
1888 }
1889 }
1890 ids.push(json!(pid));
1891 }
1892 ok(json!({ "pullRequestIds": ids }))
1893 }
1894
1895 fn update_pull_request_title(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1896 let b = body(req);
1897 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1898 let title = match str_field(&b, "title") {
1899 Some(t) => t,
1900 None => {
1901 return Err(err(
1902 "TitleRequiredException",
1903 "A pull request title is required.",
1904 ))
1905 }
1906 };
1907 if title.chars().count() > 150 {
1908 return Err(err("InvalidTitleException", "The title is not valid."));
1909 }
1910 self.mutate_pr(req, &id, |pr| {
1911 pr["title"] = json!(title);
1912 pr["lastActivityDate"] = ts(Utc::now());
1913 Ok(())
1914 })
1915 }
1916
1917 fn update_pull_request_description(
1918 &self,
1919 req: &AwsRequest,
1920 ) -> Result<AwsResponse, AwsServiceError> {
1921 let b = body(req);
1922 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1923 let desc = str_field(&b, "description").unwrap_or_default();
1924 if desc.chars().count() > 10240 {
1925 return Err(err(
1926 "InvalidDescriptionException",
1927 "The description is not valid.",
1928 ));
1929 }
1930 self.mutate_pr(req, &id, |pr| {
1931 pr["description"] = json!(desc);
1932 pr["lastActivityDate"] = ts(Utc::now());
1933 Ok(())
1934 })
1935 }
1936
1937 fn update_pull_request_status(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1938 let b = body(req);
1939 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1940 let status = require(
1941 &b,
1942 "pullRequestStatus",
1943 "PullRequestStatusRequiredException",
1944 )?;
1945 if !validate::is_enum(validate::PULL_REQUEST_STATUS, &status) {
1946 return Err(err(
1947 "InvalidPullRequestStatusException",
1948 "The pull request status is not valid.",
1949 ));
1950 }
1951 self.mutate_pr(req, &id, |pr| {
1952 let current = pr.get("pullRequestStatus").and_then(Value::as_str).unwrap_or("OPEN");
1953 if !(current == "OPEN" && status == "CLOSED") {
1957 return Err(err(
1958 "InvalidPullRequestStatusUpdateException",
1959 "The pull request status update is not valid. The only valid update is from OPEN to CLOSED.",
1960 ));
1961 }
1962 pr["pullRequestStatus"] = json!(status);
1963 pr["lastActivityDate"] = ts(Utc::now());
1964 Ok(())
1965 })
1966 }
1967
1968 fn describe_pull_request_events(
1969 &self,
1970 req: &AwsRequest,
1971 ) -> Result<AwsResponse, AwsServiceError> {
1972 let b = body(req);
1973 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1974 check_enum(
1975 &b,
1976 "pullRequestEventType",
1977 validate::PULL_REQUEST_EVENT_TYPE,
1978 "InvalidPullRequestEventTypeException",
1979 )?;
1980 let account = self.account(req);
1981 let guard = self.state.read();
1982 let st = guard.get(&account);
1983 let s = st.ok_or_else(pr_not_found)?;
1984 if !s.pull_requests.contains_key(&id) {
1985 return Err(pr_not_found());
1986 }
1987 let events = s.pull_request_events.get(&id).cloned().unwrap_or_default();
1988 ok(json!({ "pullRequestEvents": events }))
1989 }
1990
1991 fn merge_pull_request_fast_forward(
1992 &self,
1993 req: &AwsRequest,
1994 ) -> Result<AwsResponse, AwsServiceError> {
1995 self.merge_pull_request(req, "FAST_FORWARD_MERGE")
1996 }
1997
1998 fn merge_pull_request_squash(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1999 self.merge_pull_request(req, "SQUASH_MERGE")
2000 }
2001
2002 fn merge_pull_request_three_way(
2003 &self,
2004 req: &AwsRequest,
2005 ) -> Result<AwsResponse, AwsServiceError> {
2006 self.merge_pull_request(req, "THREE_WAY_MERGE")
2007 }
2008
2009 fn merge_pull_request(
2010 &self,
2011 req: &AwsRequest,
2012 merge_option: &str,
2013 ) -> Result<AwsResponse, AwsServiceError> {
2014 let b = body(req);
2015 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2016 let name = require_repository_name(&b)?;
2017 validate_merge_conflict_enums(&b)?;
2018 let author = caller_arn(req);
2019 let account = self.account(req);
2020 let mut guard = self.state.write();
2021 let st = guard.get_or_create(&account);
2022 if !st.repositories.contains_key(&name) {
2023 return Err(repo_not_found(&name));
2024 }
2025 let now = Utc::now();
2026
2027 let (source_ref, dest_ref, revision, rules) = {
2030 let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2031 if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2032 return Err(err(
2033 "PullRequestAlreadyClosedException",
2034 "The pull request status cannot be updated because it is already closed.",
2035 ));
2036 }
2037 let target = pr
2038 .get("pullRequestTargets")
2039 .and_then(Value::as_array)
2040 .and_then(|ts| {
2041 ts.iter().find(|t| {
2042 t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str())
2043 })
2044 })
2045 .ok_or_else(|| {
2046 err(
2047 "RepositoryNotAssociatedWithPullRequestException",
2048 "The repository is not associated with the pull request.",
2049 )
2050 })?;
2051 let source_ref = target
2052 .get("sourceReference")
2053 .and_then(Value::as_str)
2054 .unwrap_or_default()
2055 .to_string();
2056 let dest_ref = target
2057 .get("destinationReference")
2058 .and_then(Value::as_str)
2059 .unwrap_or_default()
2060 .to_string();
2061 let revision = pr
2062 .get("revisionId")
2063 .and_then(Value::as_str)
2064 .unwrap_or_default()
2065 .to_string();
2066 let rules = pr
2067 .get("approvalRules")
2068 .and_then(Value::as_array)
2069 .cloned()
2070 .unwrap_or_default();
2071 (source_ref, dest_ref, revision, rules)
2072 };
2073
2074 let overridden = st
2077 .pull_request_overrides
2078 .get(&id)
2079 .and_then(|m| m.get(&revision))
2080 .is_some();
2081 if !overridden && !rules.is_empty() {
2082 let approvers: std::collections::BTreeSet<String> = st
2083 .pull_request_approvals
2084 .get(&id)
2085 .and_then(|m| m.get(&revision))
2086 .map(|m| {
2087 m.iter()
2088 .filter(|(_, v)| v.as_str() == "APPROVE")
2089 .map(|(arn, _)| arn.clone())
2090 .collect()
2091 })
2092 .unwrap_or_default();
2093 let all_satisfied = rules.iter().all(|r| {
2094 let content = r
2095 .get("approvalRuleContent")
2096 .and_then(Value::as_str)
2097 .unwrap_or_default();
2098 rule_satisfied(content, &approvers)
2099 });
2100 if !all_satisfied {
2101 return Err(err(
2102 "PullRequestApprovalRulesNotSatisfiedException",
2103 "The pull request cannot be merged because one or more approval rules have not been satisfied.",
2104 ));
2105 }
2106 }
2107
2108 let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
2111 let repo = st.repositories.get_mut(&name).expect("repo checked above");
2112 let source_tip = repo.branches.get(&source_ref).cloned().ok_or_else(|| {
2113 err(
2114 "ReferenceDoesNotExistException",
2115 "The specified reference does not exist.",
2116 )
2117 })?;
2118 let dest_tip = repo.branches.get(&dest_ref).cloned().ok_or_else(|| {
2119 err(
2120 "ReferenceDoesNotExistException",
2121 "The specified reference does not exist.",
2122 )
2123 })?;
2124 if let Some(pinned) = str_field(&b, "sourceCommitId") {
2126 if pinned != source_tip {
2127 return Err(err(
2128 "TipOfSourceReferenceIsDifferentException",
2129 "The tip of the source reference is different from the specified commit ID.",
2130 ));
2131 }
2132 }
2133 let merge_commit_id = match merge_option {
2134 "FAST_FORWARD_MERGE" => {
2135 if !is_ancestor(repo, &dest_tip, &source_tip) {
2136 return Err(err(
2137 "ManualMergeRequiredException",
2138 "The fast-forward merge cannot be performed because the destination is not an ancestor of the source.",
2139 ));
2140 }
2141 repo.branches.insert(dest_ref.clone(), source_tip.clone());
2142 source_tip.clone()
2143 }
2144 _ => {
2145 let merged =
2146 merge_trees(repo, &source_tip, &dest_tip, &strategy).ok_or_else(|| {
2147 err(
2148 "ManualMergeRequiredException",
2149 "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
2150 )
2151 })?;
2152 let tree_id = tree_id_for(&merged);
2153 let commit_id = fresh_object_id();
2154 let parents = merge_parents(merge_option, &dest_tip, &source_tip);
2155 let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
2156 repo.commits.insert(commit_id.clone(), commit);
2157 repo.trees.insert(commit_id.clone(), merged);
2158 repo.branches.insert(dest_ref.clone(), commit_id.clone());
2159 commit_id
2160 }
2161 };
2162
2163 refresh_source_revisions(st, &name, &dest_ref);
2166
2167 let pr = st.pull_requests.get_mut(&id).expect("pr checked above");
2169 pr["pullRequestStatus"] = json!("CLOSED");
2170 pr["lastActivityDate"] = ts(now);
2171 if let Some(targets) = pr
2172 .get_mut("pullRequestTargets")
2173 .and_then(Value::as_array_mut)
2174 {
2175 for t in targets.iter_mut() {
2176 if t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str()) {
2177 t["mergeMetadata"] = json!({
2178 "isMerged": true,
2179 "mergedBy": author,
2180 "mergeCommitId": merge_commit_id,
2181 "mergeOption": merge_option,
2182 });
2183 }
2184 }
2185 }
2186 let pr = pr.clone();
2187 st.pull_request_events
2188 .entry(id.clone())
2189 .or_default()
2190 .push(pr_event(
2191 &id,
2192 "PULL_REQUEST_MERGE_STATE_CHANGED",
2193 &author,
2194 now,
2195 json!({}),
2196 ));
2197 ok(json!({ "pullRequest": pr }))
2198 }
2199
2200 fn create_pull_request_approval_rule(
2203 &self,
2204 req: &AwsRequest,
2205 ) -> Result<AwsResponse, AwsServiceError> {
2206 let b = body(req);
2207 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2208 let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2209 let content = require(
2210 &b,
2211 "approvalRuleContent",
2212 "ApprovalRuleContentRequiredException",
2213 )?;
2214 if rule_name.chars().count() > 100 {
2215 return Err(err(
2216 "InvalidApprovalRuleNameException",
2217 "The approval rule name is not valid.",
2218 ));
2219 }
2220 if content.chars().count() > 3000 {
2221 return Err(err(
2222 "InvalidApprovalRuleContentException",
2223 "The approval rule content is not valid.",
2224 ));
2225 }
2226 let content = normalize_rule_content(&content);
2227 let now = Utc::now();
2228 let user = caller_arn(req);
2229 let account = self.account(req);
2230 let mut guard = self.state.write();
2231 let st = guard.get_or_create(&account);
2232 let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2233 if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2234 return Err(err(
2235 "PullRequestAlreadyClosedException",
2236 "The pull request is already closed.",
2237 ));
2238 }
2239 let rules = pr
2240 .get_mut("approvalRules")
2241 .and_then(Value::as_array_mut)
2242 .ok_or_else(pr_not_found)?;
2243 if rules
2244 .iter()
2245 .any(|r| r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str()))
2246 {
2247 return Err(err(
2248 "ApprovalRuleNameAlreadyExistsException",
2249 "An approval rule with that name already exists.",
2250 ));
2251 }
2252 let rule = json!({
2253 "approvalRuleId": new_uuid(),
2254 "approvalRuleName": rule_name,
2255 "approvalRuleContent": content,
2256 "ruleContentSha256": sha256_hex(content.as_bytes()),
2257 "lastModifiedDate": ts(now),
2258 "creationDate": ts(now),
2259 "lastModifiedUser": user,
2260 });
2261 rules.push(rule.clone());
2262 ok(json!({ "approvalRule": rule }))
2263 }
2264
2265 fn delete_pull_request_approval_rule(
2266 &self,
2267 req: &AwsRequest,
2268 ) -> Result<AwsResponse, AwsServiceError> {
2269 let b = body(req);
2270 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2271 let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2272 let account = self.account(req);
2273 let mut guard = self.state.write();
2274 let st = guard.get_or_create(&account);
2275 let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2276 let mut deleted_id = String::new();
2277 if let Some(rules) = pr.get_mut("approvalRules").and_then(Value::as_array_mut) {
2278 if let Some(pos) = rules.iter().position(|r| {
2279 r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str())
2280 }) {
2281 deleted_id = rules[pos]
2282 .get("approvalRuleId")
2283 .and_then(Value::as_str)
2284 .unwrap_or_default()
2285 .to_string();
2286 rules.remove(pos);
2287 }
2288 }
2289 ok(json!({ "approvalRuleId": deleted_id }))
2290 }
2291
2292 fn update_pull_request_approval_rule_content(
2293 &self,
2294 req: &AwsRequest,
2295 ) -> Result<AwsResponse, AwsServiceError> {
2296 let b = body(req);
2297 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2298 let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2299 let content = require(&b, "newRuleContent", "ApprovalRuleContentRequiredException")?;
2300 if content.chars().count() > 3000 {
2301 return Err(err(
2302 "InvalidApprovalRuleContentException",
2303 "The approval rule content is not valid.",
2304 ));
2305 }
2306 let content = normalize_rule_content(&content);
2307 let now = Utc::now();
2308 let user = caller_arn(req);
2309 let account = self.account(req);
2310 let mut guard = self.state.write();
2311 let st = guard.get_or_create(&account);
2312 let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2313 let rules = pr
2314 .get_mut("approvalRules")
2315 .and_then(Value::as_array_mut)
2316 .ok_or_else(pr_not_found)?;
2317 let rule = rules
2318 .iter_mut()
2319 .find(|r| r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str()))
2320 .ok_or_else(|| {
2321 err(
2322 "ApprovalRuleDoesNotExistException",
2323 "The specified approval rule does not exist.",
2324 )
2325 })?;
2326 rule["approvalRuleContent"] = json!(content);
2327 rule["ruleContentSha256"] = json!(sha256_hex(content.as_bytes()));
2328 rule["lastModifiedDate"] = ts(now);
2329 rule["lastModifiedUser"] = json!(user);
2330 let rule = rule.clone();
2331 ok(json!({ "approvalRule": rule }))
2332 }
2333
2334 fn update_pull_request_approval_state(
2335 &self,
2336 req: &AwsRequest,
2337 ) -> Result<AwsResponse, AwsServiceError> {
2338 let b = body(req);
2339 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2340 let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2341 let state = require(&b, "approvalState", "ApprovalStateRequiredException")?;
2342 if !validate::is_enum(validate::APPROVAL_STATE, &state) {
2343 return Err(err(
2344 "InvalidApprovalStateException",
2345 "The approval state is not valid.",
2346 ));
2347 }
2348 let user = caller_arn(req);
2349 let account = self.account(req);
2350 let mut guard = self.state.write();
2351 let st = guard.get_or_create(&account);
2352 let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2353 if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2354 return Err(err(
2355 "PullRequestAlreadyClosedException",
2356 "The pull request is already closed.",
2357 ));
2358 }
2359 let current_revision = pr
2360 .get("revisionId")
2361 .and_then(Value::as_str)
2362 .unwrap_or_default()
2363 .to_string();
2364 if revision != current_revision {
2365 return Err(err(
2366 "RevisionNotCurrentException",
2367 "The revision ID is not the current revision of the pull request.",
2368 ));
2369 }
2370 if pr.get("authorArn").and_then(Value::as_str) == Some(user.as_str()) {
2371 return Err(err(
2372 "PullRequestCannotBeApprovedByAuthorException",
2373 "The pull request cannot be approved by the author of the pull request.",
2374 ));
2375 }
2376 st.pull_request_approvals
2377 .entry(id)
2378 .or_default()
2379 .entry(revision)
2380 .or_default()
2381 .insert(user, state);
2382 ok(json!({}))
2383 }
2384
2385 fn get_pull_request_approval_states(
2386 &self,
2387 req: &AwsRequest,
2388 ) -> Result<AwsResponse, AwsServiceError> {
2389 let b = body(req);
2390 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2391 let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2392 let account = self.account(req);
2393 let guard = self.state.read();
2394 let st = guard.get(&account);
2395 let s = st.ok_or_else(pr_not_found)?;
2396 if !s.pull_requests.contains_key(&id) {
2397 return Err(pr_not_found());
2398 }
2399 let approvals: Vec<Value> = s
2400 .pull_request_approvals
2401 .get(&id)
2402 .and_then(|m| m.get(&revision))
2403 .map(|m| {
2404 m.iter()
2405 .filter(|(_, v)| v.as_str() == "APPROVE")
2406 .map(|(arn, v)| json!({ "userArn": arn, "approvalState": v }))
2407 .collect()
2408 })
2409 .unwrap_or_default();
2410 ok(json!({ "approvals": approvals }))
2411 }
2412
2413 fn evaluate_pull_request_approval_rules(
2414 &self,
2415 req: &AwsRequest,
2416 ) -> Result<AwsResponse, AwsServiceError> {
2417 let b = body(req);
2418 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2419 let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2420 let account = self.account(req);
2421 let guard = self.state.read();
2422 let st = guard.get(&account);
2423 let s = st.ok_or_else(pr_not_found)?;
2424 let pr = s.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2425 let current_revision = pr
2426 .get("revisionId")
2427 .and_then(Value::as_str)
2428 .unwrap_or_default();
2429 if revision != current_revision {
2430 return Err(err(
2431 "RevisionNotCurrentException",
2432 "The revision ID is not the current revision of the pull request.",
2433 ));
2434 }
2435 let overridden = s
2436 .pull_request_overrides
2437 .get(&id)
2438 .and_then(|m| m.get(&revision))
2439 .is_some();
2440 let rules = pr
2441 .get("approvalRules")
2442 .and_then(Value::as_array)
2443 .cloned()
2444 .unwrap_or_default();
2445 let approvers: std::collections::BTreeSet<String> = s
2447 .pull_request_approvals
2448 .get(&id)
2449 .and_then(|m| m.get(&revision))
2450 .map(|m| {
2451 m.iter()
2452 .filter(|(_, v)| v.as_str() == "APPROVE")
2453 .map(|(arn, _)| arn.clone())
2454 .collect()
2455 })
2456 .unwrap_or_default();
2457 let mut satisfied_names = Vec::new();
2458 let mut not_satisfied_names = Vec::new();
2459 for r in &rules {
2460 let Some(name) = r.get("approvalRuleName").cloned() else {
2461 continue;
2462 };
2463 let content = r
2464 .get("approvalRuleContent")
2465 .and_then(Value::as_str)
2466 .unwrap_or_default();
2467 if overridden || rule_satisfied(content, &approvers) {
2470 satisfied_names.push(name);
2471 } else {
2472 not_satisfied_names.push(name);
2473 }
2474 }
2475 let approved = not_satisfied_names.is_empty();
2476 ok(json!({
2477 "evaluation": {
2478 "approved": approved,
2479 "overridden": overridden,
2480 "approvalRulesSatisfied": satisfied_names,
2481 "approvalRulesNotSatisfied": not_satisfied_names,
2482 }
2483 }))
2484 }
2485
2486 fn override_pull_request_approval_rules(
2487 &self,
2488 req: &AwsRequest,
2489 ) -> Result<AwsResponse, AwsServiceError> {
2490 let b = body(req);
2491 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2492 let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2493 let status = require(&b, "overrideStatus", "OverrideStatusRequiredException")?;
2494 if !validate::is_enum(validate::OVERRIDE_STATUS, &status) {
2495 return Err(err(
2496 "InvalidOverrideStatusException",
2497 "The override status is not valid.",
2498 ));
2499 }
2500 let user = caller_arn(req);
2501 let account = self.account(req);
2502 let mut guard = self.state.write();
2503 let st = guard.get_or_create(&account);
2504 {
2505 let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2506 let current_revision = pr
2507 .get("revisionId")
2508 .and_then(Value::as_str)
2509 .unwrap_or_default();
2510 if revision != current_revision {
2511 return Err(err(
2512 "RevisionNotCurrentException",
2513 "The revision ID is not the current revision of the pull request.",
2514 ));
2515 }
2516 }
2517 let overrides = st.pull_request_overrides.entry(id).or_default();
2518 if status == "OVERRIDE" {
2519 if overrides.contains_key(&revision) {
2520 return Err(err(
2521 "OverrideAlreadySetException",
2522 "The override status is already set.",
2523 ));
2524 }
2525 overrides.insert(revision, user);
2526 } else {
2527 overrides.remove(&revision);
2528 }
2529 ok(json!({}))
2530 }
2531
2532 fn get_pull_request_override_state(
2533 &self,
2534 req: &AwsRequest,
2535 ) -> Result<AwsResponse, AwsServiceError> {
2536 let b = body(req);
2537 let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2538 let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2539 let account = self.account(req);
2540 let guard = self.state.read();
2541 let st = guard.get(&account);
2542 let s = st.ok_or_else(pr_not_found)?;
2543 if !s.pull_requests.contains_key(&id) {
2544 return Err(pr_not_found());
2545 }
2546 match s
2547 .pull_request_overrides
2548 .get(&id)
2549 .and_then(|m| m.get(&revision))
2550 {
2551 Some(arn) => ok(json!({ "overridden": true, "overrider": arn })),
2552 None => ok(json!({ "overridden": false })),
2553 }
2554 }
2555
2556 fn create_approval_rule_template(
2559 &self,
2560 req: &AwsRequest,
2561 ) -> Result<AwsResponse, AwsServiceError> {
2562 let b = body(req);
2563 let name = require(
2564 &b,
2565 "approvalRuleTemplateName",
2566 "ApprovalRuleTemplateNameRequiredException",
2567 )?;
2568 let content = require(
2569 &b,
2570 "approvalRuleTemplateContent",
2571 "ApprovalRuleTemplateContentRequiredException",
2572 )?;
2573 if name.chars().count() > 100 {
2574 return Err(err(
2575 "InvalidApprovalRuleTemplateNameException",
2576 "The approval rule template name is not valid.",
2577 ));
2578 }
2579 if content.chars().count() > 3000 {
2580 return Err(err(
2581 "InvalidApprovalRuleTemplateContentException",
2582 "The approval rule template content is not valid.",
2583 ));
2584 }
2585 let content = normalize_rule_content(&content);
2586 if let Some(d) = str_field(&b, "approvalRuleTemplateDescription") {
2587 if d.chars().count() > 1000 {
2588 return Err(err(
2589 "InvalidApprovalRuleTemplateDescriptionException",
2590 "The approval rule template description is not valid.",
2591 ));
2592 }
2593 }
2594 let now = Utc::now();
2595 let user = caller_arn(req);
2596 let account = self.account(req);
2597 let mut guard = self.state.write();
2598 let st = guard.get_or_create(&account);
2599 if st.templates.contains_key(&name) {
2600 return Err(err(
2601 "ApprovalRuleTemplateNameAlreadyExistsException",
2602 format!("An approval rule template with the name {name} already exists."),
2603 ));
2604 }
2605 let mut tmpl = Map::new();
2606 tmpl.insert("approvalRuleTemplateId".into(), json!(new_uuid()));
2607 tmpl.insert("approvalRuleTemplateName".into(), json!(name));
2608 if let Some(d) = str_field(&b, "approvalRuleTemplateDescription") {
2609 tmpl.insert("approvalRuleTemplateDescription".into(), json!(d));
2610 }
2611 tmpl.insert("approvalRuleTemplateContent".into(), json!(content));
2612 tmpl.insert(
2613 "ruleContentSha256".into(),
2614 json!(sha256_hex(content.as_bytes())),
2615 );
2616 tmpl.insert("lastModifiedDate".into(), ts(now));
2617 tmpl.insert("creationDate".into(), ts(now));
2618 tmpl.insert("lastModifiedUser".into(), json!(user));
2619 let tmpl = Value::Object(tmpl);
2620 st.templates.insert(name.clone(), tmpl.clone());
2621 st.template_order.push(name);
2622 ok(json!({ "approvalRuleTemplate": tmpl }))
2623 }
2624
2625 fn get_approval_rule_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2626 let b = body(req);
2627 let name = require(
2628 &b,
2629 "approvalRuleTemplateName",
2630 "ApprovalRuleTemplateNameRequiredException",
2631 )?;
2632 let account = self.account(req);
2633 let guard = self.state.read();
2634 let st = guard.get(&account);
2635 match st.and_then(|s| s.templates.get(&name)) {
2636 Some(t) => ok(json!({ "approvalRuleTemplate": t })),
2637 None => Err(template_not_found()),
2638 }
2639 }
2640
2641 fn delete_approval_rule_template(
2642 &self,
2643 req: &AwsRequest,
2644 ) -> Result<AwsResponse, AwsServiceError> {
2645 let b = body(req);
2646 let name = require(
2647 &b,
2648 "approvalRuleTemplateName",
2649 "ApprovalRuleTemplateNameRequiredException",
2650 )?;
2651 if name.chars().count() > 100 {
2652 return Err(err(
2653 "InvalidApprovalRuleTemplateNameException",
2654 "The approval rule template name is not valid.",
2655 ));
2656 }
2657 let account = self.account(req);
2658 let mut guard = self.state.write();
2659 let st = guard.get_or_create(&account);
2660 let id = st
2661 .templates
2662 .remove(&name)
2663 .and_then(|t| {
2664 t.get("approvalRuleTemplateId")
2665 .and_then(Value::as_str)
2666 .map(str::to_string)
2667 })
2668 .unwrap_or_default();
2669 st.template_order.retain(|n| n != &name);
2670 for repo in st.repositories.values_mut() {
2671 repo.associated_templates.retain(|t| t != &name);
2672 }
2673 ok(json!({ "approvalRuleTemplateId": id }))
2674 }
2675
2676 fn list_approval_rule_templates(
2677 &self,
2678 req: &AwsRequest,
2679 ) -> Result<AwsResponse, AwsServiceError> {
2680 let account = self.account(req);
2681 let guard = self.state.read();
2682 let st = guard.get(&account);
2683 let names: Vec<Value> = st
2684 .map(|s| s.template_order.iter().map(|n| json!(n)).collect())
2685 .unwrap_or_default();
2686 ok(json!({ "approvalRuleTemplateNames": names }))
2687 }
2688
2689 fn update_approval_rule_template_content(
2690 &self,
2691 req: &AwsRequest,
2692 ) -> Result<AwsResponse, AwsServiceError> {
2693 let b = body(req);
2694 let name = require(
2695 &b,
2696 "approvalRuleTemplateName",
2697 "ApprovalRuleTemplateNameRequiredException",
2698 )?;
2699 let content = require(
2700 &b,
2701 "newRuleContent",
2702 "ApprovalRuleTemplateContentRequiredException",
2703 )?;
2704 if content.chars().count() > 3000 {
2705 return Err(err(
2706 "InvalidApprovalRuleTemplateContentException",
2707 "The approval rule template content is not valid.",
2708 ));
2709 }
2710 let content = normalize_rule_content(&content);
2711 self.mutate_template(req, &name, |t| {
2712 t["approvalRuleTemplateContent"] = json!(content);
2713 t["ruleContentSha256"] = json!(sha256_hex(content.as_bytes()));
2714 t["lastModifiedDate"] = ts(Utc::now());
2715 Ok(())
2716 })
2717 }
2718
2719 fn update_approval_rule_template_description(
2720 &self,
2721 req: &AwsRequest,
2722 ) -> Result<AwsResponse, AwsServiceError> {
2723 let b = body(req);
2724 let name = require(
2725 &b,
2726 "approvalRuleTemplateName",
2727 "ApprovalRuleTemplateNameRequiredException",
2728 )?;
2729 let desc = require(
2730 &b,
2731 "approvalRuleTemplateDescription",
2732 "InvalidApprovalRuleTemplateDescriptionException",
2733 )?;
2734 if desc.chars().count() > 1000 {
2735 return Err(err(
2736 "InvalidApprovalRuleTemplateDescriptionException",
2737 "The approval rule template description is not valid.",
2738 ));
2739 }
2740 self.mutate_template(req, &name, |t| {
2741 t["approvalRuleTemplateDescription"] = json!(desc);
2742 t["lastModifiedDate"] = ts(Utc::now());
2743 Ok(())
2744 })
2745 }
2746
2747 fn update_approval_rule_template_name(
2748 &self,
2749 req: &AwsRequest,
2750 ) -> Result<AwsResponse, AwsServiceError> {
2751 let b = body(req);
2752 let old = require(
2753 &b,
2754 "oldApprovalRuleTemplateName",
2755 "ApprovalRuleTemplateNameRequiredException",
2756 )?;
2757 let new = require(
2758 &b,
2759 "newApprovalRuleTemplateName",
2760 "ApprovalRuleTemplateNameRequiredException",
2761 )?;
2762 if new.chars().count() > 100 {
2763 return Err(err(
2764 "InvalidApprovalRuleTemplateNameException",
2765 "The approval rule template name is not valid.",
2766 ));
2767 }
2768 let account = self.account(req);
2769 let mut guard = self.state.write();
2770 let st = guard.get_or_create(&account);
2771 if !st.templates.contains_key(&old) {
2772 return Err(template_not_found());
2773 }
2774 if old != new && st.templates.contains_key(&new) {
2775 return Err(err(
2776 "ApprovalRuleTemplateNameAlreadyExistsException",
2777 format!("An approval rule template with the name {new} already exists."),
2778 ));
2779 }
2780 let mut tmpl = st.templates.remove(&old).unwrap();
2781 tmpl["approvalRuleTemplateName"] = json!(new);
2782 tmpl["lastModifiedDate"] = ts(Utc::now());
2783 for n in &mut st.template_order {
2784 if n == &old {
2785 *n = new.clone();
2786 }
2787 }
2788 for repo in st.repositories.values_mut() {
2789 for t in &mut repo.associated_templates {
2790 if t == &old {
2791 *t = new.clone();
2792 }
2793 }
2794 }
2795 st.templates.insert(new, tmpl.clone());
2796 ok(json!({ "approvalRuleTemplate": tmpl }))
2797 }
2798
2799 fn associate_template_with_repository(
2800 &self,
2801 req: &AwsRequest,
2802 ) -> Result<AwsResponse, AwsServiceError> {
2803 let b = body(req);
2804 let tmpl = require(
2805 &b,
2806 "approvalRuleTemplateName",
2807 "ApprovalRuleTemplateNameRequiredException",
2808 )?;
2809 let name = require_repository_name(&b)?;
2810 let account = self.account(req);
2811 let mut guard = self.state.write();
2812 let st = guard.get_or_create(&account);
2813 if !st.templates.contains_key(&tmpl) {
2814 return Err(template_not_found());
2815 }
2816 let repo = st
2817 .repositories
2818 .get_mut(&name)
2819 .ok_or_else(|| repo_not_found(&name))?;
2820 if !repo.associated_templates.contains(&tmpl) {
2821 repo.associated_templates.push(tmpl);
2822 }
2823 ok(json!({}))
2824 }
2825
2826 fn disassociate_template_from_repository(
2827 &self,
2828 req: &AwsRequest,
2829 ) -> Result<AwsResponse, AwsServiceError> {
2830 let b = body(req);
2831 let tmpl = require(
2832 &b,
2833 "approvalRuleTemplateName",
2834 "ApprovalRuleTemplateNameRequiredException",
2835 )?;
2836 let name = require_repository_name(&b)?;
2837 let account = self.account(req);
2838 let mut guard = self.state.write();
2839 let st = guard.get_or_create(&account);
2840 if !st.templates.contains_key(&tmpl) {
2841 return Err(template_not_found());
2842 }
2843 let repo = st
2844 .repositories
2845 .get_mut(&name)
2846 .ok_or_else(|| repo_not_found(&name))?;
2847 repo.associated_templates.retain(|t| t != &tmpl);
2848 ok(json!({}))
2849 }
2850
2851 fn batch_associate_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2852 let b = body(req);
2853 let tmpl = require(
2854 &b,
2855 "approvalRuleTemplateName",
2856 "ApprovalRuleTemplateNameRequiredException",
2857 )?;
2858 let names = b
2859 .get("repositoryNames")
2860 .and_then(Value::as_array)
2861 .ok_or_else(|| {
2862 err(
2863 "RepositoryNamesRequiredException",
2864 "Repository names are required.",
2865 )
2866 })?
2867 .clone();
2868 let account = self.account(req);
2869 let mut guard = self.state.write();
2870 let st = guard.get_or_create(&account);
2871 if !st.templates.contains_key(&tmpl) {
2872 return Err(template_not_found());
2873 }
2874 let mut associated = Vec::new();
2875 let mut errors = Vec::new();
2876 for n in &names {
2877 let Some(rn) = n.as_str() else { continue };
2878 match st.repositories.get_mut(rn) {
2879 Some(repo) => {
2880 if !repo.associated_templates.contains(&tmpl) {
2881 repo.associated_templates.push(tmpl.clone());
2882 }
2883 associated.push(json!(rn));
2884 }
2885 None => errors.push(json!({
2886 "repositoryName": rn,
2887 "errorCode": "RepositoryDoesNotExist",
2888 "errorMessage": format!("The repository {rn} does not exist."),
2889 })),
2890 }
2891 }
2892 ok(json!({ "associatedRepositoryNames": associated, "errors": errors }))
2893 }
2894
2895 fn batch_disassociate_template(
2896 &self,
2897 req: &AwsRequest,
2898 ) -> Result<AwsResponse, AwsServiceError> {
2899 let b = body(req);
2900 let tmpl = require(
2901 &b,
2902 "approvalRuleTemplateName",
2903 "ApprovalRuleTemplateNameRequiredException",
2904 )?;
2905 let names = b
2906 .get("repositoryNames")
2907 .and_then(Value::as_array)
2908 .ok_or_else(|| {
2909 err(
2910 "RepositoryNamesRequiredException",
2911 "Repository names are required.",
2912 )
2913 })?
2914 .clone();
2915 let account = self.account(req);
2916 let mut guard = self.state.write();
2917 let st = guard.get_or_create(&account);
2918 if !st.templates.contains_key(&tmpl) {
2919 return Err(template_not_found());
2920 }
2921 let mut disassociated = Vec::new();
2922 let mut errors = Vec::new();
2923 for n in &names {
2924 let Some(rn) = n.as_str() else { continue };
2925 match st.repositories.get_mut(rn) {
2926 Some(repo) => {
2927 repo.associated_templates.retain(|t| t != &tmpl);
2928 disassociated.push(json!(rn));
2929 }
2930 None => errors.push(json!({
2931 "repositoryName": rn,
2932 "errorCode": "RepositoryDoesNotExist",
2933 "errorMessage": format!("The repository {rn} does not exist."),
2934 })),
2935 }
2936 }
2937 ok(json!({ "disassociatedRepositoryNames": disassociated, "errors": errors }))
2938 }
2939
2940 fn list_associated_templates_for_repository(
2941 &self,
2942 req: &AwsRequest,
2943 ) -> Result<AwsResponse, AwsServiceError> {
2944 let b = body(req);
2945 let name = require_repository_name(&b)?;
2946 let account = self.account(req);
2947 let guard = self.state.read();
2948 let st = guard.get(&account);
2949 let repo = st
2950 .and_then(|s| s.repositories.get(&name))
2951 .ok_or_else(|| repo_not_found(&name))?;
2952 let names: Vec<Value> = repo.associated_templates.iter().map(|t| json!(t)).collect();
2953 ok(json!({ "approvalRuleTemplateNames": names }))
2954 }
2955
2956 fn list_repositories_for_template(
2957 &self,
2958 req: &AwsRequest,
2959 ) -> Result<AwsResponse, AwsServiceError> {
2960 let b = body(req);
2961 let tmpl = require(
2962 &b,
2963 "approvalRuleTemplateName",
2964 "ApprovalRuleTemplateNameRequiredException",
2965 )?;
2966 let account = self.account(req);
2967 let guard = self.state.read();
2968 let st = guard.get(&account);
2969 let s = st.ok_or_else(template_not_found)?;
2970 if !s.templates.contains_key(&tmpl) {
2971 return Err(template_not_found());
2972 }
2973 let names: Vec<Value> = s
2974 .repository_order
2975 .iter()
2976 .filter(|n| {
2977 s.repositories
2978 .get(*n)
2979 .map(|r| r.associated_templates.contains(&tmpl))
2980 .unwrap_or(false)
2981 })
2982 .map(|n| json!(n))
2983 .collect();
2984 ok(json!({ "repositoryNames": names }))
2985 }
2986
2987 fn post_comment_for_compared_commit(
2990 &self,
2991 req: &AwsRequest,
2992 ) -> Result<AwsResponse, AwsServiceError> {
2993 let b = body(req);
2994 let name = require_repository_name(&b)?;
2995 let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
2996 let content = require(&b, "content", "CommentContentRequiredException")?;
2997 if content.chars().count() > 10240 {
2998 return Err(err(
2999 "CommentContentSizeLimitExceededException",
3000 "The comment is too large. Comments are limited to 10,240 characters.",
3001 ));
3002 }
3003 let before = str_field(&b, "beforeCommitId");
3004 if before.as_deref() == Some(after.as_str()) {
3005 return Err(err(
3006 "BeforeCommitIdAndAfterCommitIdAreSameException",
3007 "The before commit ID and the after commit ID are the same.",
3008 ));
3009 }
3010 let account = self.account(req);
3011 let mut guard = self.state.write();
3012 let st = guard.get_or_create(&account);
3013 if !st.repositories.contains_key(&name) {
3014 return Err(repo_not_found(&name));
3015 }
3016 let now = Utc::now();
3017 let author = caller_arn(req);
3018 let comment_id = new_uuid();
3019 let location = b.get("location").cloned();
3020 let mut stored = comment_value(&comment_id, &content, &author, now, None);
3021 if let Some(obj) = stored.as_object_mut() {
3022 obj.insert("_ctxRepositoryName".into(), json!(name));
3023 obj.insert("_ctxAfterCommitId".into(), json!(after));
3024 if let Some(bc) = &before {
3025 obj.insert("_ctxBeforeCommitId".into(), json!(bc));
3026 }
3027 if let Some(loc) = &location {
3028 obj.insert("_ctxLocation".into(), loc.clone());
3029 }
3030 }
3031 st.comments.insert(comment_id.clone(), stored.clone());
3032 st.comment_order.push(comment_id);
3033 let public = public_comment(&stored);
3034 let mut out = Map::new();
3035 out.insert("repositoryName".into(), json!(name));
3036 if let Some(bc) = before {
3037 out.insert("beforeCommitId".into(), json!(bc));
3038 }
3039 out.insert("afterCommitId".into(), json!(after));
3040 if let Some(loc) = location {
3041 out.insert("location".into(), loc);
3042 }
3043 out.insert("comment".into(), public);
3044 ok(Value::Object(out))
3045 }
3046
3047 fn post_comment_for_pull_request(
3048 &self,
3049 req: &AwsRequest,
3050 ) -> Result<AwsResponse, AwsServiceError> {
3051 let b = body(req);
3052 let pr_id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
3053 let name = require_repository_name(&b)?;
3054 let before = require(&b, "beforeCommitId", "CommitIdRequiredException")?;
3055 let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
3056 let content = require(&b, "content", "CommentContentRequiredException")?;
3057 if content.chars().count() > 10240 {
3058 return Err(err(
3059 "CommentContentSizeLimitExceededException",
3060 "The comment is too large.",
3061 ));
3062 }
3063 let account = self.account(req);
3064 let mut guard = self.state.write();
3065 let st = guard.get_or_create(&account);
3066 if !st.repositories.contains_key(&name) {
3067 return Err(repo_not_found(&name));
3068 }
3069 if !st.pull_requests.contains_key(&pr_id) {
3070 return Err(pr_not_found());
3071 }
3072 let now = Utc::now();
3073 let author = caller_arn(req);
3074 let comment_id = new_uuid();
3075 let location = b.get("location").cloned();
3076 let mut stored = comment_value(&comment_id, &content, &author, now, None);
3077 if let Some(obj) = stored.as_object_mut() {
3078 obj.insert("_ctxRepositoryName".into(), json!(name));
3079 obj.insert("_ctxPullRequestId".into(), json!(pr_id));
3080 obj.insert("_ctxBeforeCommitId".into(), json!(before));
3081 obj.insert("_ctxAfterCommitId".into(), json!(after));
3082 if let Some(loc) = &location {
3083 obj.insert("_ctxLocation".into(), loc.clone());
3084 }
3085 }
3086 st.comments.insert(comment_id.clone(), stored.clone());
3087 st.comment_order.push(comment_id);
3088 let public = public_comment(&stored);
3089 let mut out = Map::new();
3090 out.insert("repositoryName".into(), json!(name));
3091 out.insert("pullRequestId".into(), json!(pr_id));
3092 out.insert("beforeCommitId".into(), json!(before));
3093 out.insert("afterCommitId".into(), json!(after));
3094 if let Some(loc) = location {
3095 out.insert("location".into(), loc);
3096 }
3097 out.insert("comment".into(), public);
3098 ok(Value::Object(out))
3099 }
3100
3101 fn post_comment_reply(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3102 let b = body(req);
3103 let in_reply_to = require(&b, "inReplyTo", "CommentIdRequiredException")?;
3104 let content = require(&b, "content", "CommentContentRequiredException")?;
3105 if content.chars().count() > 10240 {
3106 return Err(err(
3107 "CommentContentSizeLimitExceededException",
3108 "The comment is too large.",
3109 ));
3110 }
3111 let account = self.account(req);
3112 let mut guard = self.state.write();
3113 let st = guard.get_or_create(&account);
3114 let parent = st.comments.get(&in_reply_to).cloned().ok_or_else(|| {
3115 err(
3116 "CommentDoesNotExistException",
3117 "The specified comment does not exist.",
3118 )
3119 })?;
3120 let now = Utc::now();
3121 let author = caller_arn(req);
3122 let comment_id = new_uuid();
3123 let mut stored = comment_value(&comment_id, &content, &author, now, Some(&in_reply_to));
3124 if let (Some(obj), Some(pobj)) = (stored.as_object_mut(), parent.as_object()) {
3126 for (k, v) in pobj {
3127 if k.starts_with("_ctx") {
3128 obj.insert(k.clone(), v.clone());
3129 }
3130 }
3131 }
3132 st.comments.insert(comment_id.clone(), stored.clone());
3133 st.comment_order.push(comment_id);
3134 ok(json!({ "comment": public_comment(&stored) }))
3135 }
3136
3137 fn get_comment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3138 let b = body(req);
3139 let id = require(&b, "commentId", "CommentIdRequiredException")?;
3140 let account = self.account(req);
3141 let guard = self.state.read();
3142 let st = guard.get(&account);
3143 let comment = st.and_then(|s| s.comments.get(&id)).ok_or_else(|| {
3144 err(
3145 "CommentDoesNotExistException",
3146 "The specified comment does not exist.",
3147 )
3148 })?;
3149 if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3150 return Err(err(
3151 "CommentDeletedException",
3152 "This comment has already been deleted.",
3153 ));
3154 }
3155 ok(json!({ "comment": public_comment(comment) }))
3156 }
3157
3158 fn get_comments_for_compared_commit(
3159 &self,
3160 req: &AwsRequest,
3161 ) -> Result<AwsResponse, AwsServiceError> {
3162 let b = body(req);
3163 let name = require_repository_name(&b)?;
3164 let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
3165 let before = str_field(&b, "beforeCommitId");
3166 let account = self.account(req);
3167 let guard = self.state.read();
3168 let st = guard.get(&account);
3169 let s = st.ok_or_else(|| repo_not_found(&name))?;
3170 if !s.repositories.contains_key(&name) {
3171 return Err(repo_not_found(&name));
3172 }
3173 let mut threads: Vec<Value> = Vec::new();
3174 for cid in &s.comment_order {
3175 let Some(c) = s.comments.get(cid) else {
3176 continue;
3177 };
3178 if c.get("_ctxPullRequestId").is_some() {
3179 continue;
3180 }
3181 if c.get("_ctxRepositoryName").and_then(Value::as_str) != Some(name.as_str()) {
3182 continue;
3183 }
3184 if c.get("_ctxAfterCommitId").and_then(Value::as_str) != Some(after.as_str()) {
3185 continue;
3186 }
3187 if let Some(bc) = &before {
3188 if c.get("_ctxBeforeCommitId").and_then(Value::as_str) != Some(bc.as_str()) {
3189 continue;
3190 }
3191 }
3192 threads.push(json!({
3193 "repositoryName": name,
3194 "beforeCommitId": before,
3195 "afterCommitId": after,
3196 "location": c.get("_ctxLocation").cloned().unwrap_or(Value::Null),
3197 "comments": [public_comment(c)],
3198 }));
3199 }
3200 ok(json!({ "commentsForComparedCommitData": threads }))
3201 }
3202
3203 fn get_comments_for_pull_request(
3204 &self,
3205 req: &AwsRequest,
3206 ) -> Result<AwsResponse, AwsServiceError> {
3207 let b = body(req);
3208 let pr_id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
3209 let account = self.account(req);
3210 let guard = self.state.read();
3211 let st = guard.get(&account);
3212 let s = st.ok_or_else(pr_not_found)?;
3213 if !s.pull_requests.contains_key(&pr_id) {
3214 return Err(pr_not_found());
3215 }
3216 let mut threads: Vec<Value> = Vec::new();
3217 for cid in &s.comment_order {
3218 let Some(c) = s.comments.get(cid) else {
3219 continue;
3220 };
3221 if c.get("_ctxPullRequestId").and_then(Value::as_str) != Some(pr_id.as_str()) {
3222 continue;
3223 }
3224 threads.push(json!({
3225 "pullRequestId": pr_id,
3226 "repositoryName": c.get("_ctxRepositoryName").cloned().unwrap_or(Value::Null),
3227 "beforeCommitId": c.get("_ctxBeforeCommitId").cloned().unwrap_or(Value::Null),
3228 "afterCommitId": c.get("_ctxAfterCommitId").cloned().unwrap_or(Value::Null),
3229 "location": c.get("_ctxLocation").cloned().unwrap_or(Value::Null),
3230 "comments": [public_comment(c)],
3231 }));
3232 }
3233 ok(json!({ "commentsForPullRequestData": threads }))
3234 }
3235
3236 fn update_comment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3237 let b = body(req);
3238 let id = require(&b, "commentId", "CommentIdRequiredException")?;
3239 let content = require(&b, "content", "CommentContentRequiredException")?;
3240 if content.chars().count() > 10240 {
3241 return Err(err(
3242 "CommentContentSizeLimitExceededException",
3243 "The comment is too large.",
3244 ));
3245 }
3246 let caller = caller_arn(req);
3247 let account = self.account(req);
3248 let mut guard = self.state.write();
3249 let st = guard.get_or_create(&account);
3250 let comment = st.comments.get_mut(&id).ok_or_else(|| {
3251 err(
3252 "CommentDoesNotExistException",
3253 "The specified comment does not exist.",
3254 )
3255 })?;
3256 if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3257 return Err(err(
3258 "CommentDeletedException",
3259 "This comment has already been deleted.",
3260 ));
3261 }
3262 if comment.get("authorArn").and_then(Value::as_str) != Some(caller.as_str()) {
3263 return Err(err(
3264 "CommentNotCreatedByCallerException",
3265 "You cannot modify a comment that was not created by you.",
3266 ));
3267 }
3268 comment["content"] = json!(content);
3269 comment["lastModifiedDate"] = ts(Utc::now());
3270 ok(json!({ "comment": public_comment(comment) }))
3271 }
3272
3273 fn delete_comment_content(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3274 let b = body(req);
3275 let id = require(&b, "commentId", "CommentIdRequiredException")?;
3276 let account = self.account(req);
3277 let mut guard = self.state.write();
3278 let st = guard.get_or_create(&account);
3279 let comment = st.comments.get_mut(&id).ok_or_else(|| {
3280 err(
3281 "CommentDoesNotExistException",
3282 "The specified comment does not exist.",
3283 )
3284 })?;
3285 comment["deleted"] = json!(true);
3286 comment["content"] = json!("");
3287 comment["lastModifiedDate"] = ts(Utc::now());
3288 ok(json!({ "comment": public_comment(comment) }))
3289 }
3290
3291 fn put_comment_reaction(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3292 let b = body(req);
3293 let id = require(&b, "commentId", "CommentIdRequiredException")?;
3294 let reaction = require(&b, "reactionValue", "ReactionValueRequiredException")?;
3295 if !validate::REACTION_EMOJIS.contains(&reaction.as_str())
3296 && !reaction.starts_with(":")
3297 && !reaction.starts_with("\\u")
3298 {
3299 return Err(err(
3300 "InvalidReactionValueException",
3301 "The reaction value is not valid.",
3302 ));
3303 }
3304 let user = caller_arn(req);
3305 let account = self.account(req);
3306 let mut guard = self.state.write();
3307 let st = guard.get_or_create(&account);
3308 let comment = st.comments.get_mut(&id).ok_or_else(|| {
3309 err(
3310 "CommentDoesNotExistException",
3311 "The specified comment does not exist.",
3312 )
3313 })?;
3314 if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3315 return Err(err(
3316 "CommentDeletedException",
3317 "This comment has already been deleted.",
3318 ));
3319 }
3320 let reactions = comment
3321 .as_object_mut()
3322 .unwrap()
3323 .entry("_ctxReactions".to_string())
3324 .or_insert_with(|| json!({}));
3325 if let Some(obj) = reactions.as_object_mut() {
3326 let users = obj.entry(reaction).or_insert_with(|| json!([]));
3327 if let Some(arr) = users.as_array_mut() {
3328 if !arr.iter().any(|u| u.as_str() == Some(user.as_str())) {
3329 arr.push(json!(user));
3330 }
3331 }
3332 }
3333 ok(json!({}))
3334 }
3335
3336 fn get_comment_reactions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3337 let b = body(req);
3338 let id = require(&b, "commentId", "CommentIdRequiredException")?;
3339 let account = self.account(req);
3340 let guard = self.state.read();
3341 let st = guard.get(&account);
3342 let comment = st.and_then(|s| s.comments.get(&id)).ok_or_else(|| {
3343 err(
3344 "CommentDoesNotExistException",
3345 "The specified comment does not exist.",
3346 )
3347 })?;
3348 let mut reactions = Vec::new();
3349 if let Some(obj) = comment.get("_ctxReactions").and_then(Value::as_object) {
3350 for (emoji, users) in obj {
3351 let user_list: Vec<&str> = users
3352 .as_array()
3353 .map(|a| a.iter().filter_map(Value::as_str).collect())
3354 .unwrap_or_default();
3355 reactions.push(json!({
3356 "reaction": {
3357 "emoji": emoji,
3358 "shortCode": emoji,
3359 "unicode": "",
3360 },
3361 "reactionUsers": user_list,
3362 "reactionsFromDeletedUsersCount": 0,
3363 }));
3364 }
3365 }
3366 ok(json!({ "reactionsForComment": reactions }))
3367 }
3368
3369 fn put_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3372 let b = body(req);
3373 let name = require_repository_name(&b)?;
3374 let triggers = b
3375 .get("triggers")
3376 .and_then(Value::as_array)
3377 .ok_or_else(|| {
3378 err(
3379 "RepositoryTriggersListRequiredException",
3380 "The list of triggers for the repository is required.",
3381 )
3382 })?
3383 .clone();
3384 for t in &triggers {
3385 let tname = t.get("name").and_then(Value::as_str);
3386 if tname.map(str::is_empty).unwrap_or(true) {
3387 return Err(err(
3388 "RepositoryTriggerNameRequiredException",
3389 "A name for the trigger is required.",
3390 ));
3391 }
3392 let dest = t.get("destinationArn").and_then(Value::as_str);
3393 if dest.map(str::is_empty).unwrap_or(true) {
3394 return Err(err(
3395 "RepositoryTriggerDestinationArnRequiredException",
3396 "A destination ARN for the target service for the trigger is required.",
3397 ));
3398 }
3399 let events = t.get("events").and_then(Value::as_array);
3400 match events {
3401 None => {
3402 return Err(err(
3403 "RepositoryTriggerEventsListRequiredException",
3404 "At least one event for the trigger is required.",
3405 ))
3406 }
3407 Some(evs) => {
3408 for e in evs {
3409 if let Some(ev) = e.as_str() {
3410 if !validate::is_enum(validate::REPOSITORY_TRIGGER_EVENT, ev) {
3411 return Err(err(
3412 "InvalidRepositoryTriggerEventsException",
3413 "One or more of the trigger events is not valid.",
3414 ));
3415 }
3416 }
3417 }
3418 }
3419 }
3420 }
3421 let account = self.account(req);
3422 let mut guard = self.state.write();
3423 let st = guard.get_or_create(&account);
3424 let repo = st
3425 .repositories
3426 .get_mut(&name)
3427 .ok_or_else(|| repo_not_found(&name))?;
3428 let config_id = new_uuid();
3429 repo.triggers = triggers;
3430 repo.triggers_config_id = config_id.clone();
3431 ok(json!({ "configurationId": config_id }))
3432 }
3433
3434 fn get_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3435 let b = body(req);
3436 let name = require_repository_name(&b)?;
3437 let account = self.account(req);
3438 let guard = self.state.read();
3439 let st = guard.get(&account);
3440 let repo = st
3441 .and_then(|s| s.repositories.get(&name))
3442 .ok_or_else(|| repo_not_found(&name))?;
3443 let config_id = if repo.triggers_config_id.is_empty() {
3444 new_uuid()
3445 } else {
3446 repo.triggers_config_id.clone()
3447 };
3448 ok(json!({
3449 "configurationId": config_id,
3450 "triggers": repo.triggers,
3451 }))
3452 }
3453
3454 fn test_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3455 let b = body(req);
3456 let name = require_repository_name(&b)?;
3457 let triggers = b
3458 .get("triggers")
3459 .and_then(Value::as_array)
3460 .ok_or_else(|| {
3461 err(
3462 "RepositoryTriggersListRequiredException",
3463 "The list of triggers is required.",
3464 )
3465 })?
3466 .clone();
3467 let account = self.account(req);
3468 let guard = self.state.read();
3469 let st = guard.get(&account);
3470 let repo = st
3471 .and_then(|s| s.repositories.get(&name))
3472 .ok_or_else(|| repo_not_found(&name))?;
3473 for t in &triggers {
3475 if t.get("name")
3476 .and_then(Value::as_str)
3477 .map(str::is_empty)
3478 .unwrap_or(true)
3479 {
3480 return Err(err(
3481 "RepositoryTriggerNameRequiredException",
3482 "A name for the trigger is required.",
3483 ));
3484 }
3485 if t.get("destinationArn")
3486 .and_then(Value::as_str)
3487 .map(str::is_empty)
3488 .unwrap_or(true)
3489 {
3490 return Err(err(
3491 "RepositoryTriggerDestinationArnRequiredException",
3492 "A destination ARN for the target service for the trigger is required.",
3493 ));
3494 }
3495 if t.get("events")
3496 .and_then(Value::as_array)
3497 .map(|e| e.is_empty())
3498 .unwrap_or(true)
3499 {
3500 return Err(err(
3501 "RepositoryTriggerEventsListRequiredException",
3502 "At least one event for the trigger is required.",
3503 ));
3504 }
3505 }
3506 let mut successful = Vec::new();
3512 let mut failed = Vec::new();
3513 for t in &triggers {
3514 let tname = t.get("name").and_then(Value::as_str).unwrap_or_default();
3515 let dest = t
3516 .get("destinationArn")
3517 .and_then(Value::as_str)
3518 .unwrap_or_default();
3519 let mut failure: Option<String> = None;
3520 if !trigger_destination_ok(dest, &account, &req.region) {
3521 failure = Some(format!(
3522 "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 ({}).",
3523 req.region
3524 ));
3525 }
3526 if failure.is_none() {
3527 if let Some(evs) = t.get("events").and_then(Value::as_array) {
3528 for e in evs {
3529 if let Some(ev) = e.as_str() {
3530 if !validate::is_enum(validate::REPOSITORY_TRIGGER_EVENT, ev) {
3531 failure = Some(format!("The trigger event {ev} is not valid."));
3532 break;
3533 }
3534 }
3535 }
3536 }
3537 }
3538 if failure.is_none() {
3539 if let Some(branches) = t.get("branches").and_then(Value::as_array) {
3540 for br in branches {
3541 if let Some(bn) = br.as_str() {
3542 if !repo.branches.contains_key(bn) {
3543 failure = Some(format!(
3544 "The branch {bn} specified for the trigger does not exist in the repository."
3545 ));
3546 break;
3547 }
3548 }
3549 }
3550 }
3551 }
3552 match failure {
3553 Some(msg) => {
3554 failed.push(json!({ "trigger": tname, "failureMessage": msg }));
3555 }
3556 None => successful.push(json!(tname)),
3557 }
3558 }
3559 ok(json!({
3560 "successfulExecutions": successful,
3561 "failedExecutions": failed,
3562 }))
3563 }
3564
3565 fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3568 let b = body(req);
3569 let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3570 if !is_codecommit_arn(&arn) {
3571 return Err(err(
3572 "InvalidResourceArnException",
3573 "The resource ARN is not valid.",
3574 ));
3575 }
3576 let tags = b
3577 .get("tags")
3578 .and_then(Value::as_object)
3579 .ok_or_else(|| err("TagsMapRequiredException", "A map of tags is required."))?;
3580 let account = self.account(req);
3581 let mut guard = self.state.write();
3582 let st = guard.get_or_create(&account);
3583 if let Some(rn) = repo_name_from_arn(&arn) {
3584 if !st.repositories.contains_key(&rn) {
3585 return Err(repo_not_found(&rn));
3586 }
3587 }
3588 let entry = st.tags.entry(arn).or_default();
3589 for (k, v) in tags {
3590 if let Some(v) = v.as_str() {
3591 entry.insert(k.clone(), v.to_string());
3592 }
3593 }
3594 ok(json!({}))
3595 }
3596
3597 fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3598 let b = body(req);
3599 let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3600 if !is_codecommit_arn(&arn) {
3601 return Err(err(
3602 "InvalidResourceArnException",
3603 "The resource ARN is not valid.",
3604 ));
3605 }
3606 let keys = b
3607 .get("tagKeys")
3608 .and_then(Value::as_array)
3609 .ok_or_else(|| {
3610 err(
3611 "TagKeysListRequiredException",
3612 "A list of tag keys is required.",
3613 )
3614 })?
3615 .clone();
3616 let account = self.account(req);
3617 let mut guard = self.state.write();
3618 let st = guard.get_or_create(&account);
3619 if let Some(rn) = repo_name_from_arn(&arn) {
3620 if !st.repositories.contains_key(&rn) {
3621 return Err(repo_not_found(&rn));
3622 }
3623 }
3624 if let Some(entry) = st.tags.get_mut(&arn) {
3625 for k in &keys {
3626 if let Some(k) = k.as_str() {
3627 entry.remove(k);
3628 }
3629 }
3630 }
3631 ok(json!({}))
3632 }
3633
3634 fn list_tags_for_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3635 let b = body(req);
3636 let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3637 if !is_codecommit_arn(&arn) {
3638 return Err(err(
3639 "InvalidResourceArnException",
3640 "The resource ARN is not valid.",
3641 ));
3642 }
3643 let account = self.account(req);
3644 let guard = self.state.read();
3645 let st = guard.get(&account);
3646 if let Some(rn) = repo_name_from_arn(&arn) {
3647 if !st
3648 .map(|s| s.repositories.contains_key(&rn))
3649 .unwrap_or(false)
3650 {
3651 return Err(repo_not_found(&rn));
3652 }
3653 }
3654 let tags = st
3655 .and_then(|s| s.tags.get(&arn))
3656 .map(|m| {
3657 let mut obj = Map::new();
3658 for (k, v) in m {
3659 obj.insert(k.clone(), json!(v));
3660 }
3661 Value::Object(obj)
3662 })
3663 .unwrap_or_else(|| json!({}));
3664 ok(json!({ "tags": tags }))
3665 }
3666
3667 fn mutate_pr(
3670 &self,
3671 req: &AwsRequest,
3672 id: &str,
3673 f: impl FnOnce(&mut Value) -> Result<(), AwsServiceError>,
3674 ) -> Result<AwsResponse, AwsServiceError> {
3675 let account = self.account(req);
3676 let mut guard = self.state.write();
3677 let st = guard.get_or_create(&account);
3678 let pr = st.pull_requests.get_mut(id).ok_or_else(pr_not_found)?;
3679 f(pr)?;
3680 let pr = pr.clone();
3681 ok(json!({ "pullRequest": pr }))
3682 }
3683
3684 fn mutate_template(
3685 &self,
3686 req: &AwsRequest,
3687 name: &str,
3688 f: impl FnOnce(&mut Value) -> Result<(), AwsServiceError>,
3689 ) -> Result<AwsResponse, AwsServiceError> {
3690 let account = self.account(req);
3691 let mut guard = self.state.write();
3692 let st = guard.get_or_create(&account);
3693 let t = st.templates.get_mut(name).ok_or_else(template_not_found)?;
3694 f(t)?;
3695 let t = t.clone();
3696 ok(json!({ "approvalRuleTemplate": t }))
3697 }
3698}
3699
3700#[cfg(test)]
3705mod normalize_tests {
3706 use crate::codecommit_helpers::normalize_rule_content;
3707
3708 #[test]
3709 fn compacts_and_preserves_member_order() {
3710 let input = " {\n\t \"Version\": \"2018-11-08\",\n\t \"DestinationReferences\": [\"refs/heads/master\"],\n\t \"Statements\": []\n }\n";
3711 let out = normalize_rule_content(input);
3712 assert_eq!(
3713 out,
3714 "{\"Version\":\"2018-11-08\",\"DestinationReferences\":[\"refs/heads/master\"],\"Statements\":[]}"
3715 );
3716 }
3717}
3718
3719#[cfg(test)]
3720mod engine_tests {
3721 use super::*;
3722
3723 fn entry(id: &str) -> FileEntry {
3724 FileEntry {
3725 blob_id: id.to_string(),
3726 mode: "100644".to_string(),
3727 }
3728 }
3729
3730 fn commit(parents: &[&str]) -> Value {
3731 json!({ "parents": parents, "treeId": "t" })
3732 }
3733
3734 #[test]
3736 fn merge_base_is_lowest_common_ancestor() {
3737 let mut repo = Repo::default();
3742 repo.commits.insert("root".into(), commit(&[]));
3743 repo.commits.insert("X".into(), commit(&["root"]));
3744 repo.commits.insert("A".into(), commit(&["X"]));
3745 repo.commits.insert("B".into(), commit(&["X", "root"]));
3746 assert_eq!(merge_base(&repo, "A", "B"), Some("X".to_string()));
3747 assert_eq!(merge_base(&repo, "B", "A"), Some("X".to_string()));
3748 }
3749
3750 #[test]
3751 fn merge_base_of_a_diamond_is_the_fork_point() {
3752 let mut repo = Repo::default();
3754 repo.commits.insert("root".into(), commit(&[]));
3755 repo.commits.insert("L".into(), commit(&["root"]));
3756 repo.commits.insert("R".into(), commit(&["root"]));
3757 repo.commits.insert("A".into(), commit(&["L"]));
3758 repo.commits.insert("B".into(), commit(&["R"]));
3759 assert_eq!(merge_base(&repo, "A", "B"), Some("root".to_string()));
3760 }
3761
3762 #[test]
3764 fn three_way_merge_propagates_source_deletion() {
3765 let mut repo = Repo::default();
3766 repo.commits.insert("base".into(), commit(&[]));
3767 repo.commits.insert("src".into(), commit(&["base"]));
3768 repo.commits.insert("dst".into(), commit(&["base"]));
3769 let mut base = Tree::new();
3771 base.insert("a.txt".into(), entry("a1"));
3772 base.insert("b.txt".into(), entry("b1"));
3773 repo.trees.insert("base".into(), base);
3774 let mut src = Tree::new();
3776 src.insert("a.txt".into(), entry("a1"));
3777 repo.trees.insert("src".into(), src);
3778 let mut dst = Tree::new();
3780 dst.insert("a.txt".into(), entry("a1"));
3781 dst.insert("b.txt".into(), entry("b1"));
3782 dst.insert("c.txt".into(), entry("c1"));
3783 repo.trees.insert("dst".into(), dst);
3784
3785 let merged = merge_trees(&repo, "src", "dst", "").expect("mergeable");
3786 assert!(!merged.contains_key("b.txt"), "source deletion must win");
3787 assert!(merged.contains_key("a.txt"));
3788 assert!(merged.contains_key("c.txt"));
3789 }
3790
3791 #[test]
3792 fn three_way_merge_flags_true_conflict() {
3793 let mut repo = Repo::default();
3794 repo.commits.insert("base".into(), commit(&[]));
3795 repo.commits.insert("src".into(), commit(&["base"]));
3796 repo.commits.insert("dst".into(), commit(&["base"]));
3797 let mut base = Tree::new();
3798 base.insert("a.txt".into(), entry("a1"));
3799 repo.trees.insert("base".into(), base);
3800 let mut src = Tree::new();
3801 src.insert("a.txt".into(), entry("a-src"));
3802 repo.trees.insert("src".into(), src);
3803 let mut dst = Tree::new();
3804 dst.insert("a.txt".into(), entry("a-dst"));
3805 repo.trees.insert("dst".into(), dst);
3806 assert!(merge_trees(&repo, "src", "dst", "").is_none());
3808 let empty = Tree::new();
3809 let (_, bt, st, dt) = merge_inputs(&repo, "src", "dst", &empty);
3810 assert_eq!(conflicting_paths_in(bt, st, dt), vec!["a.txt"]);
3811 let merged = merge_trees(&repo, "src", "dst", "ACCEPT_SOURCE").expect("resolved");
3813 assert_eq!(merged.get("a.txt").unwrap().blob_id, "a-src");
3814 }
3815}
3816
3817#[cfg(test)]
3818mod handler_tests {
3819 use super::*;
3820 use bytes::Bytes;
3821 use fakecloud_core::auth::{Principal, PrincipalType};
3822 use fakecloud_core::multi_account::MultiAccountState;
3823 use http::{HeaderMap, Method};
3824 use parking_lot::RwLock;
3825 use std::collections::HashMap;
3826
3827 fn svc() -> CodeCommitService {
3828 CodeCommitService::new(Arc::new(RwLock::new(MultiAccountState::new(
3829 "000000000000",
3830 "us-east-1",
3831 "",
3832 ))))
3833 }
3834
3835 fn req_as(action: &str, body: Value, arn: Option<&str>) -> AwsRequest {
3836 let principal = arn.map(|a| Principal {
3837 arn: a.to_string(),
3838 user_id: "AIDATEST".to_string(),
3839 account_id: "000000000000".to_string(),
3840 principal_type: PrincipalType::User,
3841 source_identity: None,
3842 tags: None,
3843 });
3844 AwsRequest {
3845 service: "codecommit".into(),
3846 action: action.into(),
3847 region: "us-east-1".into(),
3848 account_id: "000000000000".into(),
3849 request_id: "req".into(),
3850 headers: HeaderMap::new(),
3851 query_params: HashMap::new(),
3852 body: Bytes::from(serde_json::to_vec(&body).unwrap()),
3853 body_stream: parking_lot::Mutex::new(None),
3854 path_segments: vec![],
3855 raw_path: String::new(),
3856 raw_query: String::new(),
3857 method: Method::POST,
3858 is_query_protocol: false,
3859 access_key_id: None,
3860 principal,
3861 }
3862 }
3863
3864 fn call(s: &CodeCommitService, action: &str, body: Value) -> Value {
3865 let resp = s
3866 .dispatch(action, &req_as(action, body, None))
3867 .expect("op ok");
3868 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
3869 }
3870
3871 fn call_as(s: &CodeCommitService, action: &str, body: Value, arn: &str) -> Value {
3872 let resp = s
3873 .dispatch(action, &req_as(action, body, Some(arn)))
3874 .expect("op ok");
3875 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
3876 }
3877
3878 fn call_err(s: &CodeCommitService, action: &str, body: Value) -> AwsServiceError {
3879 s.dispatch(action, &req_as(action, body, None))
3880 .err()
3881 .expect("op err")
3882 }
3883
3884 fn b64(s: &str) -> String {
3885 base64::engine::general_purpose::STANDARD.encode(s.as_bytes())
3886 }
3887
3888 fn make_repo(s: &CodeCommitService) {
3890 call(s, "CreateRepository", json!({ "repositoryName": "repo" }));
3891 }
3892
3893 fn put(
3895 s: &CodeCommitService,
3896 branch: &str,
3897 path: &str,
3898 content: &str,
3899 parent: Option<&str>,
3900 ) -> String {
3901 let mut body = json!({
3902 "repositoryName": "repo",
3903 "branchName": branch,
3904 "filePath": path,
3905 "fileContent": b64(content),
3906 });
3907 if let Some(p) = parent {
3908 body["parentCommitId"] = json!(p);
3909 }
3910 call(s, "PutFile", body)["commitId"]
3911 .as_str()
3912 .unwrap()
3913 .to_string()
3914 }
3915
3916 fn diverged_repo(s: &CodeCommitService) -> (String, String) {
3919 make_repo(s);
3920 let c1 = put(s, "main", "a.txt", "base", None);
3921 call(
3922 s,
3923 "CreateBranch",
3924 json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c1 }),
3925 );
3926 let feature_tip = put(s, "feature", "b.txt", "from-feature", Some(&c1));
3927 let main_tip = put(s, "main", "c.txt", "from-main", Some(&c1));
3928 (feature_tip, main_tip)
3929 }
3930
3931 fn create_pr(s: &CodeCommitService, dest: &str) -> Value {
3932 call(
3933 s,
3934 "CreatePullRequest",
3935 json!({
3936 "title": "PR",
3937 "targets": [{
3938 "repositoryName": "repo",
3939 "sourceReference": "feature",
3940 "destinationReference": dest,
3941 }],
3942 }),
3943 )["pullRequest"]
3944 .clone()
3945 }
3946
3947 #[test]
3950 fn merge_pr_three_way_advances_branch_and_writes_commit() {
3951 let s = svc();
3952 diverged_repo(&s);
3953 let pr = create_pr(&s, "main");
3954 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
3955
3956 let out = call(
3957 &s,
3958 "MergePullRequestByThreeWay",
3959 json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
3960 );
3961 let target = &out["pullRequest"]["pullRequestTargets"][0];
3962 assert_eq!(target["mergeMetadata"]["isMerged"], json!(true));
3963 let merge_commit = target["mergeMetadata"]["mergeCommitId"]
3964 .as_str()
3965 .unwrap()
3966 .to_string();
3967 assert!(is_object_id(&merge_commit));
3968 assert_eq!(out["pullRequest"]["pullRequestStatus"], json!("CLOSED"));
3969
3970 let branch = call(
3972 &s,
3973 "GetBranch",
3974 json!({ "repositoryName": "repo", "branchName": "main" }),
3975 );
3976 assert_eq!(branch["branch"]["commitId"], json!(merge_commit));
3977
3978 let got = call(
3980 &s,
3981 "GetCommit",
3982 json!({ "repositoryName": "repo", "commitId": merge_commit }),
3983 );
3984 assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 2);
3985 }
3986
3987 #[test]
3990 fn merge_pr_squash_deletes_file_and_has_one_parent() {
3991 let s = svc();
3992 make_repo(&s);
3993 let c1 = put(&s, "main", "a.txt", "a", None);
3994 let c2 = put(&s, "main", "b.txt", "b", Some(&c1));
3995 call(
3996 &s,
3997 "CreateBranch",
3998 json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c2 }),
3999 );
4000 call(
4002 &s,
4003 "DeleteFile",
4004 json!({ "repositoryName": "repo", "branchName": "feature", "filePath": "b.txt", "parentCommitId": c2 }),
4005 );
4006 put(&s, "main", "c.txt", "c", Some(&c2));
4007
4008 let pr = create_pr(&s, "main");
4009 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4010 let out = call(
4011 &s,
4012 "MergePullRequestBySquash",
4013 json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
4014 );
4015 let merge_commit = out["pullRequest"]["pullRequestTargets"][0]["mergeMetadata"]
4016 ["mergeCommitId"]
4017 .as_str()
4018 .unwrap()
4019 .to_string();
4020 let got = call(
4022 &s,
4023 "GetCommit",
4024 json!({ "repositoryName": "repo", "commitId": merge_commit }),
4025 );
4026 assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4027 let err = call_err(
4029 &s,
4030 "GetFile",
4031 json!({ "repositoryName": "repo", "commitSpecifier": "main", "filePath": "b.txt" }),
4032 );
4033 assert_eq!(err.code(), "FileDoesNotExistException");
4034 }
4035
4036 #[test]
4038 fn merge_branches_squash_single_parent() {
4039 let s = svc();
4040 let (feature_tip, main_tip) = diverged_repo(&s);
4041 let out = call(
4042 &s,
4043 "MergeBranchesBySquash",
4044 json!({
4045 "repositoryName": "repo",
4046 "sourceCommitSpecifier": feature_tip,
4047 "destinationCommitSpecifier": main_tip,
4048 "targetBranch": "main",
4049 }),
4050 );
4051 let cid = out["commitId"].as_str().unwrap().to_string();
4052 let got = call(
4053 &s,
4054 "GetCommit",
4055 json!({ "repositoryName": "repo", "commitId": cid }),
4056 );
4057 assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4058 }
4059
4060 #[test]
4063 fn template_applied_and_two_approvals_needed() {
4064 let s = svc();
4065 let content = "{\"Version\":\"2018-11-08\",\"Statements\":[{\"Type\":\"Approvers\",\"NumberOfApprovalsNeeded\":2,\"ApprovalPoolMembers\":[]}]}";
4066 call(
4067 &s,
4068 "CreateApprovalRuleTemplate",
4069 json!({ "approvalRuleTemplateName": "two", "approvalRuleTemplateContent": content }),
4070 );
4071 diverged_repo(&s);
4072 call(
4073 &s,
4074 "AssociateApprovalRuleTemplateWithRepository",
4075 json!({ "approvalRuleTemplateName": "two", "repositoryName": "repo" }),
4076 );
4077 let pr = create_pr(&s, "main");
4078 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4079 let revision = pr["revisionId"].as_str().unwrap().to_string();
4080
4081 let rules = pr["approvalRules"].as_array().unwrap();
4083 assert_eq!(rules.len(), 1);
4084 assert_eq!(rules[0]["approvalRuleName"], json!("two"));
4085 assert!(rules[0]["originApprovalRuleTemplate"].is_object());
4086
4087 call_as(
4089 &s,
4090 "UpdatePullRequestApprovalState",
4091 json!({ "pullRequestId": pr_id, "revisionId": revision, "approvalState": "APPROVE" }),
4092 "arn:aws:iam::000000000000:user/reviewer",
4093 );
4094 let eval = call(
4095 &s,
4096 "EvaluatePullRequestApprovalRules",
4097 json!({ "pullRequestId": pr_id, "revisionId": revision }),
4098 );
4099 assert_eq!(eval["evaluation"]["approved"], json!(false));
4100 assert_eq!(
4101 eval["evaluation"]["approvalRulesNotSatisfied"],
4102 json!(["two"])
4103 );
4104 }
4105
4106 #[test]
4108 fn closed_pr_cannot_reopen() {
4109 let s = svc();
4110 diverged_repo(&s);
4111 let pr = create_pr(&s, "main");
4112 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4113 call(
4114 &s,
4115 "UpdatePullRequestStatus",
4116 json!({ "pullRequestId": pr_id, "pullRequestStatus": "CLOSED" }),
4117 );
4118 let err = call_err(
4119 &s,
4120 "UpdatePullRequestStatus",
4121 json!({ "pullRequestId": pr_id, "pullRequestStatus": "OPEN" }),
4122 );
4123 assert_eq!(err.code(), "InvalidPullRequestStatusUpdateException");
4124 }
4125
4126 #[test]
4128 fn create_pr_rejects_missing_destination() {
4129 let s = svc();
4130 diverged_repo(&s);
4131 let err = call_err(
4132 &s,
4133 "CreatePullRequest",
4134 json!({
4135 "title": "PR",
4136 "targets": [{
4137 "repositoryName": "repo",
4138 "sourceReference": "feature",
4139 "destinationReference": "does-not-exist",
4140 }],
4141 }),
4142 );
4143 assert_eq!(err.code(), "ReferenceDoesNotExistException");
4144 }
4145
4146 #[test]
4148 fn describe_and_batch_report_conflict() {
4149 let s = svc();
4150 make_repo(&s);
4151 let c1 = put(&s, "main", "a.txt", "base-content", None);
4152 call(
4153 &s,
4154 "CreateBranch",
4155 json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c1 }),
4156 );
4157 put(&s, "feature", "a.txt", "feature-side", Some(&c1));
4158 put(&s, "main", "a.txt", "main-side", Some(&c1));
4159
4160 let base_body = json!({
4161 "repositoryName": "repo",
4162 "sourceCommitSpecifier": "feature",
4163 "destinationCommitSpecifier": "main",
4164 "mergeOption": "THREE_WAY_MERGE",
4165 });
4166
4167 let mut d = base_body.clone();
4168 d["filePath"] = json!("a.txt");
4169 let desc = call(&s, "DescribeMergeConflicts", d);
4170 assert_eq!(desc["conflictMetadata"]["numberOfConflicts"], json!(1));
4171 assert_eq!(desc["conflictMetadata"]["contentConflict"], json!(true));
4172 assert_eq!(desc["mergeHunks"][0]["isConflict"], json!(true));
4173
4174 let batch = call(&s, "BatchDescribeMergeConflicts", base_body.clone());
4175 let conflicts = batch["conflicts"].as_array().unwrap();
4176 assert_eq!(conflicts.len(), 1);
4177 assert_eq!(conflicts[0]["conflictMetadata"]["filePath"], json!("a.txt"));
4178
4179 let gmc = call(&s, "GetMergeConflicts", base_body);
4181 assert_eq!(gmc["mergeable"], json!(false));
4182 assert_eq!(gmc["conflictMetadataList"][0]["filePath"], json!("a.txt"));
4183 }
4184
4185 #[test]
4188 fn merge_pr_fast_forward_non_ff_rejected() {
4189 let s = svc();
4190 diverged_repo(&s);
4191 let pr = create_pr(&s, "main");
4192 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4193 let err = call_err(
4194 &s,
4195 "MergePullRequestByFastForward",
4196 json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
4197 );
4198 assert_eq!(err.code(), "ManualMergeRequiredException");
4199 let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4201 assert_eq!(got["pullRequest"]["pullRequestStatus"], json!("OPEN"));
4202 }
4203
4204 fn branch_tip(s: &CodeCommitService, branch: &str) -> String {
4205 call(
4206 s,
4207 "GetBranch",
4208 json!({ "repositoryName": "repo", "branchName": branch }),
4209 )["branch"]["commitId"]
4210 .as_str()
4211 .unwrap()
4212 .to_string()
4213 }
4214
4215 fn one_approval_rule() -> &'static str {
4216 "{\"Version\":\"2018-11-08\",\"Statements\":[{\"Type\":\"Approvers\",\"NumberOfApprovalsNeeded\":1,\"ApprovalPoolMembers\":[]}]}"
4217 }
4218
4219 #[test]
4222 fn new_source_commit_bumps_revision_and_invalidates_approvals() {
4223 let s = svc();
4224 let (feature_tip, _) = diverged_repo(&s);
4225 let pr = create_pr(&s, "main");
4226 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4227 let rev1 = pr["revisionId"].as_str().unwrap().to_string();
4228 call(
4229 &s,
4230 "CreatePullRequestApprovalRule",
4231 json!({
4232 "pullRequestId": pr_id,
4233 "approvalRuleName": "r",
4234 "approvalRuleContent": one_approval_rule(),
4235 }),
4236 );
4237 call_as(
4239 &s,
4240 "UpdatePullRequestApprovalState",
4241 json!({ "pullRequestId": pr_id, "revisionId": rev1, "approvalState": "APPROVE" }),
4242 "arn:aws:iam::000000000000:user/reviewer",
4243 );
4244 let eval1 = call(
4245 &s,
4246 "EvaluatePullRequestApprovalRules",
4247 json!({ "pullRequestId": pr_id, "revisionId": rev1 }),
4248 );
4249 assert_eq!(eval1["evaluation"]["approved"], json!(true));
4250
4251 put(&s, "feature", "d.txt", "more", Some(&feature_tip));
4253 let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4254 let rev2 = got["pullRequest"]["revisionId"]
4255 .as_str()
4256 .unwrap()
4257 .to_string();
4258 assert_ne!(rev1, rev2, "source advance must re-mint the revisionId");
4259
4260 let eval2 = call(
4262 &s,
4263 "EvaluatePullRequestApprovalRules",
4264 json!({ "pullRequestId": pr_id, "revisionId": rev2 }),
4265 );
4266 assert_eq!(eval2["evaluation"]["approved"], json!(false));
4267 }
4268
4269 #[test]
4271 fn get_pull_request_reflects_advanced_source_tip() {
4272 let s = svc();
4273 let (feature_tip, _) = diverged_repo(&s);
4274 let pr = create_pr(&s, "main");
4275 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4276 assert_eq!(
4277 pr["pullRequestTargets"][0]["sourceCommit"],
4278 json!(feature_tip)
4279 );
4280 put(&s, "feature", "e.txt", "x", Some(&feature_tip));
4281 let new_tip = branch_tip(&s, "feature");
4282 let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4283 assert_eq!(
4284 got["pullRequest"]["pullRequestTargets"][0]["sourceCommit"],
4285 json!(new_tip)
4286 );
4287 }
4288
4289 #[test]
4291 fn create_unreferenced_squash_single_parent() {
4292 let s = svc();
4293 diverged_repo(&s);
4294 let out = call(
4295 &s,
4296 "CreateUnreferencedMergeCommit",
4297 json!({
4298 "repositoryName": "repo",
4299 "sourceCommitSpecifier": "feature",
4300 "destinationCommitSpecifier": "main",
4301 "mergeOption": "SQUASH_MERGE",
4302 }),
4303 );
4304 let cid = out["commitId"].as_str().unwrap().to_string();
4305 let got = call(
4306 &s,
4307 "GetCommit",
4308 json!({ "repositoryName": "repo", "commitId": cid }),
4309 );
4310 assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4311 }
4312
4313 #[test]
4316 fn invalid_conflict_strategy_rejected() {
4317 let s = svc();
4318 diverged_repo(&s);
4319 let pr = create_pr(&s, "main");
4320 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4321 let e1 = call_err(
4322 &s,
4323 "MergePullRequestByThreeWay",
4324 json!({
4325 "pullRequestId": pr_id,
4326 "repositoryName": "repo",
4327 "conflictResolutionStrategy": "BOGUS",
4328 }),
4329 );
4330 assert_eq!(e1.code(), "InvalidConflictResolutionStrategyException");
4331 let e2 = call_err(
4332 &s,
4333 "CreateUnreferencedMergeCommit",
4334 json!({
4335 "repositoryName": "repo",
4336 "sourceCommitSpecifier": "feature",
4337 "destinationCommitSpecifier": "main",
4338 "mergeOption": "THREE_WAY_MERGE",
4339 "conflictResolutionStrategy": "BOGUS",
4340 }),
4341 );
4342 assert_eq!(e2.code(), "InvalidConflictResolutionStrategyException");
4343 }
4344
4345 #[test]
4347 fn open_to_open_status_rejected() {
4348 let s = svc();
4349 diverged_repo(&s);
4350 let pr = create_pr(&s, "main");
4351 let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4352 let err = call_err(
4353 &s,
4354 "UpdatePullRequestStatus",
4355 json!({ "pullRequestId": pr_id, "pullRequestStatus": "OPEN" }),
4356 );
4357 assert_eq!(err.code(), "InvalidPullRequestStatusUpdateException");
4358 }
4359
4360 #[test]
4363 fn test_triggers_reports_unresolvable_destination() {
4364 let s = svc();
4365 make_repo(&s);
4366 let out = call(
4367 &s,
4368 "TestRepositoryTriggers",
4369 json!({
4370 "repositoryName": "repo",
4371 "triggers": [
4372 {
4373 "name": "ok",
4374 "destinationArn": "arn:aws:sns:us-east-1:000000000000:topic",
4375 "events": ["all"],
4376 },
4377 {
4378 "name": "bad",
4379 "destinationArn": "arn:aws:s3:::some-bucket",
4380 "events": ["all"],
4381 },
4382 ],
4383 }),
4384 );
4385 assert_eq!(out["successfulExecutions"], json!(["ok"]));
4386 let failed = out["failedExecutions"].as_array().unwrap();
4387 assert_eq!(failed.len(), 1);
4388 assert_eq!(failed[0]["trigger"], json!("bad"));
4389 assert!(failed[0]["failureMessage"].as_str().unwrap().contains("s3"));
4390 }
4391
4392 #[test]
4394 fn delete_repository_clears_template_association() {
4395 let s = svc();
4396 make_repo(&s);
4397 call(
4398 &s,
4399 "CreateApprovalRuleTemplate",
4400 json!({
4401 "approvalRuleTemplateName": "t",
4402 "approvalRuleTemplateContent": one_approval_rule(),
4403 }),
4404 );
4405 call(
4406 &s,
4407 "AssociateApprovalRuleTemplateWithRepository",
4408 json!({ "approvalRuleTemplateName": "t", "repositoryName": "repo" }),
4409 );
4410 let before = call(
4411 &s,
4412 "ListRepositoriesForApprovalRuleTemplate",
4413 json!({ "approvalRuleTemplateName": "t" }),
4414 );
4415 assert_eq!(before["repositoryNames"], json!(["repo"]));
4416 call(&s, "DeleteRepository", json!({ "repositoryName": "repo" }));
4417 let after = call(
4418 &s,
4419 "ListRepositoriesForApprovalRuleTemplate",
4420 json!({ "approvalRuleTemplateName": "t" }),
4421 );
4422 assert_eq!(after["repositoryNames"], json!([]));
4423 }
4424
4425 #[test]
4426 fn list_file_commit_history_filters_by_file_path() {
4427 let s = svc();
4430 make_repo(&s);
4431 let c1 = put(&s, "main", "a.txt", "a1", None);
4433 let c2 = put(&s, "main", "b.txt", "b1", Some(&c1));
4434 let c3 = put(&s, "main", "a.txt", "a2", Some(&c2));
4435
4436 let hist = call(
4438 &s,
4439 "ListFileCommitHistory",
4440 json!({
4441 "repositoryName": "repo",
4442 "commitSpecifier": "main",
4443 "filePath": "a.txt"
4444 }),
4445 );
4446 let commits: Vec<&str> = hist["revisionDag"]
4447 .as_array()
4448 .unwrap()
4449 .iter()
4450 .map(|d| d["commit"].as_str().unwrap())
4451 .collect();
4452 assert_eq!(
4453 commits,
4454 vec![c3.as_str(), c1.as_str()],
4455 "only a.txt commits"
4456 );
4457
4458 let hist_b = call(
4460 &s,
4461 "ListFileCommitHistory",
4462 json!({
4463 "repositoryName": "repo",
4464 "commitSpecifier": "main",
4465 "filePath": "b.txt"
4466 }),
4467 );
4468 let commits_b: Vec<&str> = hist_b["revisionDag"]
4469 .as_array()
4470 .unwrap()
4471 .iter()
4472 .map(|d| d["commit"].as_str().unwrap())
4473 .collect();
4474 assert_eq!(commits_b, vec![c2.as_str()], "only b.txt commit");
4475 }
4476}