1use std::collections::BTreeMap;
22
23use onetaskgraph_plugin_api::{
24 DependencyEdge, DependencyEndpoint, DependencyKind, Direction, ItemKind, ItemWrite, NativeId,
25 Page, PageRequest, Project, ProjectQuery, Repository, SourceError, SourceName, Task, TaskQuery,
26};
27use schemars::JsonSchema;
28use serde::{Deserialize, Serialize};
29use serde_json::Value;
30
31use crate::GlobalId;
32use crate::resolve::ResolvedSource;
33
34use super::local::ProjectSelector;
35use super::{Engine, EngineError, Filters, Paging, TaskRequest};
36
37#[derive(Debug, Clone)]
39pub struct CopyRequest {
40 pub items: CopyItems,
42 pub scope: CopyScope,
44 pub destination: SourceName,
46 pub match_by: Option<MatchBy>,
48 pub recreate: bool,
51 pub dry_run: bool,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct CopyItems(Vec<GlobalId>);
63
64impl CopyItems {
65 #[must_use]
67 pub fn new(items: Vec<GlobalId>) -> Option<Self> {
68 (!items.is_empty()).then_some(Self(items))
69 }
70
71 #[must_use]
73 pub fn as_slice(&self) -> &[GlobalId] {
74 &self.0
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum CopyScope {
85 Tasks,
87 Projects {
89 tasks: bool,
91 },
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum MatchBy {
101 Title,
103 Metadata(String),
105}
106
107impl MatchBy {
108 #[must_use]
110 pub fn parse(key: &str) -> Self {
111 if key == "title" {
112 Self::Title
113 } else {
114 Self::Metadata(key.to_owned())
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
124pub struct CopyReport {
125 pub items: Vec<CopyOutcome>,
127}
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
137pub struct CopyOutcome {
138 pub source: GlobalId,
140 #[serde(flatten)]
142 pub action: CopyAction,
143}
144
145impl CopyOutcome {
146 #[must_use]
148 pub fn destination(&self) -> Option<&GlobalId> {
149 self.action.destination()
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
155#[serde(tag = "action", rename_all = "kebab-case")]
156pub enum CopyAction {
157 Created {
162 destination: Option<GlobalId>,
165 },
166 Updated {
168 destination: GlobalId,
170 },
171 Unchanged {
173 destination: GlobalId,
175 },
176 Orphaned {
179 destination: GlobalId,
181 },
182}
183
184impl CopyAction {
185 #[must_use]
187 pub fn destination(&self) -> Option<&GlobalId> {
188 match self {
189 Self::Created { destination } => destination.as_ref(),
190 Self::Updated { destination }
191 | Self::Unchanged { destination }
192 | Self::Orphaned { destination } => Some(destination),
193 }
194 }
195
196 #[must_use]
202 pub fn name(&self) -> String {
203 serde_json::to_value(self).expect("a contract enum serialises")["action"]
204 .as_str()
205 .expect("an internally tagged enum carries its tag")
206 .to_owned()
207 }
208}
209
210enum Target {
212 Update(NativeId),
214 Create,
216}
217
218enum Wanted {
220 Origin(String),
222 Title(String),
224 Metadata(String, Value),
226}
227
228impl Wanted {
229 fn found(&self, title: &str, metadata: &BTreeMap<String, Value>) -> bool {
231 match self {
232 Self::Origin(id) => {
233 metadata.get(GlobalId::ORIGIN_KEY) == Some(&Value::String(id.clone()))
234 }
235 Self::Title(wanted) => title == wanted,
236 Self::Metadata(key, value) => metadata.get(key) == Some(value),
237 }
238 }
239}
240
241struct Planned {
243 source: GlobalId,
245 item: Item,
247 edges: Vec<DependencyEdge>,
249 target: Target,
251}
252
253enum Item {
255 Task(Box<Task>),
257 Project(Box<Project>),
259}
260
261impl Item {
262 fn id(&self) -> &NativeId {
263 match self {
264 Self::Task(task) => &task.id,
265 Self::Project(project) => &project.id,
266 }
267 }
268
269 fn kind(&self) -> ItemKind {
270 match self {
271 Self::Task(_) => ItemKind::Task,
272 Self::Project(_) => ItemKind::Project,
273 }
274 }
275}
276
277impl Engine {
278 pub async fn copy(&self, request: &CopyRequest) -> Result<CopyReport, EngineError> {
292 let destination = self.writable(&request.destination)?;
293 match request.scope {
294 CopyScope::Tasks => Ok(CopyReport {
298 items: self
299 .copy_items(
300 destination,
301 request,
302 ItemKind::Task,
303 request.items.as_slice(),
304 None,
305 )
306 .await?,
307 }),
308 CopyScope::Projects { tasks } => {
309 let mut items = Vec::new();
310 for id in request.items.as_slice() {
311 items.extend(self.copy_project(destination, request, id, tasks).await?);
312 }
313 Ok(CopyReport { items })
314 }
315 }
316 }
317
318 fn writable(&self, name: &SourceName) -> Result<&ResolvedSource, EngineError> {
320 let name = self.known(name)?;
321 if let Some(unavailable) = self.unavailable().find(|source| source.name() == &name) {
322 return Err(EngineError::DestinationUnavailable {
323 name: name.to_string(),
324 error: unavailable.error().clone(),
325 });
326 }
327 let source = self
328 .ready()
329 .find(|source| source.name() == &name)
330 .ok_or(EngineError::NoSources)?;
331 if !source.source().writes().is_supported() {
332 return Err(EngineError::NotWritable {
333 name: name.to_string(),
334 kind: source.kind().to_owned(),
335 });
336 }
337 Ok(source)
338 }
339
340 async fn copy_project(
342 &self,
343 destination: &ResolvedSource,
344 request: &CopyRequest,
345 id: &GlobalId,
346 tasks: bool,
347 ) -> Result<Vec<CopyOutcome>, EngineError> {
348 let members = if tasks {
349 self.project_members(id).await?
350 } else {
351 Vec::new()
352 };
353 let project_plan = self
357 .plan(destination, request, ItemKind::Project, id)
358 .await?;
359 let mut copied = vec![id.clone()];
360 copied.extend(members.iter().cloned());
361 let mut known = BTreeMap::new();
362 if let Target::Update(target) = &project_plan.target {
363 known.insert(id.to_string(), target.clone());
364 }
365 for member in &members {
366 let member_plan = self
367 .plan(destination, request, ItemKind::Task, member)
368 .await?;
369 if let Target::Update(target) = member_plan.target {
370 known.insert(member.to_string(), target);
371 }
372 }
373 let project_was_unchanged = if let Target::Update(target) = &project_plan.target {
374 let edges = mapped_edges(
375 &project_plan.edges,
376 &id.source,
377 destination,
378 &copied,
379 &known,
380 );
381 !edges.iter().any(Option::is_none)
382 && !self
383 .changes(destination, &project_plan, target, &None, &resolved(&edges))
384 .await?
385 } else {
386 false
387 };
388 let mut outcomes = self
389 .copy_items(
390 destination,
391 request,
392 ItemKind::Project,
393 std::slice::from_ref(id),
394 None,
395 )
396 .await?;
397 if !tasks {
398 return Ok(outcomes);
399 }
400 let project = outcomes.first().and_then(CopyOutcome::destination).cloned();
404 let task_outcomes = self
405 .copy_items(
406 destination,
407 request,
408 ItemKind::Task,
409 &members,
410 project.as_ref().map(|project| project.native.clone()),
411 )
412 .await?;
413 outcomes.extend(task_outcomes);
414 if let Some(project) = project {
415 if !request.dry_run {
416 let planned = self
420 .plan(destination, request, ItemKind::Project, id)
421 .await?;
422 let mut copied = vec![id.clone()];
423 copied.extend(members.iter().cloned());
424 let written: BTreeMap<String, NativeId> = outcomes
425 .iter()
426 .filter_map(|outcome| {
427 outcome.destination().map(|destination| {
428 (outcome.source.to_string(), destination.native.clone())
429 })
430 })
431 .collect();
432 let edges =
433 mapped_edges(&planned.edges, &id.source, destination, &copied, &written);
434 self.write(
435 destination,
436 &planned,
437 Some(project.native.clone()),
438 self.filed(destination, &planned, None).await?,
439 &resolved(&edges),
440 )
441 .await?;
442 }
443 if project_was_unchanged {
444 outcomes[0].action = CopyAction::Unchanged {
445 destination: project.clone(),
446 };
447 }
448 outcomes.extend(
449 self.orphans(destination, id, &project.native, &members)
450 .await?,
451 );
452 }
453 Ok(outcomes)
454 }
455
456 async fn project_members(&self, project: &GlobalId) -> Result<Vec<GlobalId>, EngineError> {
458 let mut request = TaskRequest {
459 sources: vec![project.source.clone()],
460 filters: Filters::default(),
461 project: ProjectSelector::Qualified(project.clone()),
462 paging: Paging {
463 limit: PROJECT_PAGE,
464 token: None,
465 },
466 };
467 let mut members = Vec::new();
468 loop {
469 let response = self.tasks(&request).await?;
470 if let Some(failure) = response.errors.first() {
471 return Err(EngineError::SourceRefused {
472 name: failure.source.to_string(),
473 error: failure.error.clone(),
474 });
475 }
476 members.extend(response.items.into_iter().map(|task| task.id));
477 match response.next {
478 Some(token) => request.paging.token = Some(token),
479 None => return Ok(members),
480 }
481 }
482 }
483
484 async fn orphans(
489 &self,
490 destination: &ResolvedSource,
491 project: &GlobalId,
492 at_destination: &NativeId,
493 copied: &[GlobalId],
494 ) -> Result<Vec<CopyOutcome>, EngineError> {
495 let mut orphans = Vec::new();
496 let mut cursor = None;
497 loop {
498 let page: Page<Task> = destination
499 .source()
500 .query_tasks(&TaskQuery::default(), &request_for(destination, cursor))
501 .await
502 .map_err(|error| refused(destination, error))?;
503 for task in &page.items {
504 if task.project.as_ref() != Some(at_destination) {
505 continue;
506 }
507 let Some(origin) = origin_of(&task.metadata) else {
508 continue;
509 };
510 if origin.source != project.source || copied.contains(&origin) {
511 continue;
512 }
513 orphans.push(CopyOutcome {
514 source: origin,
515 action: CopyAction::Orphaned {
516 destination: GlobalId::new(destination.name().clone(), task.id.clone()),
517 },
518 });
519 }
520 match page.next {
521 Some(next) => cursor = Some(next),
522 None => return Ok(orphans),
523 }
524 }
525 }
526
527 async fn copy_items(
533 &self,
534 destination: &ResolvedSource,
535 request: &CopyRequest,
536 kind: ItemKind,
537 items: &[GlobalId],
538 project: Option<NativeId>,
539 ) -> Result<Vec<CopyOutcome>, EngineError> {
540 let mut planned = Vec::new();
541 for id in items {
542 planned.push(self.plan(destination, request, kind, id).await?);
543 }
544
545 let copied: Vec<GlobalId> = planned.iter().map(|item| item.source.clone()).collect();
546 let mut written: BTreeMap<String, NativeId> = BTreeMap::new();
550 for item in &planned {
551 if let Target::Update(id) = &item.target {
552 written.insert(item.source.to_string(), id.clone());
553 }
554 }
555
556 let mut filed = Vec::new();
559 for item in &planned {
560 filed.push(self.filed(destination, item, project.clone()).await?);
561 }
562
563 let mut outcomes = Vec::new();
564 let mut deferred = Vec::new();
565 for (index, item) in planned.iter().enumerate() {
566 let edges = mapped_edges(
567 &item.edges,
568 &item.source.source,
569 destination,
570 &copied,
571 &written,
572 );
573 if edges.iter().any(Option::is_none) {
574 deferred.push(index);
575 }
576 let outcome = self
577 .land(destination, request, item, filed[index].clone(), &edges)
578 .await?;
579 if let Some(id) = outcome.destination() {
580 written.insert(item.source.to_string(), id.native.clone());
581 }
582 outcomes.push(outcome);
583 }
584
585 if request.dry_run {
586 return Ok(outcomes);
587 }
588 for index in deferred {
589 let item = &planned[index];
590 let Some(id) = outcomes[index].destination().cloned() else {
591 continue;
592 };
593 let edges = mapped_edges(
594 &item.edges,
595 &item.source.source,
596 destination,
597 &copied,
598 &written,
599 );
600 self.write(
601 destination,
602 item,
603 Some(id.native),
604 filed[index].clone(),
605 &resolved(&edges),
606 )
607 .await?;
608 }
609 Ok(outcomes)
610 }
611
612 async fn plan(
614 &self,
615 destination: &ResolvedSource,
616 request: &CopyRequest,
617 kind: ItemKind,
618 id: &GlobalId,
619 ) -> Result<Planned, EngineError> {
620 let source = self.readable(&id.source)?;
621 let item = match kind {
622 ItemKind::Task => source
623 .source()
624 .get_task(&id.native)
625 .await
626 .map_err(|error| refused(source, error))?
627 .map(|task| Item::Task(Box::new(task))),
628 ItemKind::Project => source
629 .source()
630 .get_project(&id.native)
631 .await
632 .map_err(|error| refused(source, error))?
633 .map(|project| Item::Project(Box::new(project))),
634 }
635 .ok_or_else(|| EngineError::NoSuchItem { id: id.to_string() })?;
636 let edges = forward_edges(source, &id.native, item.kind()).await?;
637 let target = self.target(destination, request, id, &item).await?;
638 Ok(Planned {
639 source: id.clone(),
640 item,
641 edges,
642 target,
643 })
644 }
645
646 async fn target(
649 &self,
650 destination: &ResolvedSource,
651 request: &CopyRequest,
652 id: &GlobalId,
653 item: &Item,
654 ) -> Result<Target, EngineError> {
655 let (title, metadata) = described(item);
656 if let Some(origin) = origin_of(metadata)
657 && &origin.source == destination.name()
658 {
659 if exists(destination, &origin.native, item.kind()).await? {
660 return Ok(Target::Update(origin.native));
661 }
662 if !request.recreate {
663 return Err(EngineError::StaleOrigin {
664 item: id.to_string(),
665 origin: origin.to_string(),
666 });
667 }
668 }
669 if let Some(found) = self
670 .scan(destination, item.kind(), &Wanted::Origin(id.to_string()))
671 .await?
672 {
673 return Ok(Target::Update(found));
674 }
675 let wanted = match &request.match_by {
676 Some(MatchBy::Title) => Some(Wanted::Title(title.to_owned())),
677 Some(MatchBy::Metadata(key)) => metadata
678 .get(key)
679 .map(|value| Wanted::Metadata(key.clone(), value.clone())),
680 None => None,
681 };
682 if let Some(wanted) = wanted
683 && let Some(found) = self.scan(destination, item.kind(), &wanted).await?
684 {
685 return Ok(Target::Update(found));
686 }
687 Ok(Target::Create)
688 }
689
690 async fn scan(
695 &self,
696 destination: &ResolvedSource,
697 kind: ItemKind,
698 wanted: &Wanted,
699 ) -> Result<Option<NativeId>, EngineError> {
700 let mut cursor = None;
701 loop {
702 let next = match kind {
703 ItemKind::Task => {
704 let page = destination
705 .source()
706 .query_tasks(&TaskQuery::default(), &request_for(destination, cursor))
707 .await
708 .map_err(|error| refused(destination, error))?;
709 for task in &page.items {
710 if wanted.found(&task.title, &task.metadata) {
711 return Ok(Some(task.id.clone()));
712 }
713 }
714 page.next
715 }
716 ItemKind::Project => {
717 let page = destination
718 .source()
719 .query_projects(&ProjectQuery::default(), &request_for(destination, cursor))
720 .await
721 .map_err(|error| refused(destination, error))?;
722 for project in &page.items {
723 if wanted.found(&project.title, &project.metadata) {
724 return Ok(Some(project.id.clone()));
725 }
726 }
727 page.next
728 }
729 };
730 match next {
731 Some(next) => cursor = Some(next),
732 None => return Ok(None),
733 }
734 }
735 }
736
737 async fn land(
739 &self,
740 destination: &ResolvedSource,
741 request: &CopyRequest,
742 item: &Planned,
743 project: Option<NativeId>,
744 edges: &[Option<DependencyEdge>],
745 ) -> Result<CopyOutcome, EngineError> {
746 let target = match &item.target {
747 Target::Update(id) => Some(id.clone()),
748 Target::Create => None,
749 };
750 let edges = resolved(edges);
751 let qualified = |native: NativeId| GlobalId::new(destination.name().clone(), native);
752 if let Some(id) = &target
753 && !self
754 .changes(destination, item, id, &project, &edges)
755 .await?
756 {
757 return Ok(CopyOutcome {
758 source: item.source.clone(),
759 action: CopyAction::Unchanged {
760 destination: qualified(id.clone()),
761 },
762 });
763 }
764 if request.dry_run {
765 return Ok(CopyOutcome {
766 source: item.source.clone(),
767 action: match target {
768 Some(id) => CopyAction::Updated {
769 destination: qualified(id),
770 },
771 None => CopyAction::Created { destination: None },
773 },
774 });
775 }
776 let updating = target.is_some();
777 let written = qualified(
778 self.write(destination, item, target, project, &edges)
779 .await?,
780 );
781 Ok(CopyOutcome {
782 source: item.source.clone(),
783 action: if updating {
784 CopyAction::Updated {
785 destination: written,
786 }
787 } else {
788 CopyAction::Created {
789 destination: Some(written),
790 }
791 },
792 })
793 }
794
795 async fn filed(
800 &self,
801 destination: &ResolvedSource,
802 item: &Planned,
803 project: Option<NativeId>,
804 ) -> Result<Option<NativeId>, EngineError> {
805 match (&item.item, project) {
806 (Item::Task(task), None) => self.counterpart(destination, item, task).await,
807 (Item::Task(_), filed) => Ok(filed),
808 (Item::Project(_), _) => Ok(None),
809 }
810 }
811
812 async fn counterpart(
818 &self,
819 destination: &ResolvedSource,
820 item: &Planned,
821 task: &Task,
822 ) -> Result<Option<NativeId>, EngineError> {
823 let Some(project) = &task.project else {
824 return Ok(None);
825 };
826 let qualified = GlobalId::new(item.source.source.clone(), project.clone());
827 let found = self
828 .scan(
829 destination,
830 ItemKind::Project,
831 &Wanted::Origin(qualified.to_string()),
832 )
833 .await?;
834 Ok(Some(found.unwrap_or_else(|| project.clone())))
835 }
836
837 async fn changes(
839 &self,
840 destination: &ResolvedSource,
841 item: &Planned,
842 target: &NativeId,
843 project: &Option<NativeId>,
844 edges: &[DependencyEdge],
845 ) -> Result<bool, EngineError> {
846 let held = match &item.item {
847 Item::Task(_) => destination
848 .source()
849 .get_task(target)
850 .await
851 .map_err(|error| refused(destination, error))?
852 .map(|task| Item::Task(Box::new(task))),
853 Item::Project(_) => destination
854 .source()
855 .get_project(target)
856 .await
857 .map_err(|error| refused(destination, error))?
858 .map(|project| Item::Project(Box::new(project))),
859 };
860 let Some(held) = held else {
861 return Ok(true);
862 };
863 let outgoing = outgoing(item, target.clone(), project.clone());
864 if !same(&held, &outgoing) {
865 return Ok(true);
866 }
867 let at_destination = forward_edges(destination, target, item.item.kind()).await?;
868 Ok(!same_edges(&at_destination, edges))
869 }
870
871 async fn write(
873 &self,
874 destination: &ResolvedSource,
875 item: &Planned,
876 target: Option<NativeId>,
877 project: Option<NativeId>,
878 edges: &[DependencyEdge],
879 ) -> Result<NativeId, EngineError> {
880 let suggested = target.clone().unwrap_or_else(|| item.item.id().clone());
881 match outgoing(item, suggested, project) {
882 Item::Task(task) => destination
883 .source()
884 .write_task(&ItemWrite {
885 target,
886 item: *task,
887 depends_on: edges.to_vec(),
888 })
889 .await
890 .map_err(|error| refused(destination, error)),
891 Item::Project(project) => destination
892 .source()
893 .write_project(&ItemWrite {
894 target,
895 item: *project,
896 depends_on: edges.to_vec(),
897 })
898 .await
899 .map_err(|error| refused(destination, error)),
900 }
901 }
902
903 fn readable(&self, name: &SourceName) -> Result<&ResolvedSource, EngineError> {
905 let name = self.known(name)?;
906 if let Some(unavailable) = self.unavailable().find(|source| source.name() == &name) {
907 return Err(EngineError::SourceRefused {
908 name: name.to_string(),
909 error: unavailable.error().clone(),
910 });
911 }
912 self.ready()
913 .find(|source| source.name() == &name)
914 .ok_or(EngineError::NoSources)
915 }
916}
917
918const PROJECT_PAGE: std::num::NonZeroU32 = std::num::NonZeroU32::new(50).expect("50 is not zero");
920
921fn request_for(
923 source: &ResolvedSource,
924 cursor: Option<onetaskgraph_plugin_api::Cursor>,
925) -> PageRequest {
926 PageRequest {
927 cursor,
928 limit: source.source().capabilities().max_page_size.max(1),
929 }
930}
931
932fn refused(source: &ResolvedSource, error: SourceError) -> EngineError {
934 EngineError::SourceRefused {
935 name: source.name().to_string(),
936 error,
937 }
938}
939
940async fn forward_edges(
942 source: &ResolvedSource,
943 id: &NativeId,
944 kind: ItemKind,
945) -> Result<Vec<DependencyEdge>, EngineError> {
946 let mut edges = Vec::new();
947 let mut cursor = None;
948 loop {
949 let page = match kind {
950 ItemKind::Task => {
951 source
952 .source()
953 .task_dependencies(id, Direction::DependsOn, &request_for(source, cursor))
954 .await
955 }
956 ItemKind::Project => {
957 source
958 .source()
959 .project_dependencies(id, Direction::DependsOn, &request_for(source, cursor))
960 .await
961 }
962 }
963 .map_err(|error| refused(source, error))?;
964 edges.extend(page.items);
965 match page.next {
966 Some(next) => cursor = Some(next),
967 None => return Ok(edges),
968 }
969 }
970}
971
972fn origin_of(metadata: &BTreeMap<String, Value>) -> Option<GlobalId> {
974 metadata
975 .get(GlobalId::ORIGIN_KEY)?
976 .as_str()?
977 .parse::<GlobalId>()
978 .ok()
979}
980
981fn described(item: &Item) -> (&str, &BTreeMap<String, Value>) {
983 match item {
984 Item::Task(task) => (&task.title, &task.metadata),
985 Item::Project(project) => (&project.title, &project.metadata),
986 }
987}
988
989fn outgoing(item: &Planned, id: NativeId, project: Option<NativeId>) -> Item {
996 let origin = item.source.to_string();
997 match &item.item {
998 Item::Task(task) => Item::Task(Box::new(Task {
999 id,
1000 url: None,
1001 created_at: None,
1002 updated_at: None,
1003 project,
1004 metadata: carried(&task.metadata, &origin),
1005 ..(**task).clone()
1006 })),
1007 Item::Project(project) => Item::Project(Box::new(Project {
1008 id,
1009 url: None,
1010 created_at: None,
1011 updated_at: None,
1012 metadata: carried(&project.metadata, &origin),
1013 ..(**project).clone()
1014 })),
1015 }
1016}
1017
1018fn carried(metadata: &BTreeMap<String, Value>, origin: &str) -> BTreeMap<String, Value> {
1020 let mut carried = metadata.clone();
1021 carried.remove(Repository::METADATA_KEY);
1022 carried.remove(DependencyEdge::RECORDED_KEY);
1023 carried.insert(
1024 GlobalId::ORIGIN_KEY.to_owned(),
1025 Value::String(origin.to_owned()),
1026 );
1027 carried
1028}
1029
1030fn same(held: &Item, outgoing: &Item) -> bool {
1035 match (held, outgoing) {
1036 (Item::Task(held), Item::Task(outgoing)) => {
1037 held.title == outgoing.title
1038 && held.content == outgoing.content
1039 && held.status == outgoing.status
1040 && held.labels == outgoing.labels
1041 && held.project == outgoing.project
1042 && held.metadata == outgoing.metadata
1043 && held.repositories == outgoing.repositories
1044 }
1045 (Item::Project(held), Item::Project(outgoing)) => {
1046 held.title == outgoing.title
1047 && held.content == outgoing.content
1048 && held.status == outgoing.status
1049 && held.labels == outgoing.labels
1050 && held.metadata == outgoing.metadata
1051 && held.repositories == outgoing.repositories
1052 }
1053 _ => false,
1054 }
1055}
1056
1057fn same_edges(held: &[DependencyEdge], outgoing: &[DependencyEdge]) -> bool {
1059 let ends = |edges: &[DependencyEdge]| {
1060 let mut ends: Vec<(String, ItemKind, DependencyKind)> = edges
1061 .iter()
1062 .map(|edge| (edge.to.id().to_owned(), edge.to.kind, edge.kind))
1063 .collect();
1064 ends.sort_by(|left, right| left.0.cmp(&right.0));
1065 ends
1066 };
1067 ends(held) == ends(outgoing)
1068}
1069
1070fn mapped_edges(
1073 edges: &[DependencyEdge],
1074 origin: &SourceName,
1075 destination: &ResolvedSource,
1076 copied: &[GlobalId],
1077 written: &BTreeMap<String, NativeId>,
1078) -> Vec<Option<DependencyEdge>> {
1079 edges
1080 .iter()
1081 .map(|edge| {
1082 let far = GlobalId::new(origin.clone(), NativeId(edge.to.id().to_owned()));
1083 let id = if let Some(native) = names(&edge.to, destination.name()) {
1084 Some(native)
1090 } else if edge.to.is_qualified() || origin == destination.name() {
1091 Some(edge.to.id().to_owned())
1094 } else if copied.contains(&far) {
1095 written.get(&far.to_string()).map(|native| native.0.clone())
1096 } else {
1097 Some(far.to_string())
1098 }?;
1099 DependencyEndpoint::new(id, edge.to.kind)
1100 .ok()
1101 .map(|to| DependencyEdge {
1102 from: edge.from.clone(),
1103 to,
1104 kind: edge.kind,
1105 })
1106 })
1107 .collect()
1108}
1109
1110fn names(endpoint: &DependencyEndpoint, destination: &SourceName) -> Option<String> {
1112 if !endpoint.is_qualified() {
1113 return None;
1114 }
1115 let id: GlobalId = endpoint.id().parse().ok()?;
1116 (&id.source == destination).then_some(id.native.0)
1117}
1118
1119fn resolved(edges: &[Option<DependencyEdge>]) -> Vec<DependencyEdge> {
1121 edges.iter().flatten().cloned().collect()
1122}
1123
1124async fn exists(
1126 destination: &ResolvedSource,
1127 id: &NativeId,
1128 kind: ItemKind,
1129) -> Result<bool, EngineError> {
1130 let found = match kind {
1131 ItemKind::Task => destination
1132 .source()
1133 .get_task(id)
1134 .await
1135 .map_err(|error| refused(destination, error))?
1136 .is_some(),
1137 ItemKind::Project => destination
1138 .source()
1139 .get_project(id)
1140 .await
1141 .map_err(|error| refused(destination, error))?
1142 .is_some(),
1143 };
1144 Ok(found)
1145}