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