1use nix_uri::urls::UrlWrapper;
2use nix_uri::{FlakeRef, NixUriResult};
3use ropey::Rope;
4
5use crate::change::Change;
6use crate::edit::{FlakeEdit, InputMap, sorted_input_ids, sorted_input_ids_owned};
7use crate::error::FlakeEditError;
8use crate::lock::{FlakeLock, NestedInput};
9use crate::tui;
10use crate::update::Updater;
11use crate::validate;
12
13use super::editor::Editor;
14use super::state::AppState;
15
16fn updater(editor: &Editor, inputs: InputMap) -> Updater {
17 Updater::new(Rope::from_str(&editor.text()), inputs)
18}
19
20pub type Result<T> = std::result::Result<T, CommandError>;
21
22#[derive(Debug, thiserror::Error)]
23pub enum CommandError {
24 #[error(transparent)]
25 FlakeEdit(#[from] FlakeEditError),
26
27 #[error(transparent)]
28 Io(#[from] std::io::Error),
29
30 #[error("No URI provided")]
31 NoUri,
32
33 #[error("No ID provided")]
34 NoId,
35
36 #[error("Could not infer ID from flake reference: {0}")]
37 CouldNotInferId(String),
38
39 #[error("Invalid URI: {0}")]
40 InvalidUri(String),
41
42 #[error("No inputs found in the flake")]
43 NoInputs,
44
45 #[error("Could not read flake.lock")]
46 NoLock,
47
48 #[error("Input not found: {0}")]
49 InputNotFound(String),
50
51 #[error("The input could not be removed: {0}")]
52 CouldNotRemove(String),
53}
54
55fn load_flake_lock(state: &AppState) -> std::result::Result<FlakeLock, FlakeEditError> {
57 if let Some(lock_path) = &state.lock_file {
58 FlakeLock::from_file(lock_path)
59 } else {
60 FlakeLock::from_default_path()
61 }
62}
63
64struct FollowContext {
65 nested_inputs: Vec<NestedInput>,
66 top_level_inputs: std::collections::HashSet<String>,
67 inputs: crate::edit::InputMap,
69}
70
71fn is_follows_reference_to_parent(url: &str, parent: &str) -> bool {
75 let url_trimmed = url.trim_matches('"');
76 url_trimmed.starts_with(&format!("{}/", parent))
77}
78
79fn load_follow_context(
82 flake_edit: &mut FlakeEdit,
83 state: &AppState,
84) -> Result<Option<FollowContext>> {
85 let nested_inputs: Vec<NestedInput> = load_flake_lock(state)
86 .map(|lock| lock.nested_inputs())
87 .unwrap_or_default();
88
89 if nested_inputs.is_empty() {
90 eprintln!("No nested inputs found in flake.lock");
91 eprintln!("Make sure you have run `nix flake lock` first.");
92 return Ok(None);
93 }
94
95 let inputs = flake_edit.list().clone();
96 let top_level_inputs: std::collections::HashSet<String> = inputs.keys().cloned().collect();
97
98 if top_level_inputs.is_empty() {
99 return Err(CommandError::NoInputs);
100 }
101
102 Ok(Some(FollowContext {
103 nested_inputs,
104 top_level_inputs,
105 inputs,
106 }))
107}
108
109enum ConfirmResult {
111 Applied,
113 Cancelled,
115 Back,
117}
118
119fn interactive_single_select<F, OnApplied, ExtraData>(
127 editor: &Editor,
128 state: &AppState,
129 title: &str,
130 prompt: &str,
131 items: Vec<String>,
132 make_change: F,
133 on_applied: OnApplied,
134) -> Result<()>
135where
136 F: Fn(&str) -> Result<(String, ExtraData)>,
137 OnApplied: Fn(&str, ExtraData),
138{
139 loop {
140 let select_app = tui::App::select_one(title, prompt, items.clone(), state.diff);
141 let Some(tui::AppResult::SingleSelect(result)) = tui::run(select_app)? else {
142 return Ok(());
143 };
144 let tui::SingleSelectResult {
145 item: id,
146 show_diff,
147 } = result;
148 let (change, extra_data) = make_change(&id)?;
149
150 match confirm_or_apply(editor, state, title, &change, show_diff)? {
151 ConfirmResult::Applied => {
152 on_applied(&id, extra_data);
153 break;
154 }
155 ConfirmResult::Back => continue,
156 ConfirmResult::Cancelled => return Ok(()),
157 }
158 }
159 Ok(())
160}
161
162fn interactive_multi_select<F>(
164 editor: &Editor,
165 state: &AppState,
166 title: &str,
167 prompt: &str,
168 items: Vec<String>,
169 make_change: F,
170) -> Result<()>
171where
172 F: Fn(&[String]) -> String,
173{
174 loop {
175 let select_app = tui::App::select_many(title, prompt, items.clone(), state.diff);
176 let Some(tui::AppResult::MultiSelect(result)) = tui::run(select_app)? else {
177 return Ok(());
178 };
179 let tui::MultiSelectResultData {
180 items: selected,
181 show_diff,
182 } = result;
183 let change = make_change(&selected);
184
185 match confirm_or_apply(editor, state, title, &change, show_diff)? {
186 ConfirmResult::Applied => break,
187 ConfirmResult::Back => continue,
188 ConfirmResult::Cancelled => return Ok(()),
189 }
190 }
191 Ok(())
192}
193
194fn confirm_or_apply(
202 editor: &Editor,
203 state: &AppState,
204 context: &str,
205 change: &str,
206 show_diff: bool,
207) -> Result<ConfirmResult> {
208 if show_diff || state.diff {
209 let diff = crate::diff::Diff::new(&editor.text(), change).to_string_plain();
210 let confirm_app = tui::App::confirm(context, &diff);
211 let Some(tui::AppResult::Confirm(action)) = tui::run(confirm_app)? else {
212 return Ok(ConfirmResult::Cancelled);
213 };
214 match action {
215 tui::ConfirmResultAction::Apply => {
216 let mut apply_state = state.clone();
217 apply_state.diff = false;
218 editor.apply_or_diff(change, &apply_state)?;
219 Ok(ConfirmResult::Applied)
220 }
221 tui::ConfirmResultAction::Back => Ok(ConfirmResult::Back),
222 tui::ConfirmResultAction::Exit => Ok(ConfirmResult::Cancelled),
223 }
224 } else {
225 editor.apply_or_diff(change, state)?;
226 Ok(ConfirmResult::Applied)
227 }
228}
229
230fn apply_uri_options(
232 mut flake_ref: FlakeRef,
233 ref_or_rev: Option<&str>,
234 shallow: bool,
235) -> std::result::Result<FlakeRef, String> {
236 if let Some(ror) = ref_or_rev {
237 flake_ref.r#type.ref_or_rev(Some(ror.to_string())).map_err(|e| {
238 format!(
239 "Cannot apply --ref-or-rev: {}. \
240 The --ref-or-rev option only works with git forge types (github:, gitlab:, sourcehut:) and indirect types (flake:). \
241 For other URI types, use ?ref= or ?rev= query parameters in the URI itself.",
242 e
243 )
244 })?;
245 }
246 if shallow {
247 flake_ref.params.set_shallow(Some("1".to_string()));
248 }
249 Ok(flake_ref)
250}
251
252fn transform_uri(uri: String, ref_or_rev: Option<&str>, shallow: bool) -> Result<String> {
258 let flake_ref: FlakeRef = uri
259 .parse()
260 .map_err(|e| CommandError::InvalidUri(format!("{}: {}", uri, e)))?;
261
262 if ref_or_rev.is_none() && !shallow {
263 return Ok(uri);
264 }
265
266 apply_uri_options(flake_ref, ref_or_rev, shallow)
267 .map(|f| f.to_string())
268 .map_err(CommandError::CouldNotInferId)
269}
270
271#[derive(Default)]
272pub struct UriOptions<'a> {
273 pub ref_or_rev: Option<&'a str>,
274 pub shallow: bool,
275 pub no_flake: bool,
276}
277
278pub fn add(
279 editor: &Editor,
280 flake_edit: &mut FlakeEdit,
281 state: &AppState,
282 id: Option<String>,
283 uri: Option<String>,
284 opts: UriOptions<'_>,
285) -> Result<()> {
286 let change = match (id, uri, state.interactive) {
287 (Some(id_val), Some(uri_str), _) => add_with_id_and_uri(id_val, uri_str, &opts)?,
289 (id, None, true) | (None, id, true) => {
291 add_interactive(editor, state, id.as_deref(), &opts)?
292 }
293 (Some(uri), None, false) | (None, Some(uri), false) => add_infer_id(uri, &opts)?,
295 (None, None, false) => {
297 return Err(CommandError::NoUri);
298 }
299 };
300
301 apply_change(editor, flake_edit, state, change)
302}
303
304fn add_with_id_and_uri(id: String, uri: String, opts: &UriOptions<'_>) -> Result<Change> {
305 let final_uri = transform_uri(uri, opts.ref_or_rev, opts.shallow)?;
306 Ok(Change::Add {
307 id: Some(id),
308 uri: Some(final_uri),
309 flake: !opts.no_flake,
310 })
311}
312
313fn add_interactive(
314 editor: &Editor,
315 state: &AppState,
316 prefill_uri: Option<&str>,
317 opts: &UriOptions<'_>,
318) -> Result<Change> {
319 let tui_app = tui::App::add("Add", editor.text(), prefill_uri, state.cache_config());
320 let Some(tui::AppResult::Change(tui_change)) = tui::run(tui_app)? else {
321 return Ok(Change::None);
323 };
324
325 if let Change::Add { id, uri, flake } = tui_change {
327 let final_uri = uri
328 .map(|u| transform_uri(u, opts.ref_or_rev, opts.shallow))
329 .transpose()?;
330 Ok(Change::Add {
331 id,
332 uri: final_uri,
333 flake: flake && !opts.no_flake,
334 })
335 } else {
336 Ok(tui_change)
337 }
338}
339
340fn add_infer_id(uri: String, opts: &UriOptions<'_>) -> Result<Change> {
342 let flake_ref: NixUriResult<FlakeRef> = UrlWrapper::convert_or_parse(&uri);
343
344 let (inferred_id, final_uri) = if let Ok(flake_ref) = flake_ref {
345 let flake_ref = apply_uri_options(flake_ref, opts.ref_or_rev, opts.shallow)
346 .map_err(CommandError::CouldNotInferId)?;
347 let parsed_uri = flake_ref.to_string();
348 let final_uri = if parsed_uri.is_empty() || parsed_uri == "none" {
349 uri.clone()
350 } else {
351 parsed_uri
352 };
353 (flake_ref.id(), final_uri)
354 } else {
355 (None, uri.clone())
356 };
357
358 let final_id = inferred_id.ok_or(CommandError::CouldNotInferId(uri))?;
359
360 Ok(Change::Add {
361 id: Some(final_id),
362 uri: Some(final_uri),
363 flake: !opts.no_flake,
364 })
365}
366
367pub fn remove(
368 editor: &Editor,
369 flake_edit: &mut FlakeEdit,
370 state: &AppState,
371 id: Option<String>,
372) -> Result<()> {
373 let change = if let Some(id) = id {
374 Change::Remove {
375 ids: vec![id.into()],
376 }
377 } else if state.interactive {
378 let inputs = flake_edit.list();
379 let mut removable: Vec<String> = Vec::new();
380 for input_id in sorted_input_ids(inputs) {
381 let input = &inputs[input_id];
382 removable.push(input_id.clone());
383 for follows in input.follows() {
384 if let crate::input::Follows::Indirect(from, to) = follows {
385 removable.push(format!("{}.{} => {}", input_id, from, to));
386 }
387 }
388 }
389 if removable.is_empty() {
390 return Err(CommandError::NoInputs);
391 }
392
393 let tui_app = tui::App::remove("Remove", editor.text(), removable);
394 let Some(tui::AppResult::Change(tui_change)) = tui::run(tui_app)? else {
395 return Ok(());
396 };
397
398 if let Change::Remove { ids } = tui_change {
400 let stripped_ids: Vec<_> = ids
401 .iter()
402 .map(|id| {
403 id.to_string()
404 .split(" => ")
405 .next()
406 .unwrap_or(&id.to_string())
407 .to_string()
408 .into()
409 })
410 .collect();
411 Change::Remove { ids: stripped_ids }
412 } else {
413 tui_change
414 }
415 } else {
416 return Err(CommandError::NoId);
417 };
418
419 apply_change(editor, flake_edit, state, change)
420}
421
422pub fn change(
423 editor: &Editor,
424 flake_edit: &mut FlakeEdit,
425 state: &AppState,
426 id: Option<String>,
427 uri: Option<String>,
428 ref_or_rev: Option<&str>,
429 shallow: bool,
430) -> Result<()> {
431 let inputs = flake_edit.list();
432
433 let change = match (id, uri, state.interactive) {
434 (None, None, true) | (None, Some(_), true) => {
437 change_full_interactive(editor, state, inputs, ref_or_rev, shallow)?
438 }
439 (Some(id), None, true) => {
441 change_uri_interactive(editor, state, inputs, &id, ref_or_rev, shallow)?
442 }
443 (Some(id_val), Some(uri_str), _) => {
445 change_with_id_and_uri(id_val, uri_str, ref_or_rev, shallow)?
446 }
447 (Some(uri), None, false) | (None, Some(uri), false) => {
449 change_infer_id(uri, ref_or_rev, shallow)?
450 }
451 (None, None, false) => {
453 return Err(CommandError::NoId);
454 }
455 };
456
457 apply_change(editor, flake_edit, state, change)
458}
459
460fn change_full_interactive(
462 editor: &Editor,
463 state: &AppState,
464 inputs: &crate::edit::InputMap,
465 ref_or_rev: Option<&str>,
466 shallow: bool,
467) -> Result<Change> {
468 let input_pairs: Vec<(String, String)> = sorted_input_ids(inputs)
469 .into_iter()
470 .map(|id| (id.clone(), inputs[id].url().trim_matches('"').to_string()))
471 .collect();
472
473 if input_pairs.is_empty() {
474 return Err(CommandError::NoInputs);
475 }
476
477 let tui_app = tui::App::change("Change", editor.text(), input_pairs, state.cache_config());
478 let Some(tui::AppResult::Change(tui_change)) = tui::run(tui_app)? else {
479 return Ok(Change::None);
480 };
481
482 if let Change::Change { id, uri, .. } = tui_change {
484 let final_uri = uri
485 .map(|u| transform_uri(u, ref_or_rev, shallow))
486 .transpose()?;
487 Ok(Change::Change {
488 id,
489 uri: final_uri,
490 ref_or_rev: None,
491 })
492 } else {
493 Ok(tui_change)
494 }
495}
496
497fn change_uri_interactive(
499 editor: &Editor,
500 state: &AppState,
501 inputs: &crate::edit::InputMap,
502 id: &str,
503 ref_or_rev: Option<&str>,
504 shallow: bool,
505) -> Result<Change> {
506 let current_uri = inputs.get(id).map(|i| i.url().trim_matches('"'));
507 let tui_app = tui::App::change_uri(
508 "Change",
509 editor.text(),
510 id,
511 current_uri,
512 state.diff,
513 state.cache_config(),
514 );
515
516 let Some(tui::AppResult::Change(tui_change)) = tui::run(tui_app)? else {
517 return Ok(Change::None);
518 };
519
520 if let Change::Change {
522 uri: Some(new_uri), ..
523 } = tui_change
524 {
525 let final_uri = transform_uri(new_uri, ref_or_rev, shallow)?;
526 Ok(Change::Change {
527 id: Some(id.to_string()),
528 uri: Some(final_uri),
529 ref_or_rev: None,
530 })
531 } else {
532 Err(CommandError::NoUri)
533 }
534}
535
536fn change_with_id_and_uri(
537 id: String,
538 uri: String,
539 ref_or_rev: Option<&str>,
540 shallow: bool,
541) -> Result<Change> {
542 let final_uri = transform_uri(uri, ref_or_rev, shallow)?;
543 Ok(Change::Change {
544 id: Some(id),
545 uri: Some(final_uri),
546 ref_or_rev: None,
547 })
548}
549
550fn change_infer_id(uri: String, ref_or_rev: Option<&str>, shallow: bool) -> Result<Change> {
552 let flake_ref: NixUriResult<FlakeRef> = UrlWrapper::convert_or_parse(&uri);
553
554 let flake_ref = flake_ref.map_err(|_| CommandError::CouldNotInferId(uri.clone()))?;
555 let flake_ref =
556 apply_uri_options(flake_ref, ref_or_rev, shallow).map_err(CommandError::CouldNotInferId)?;
557
558 let final_uri = if flake_ref.to_string().is_empty() {
559 uri.clone()
560 } else {
561 flake_ref.to_string()
562 };
563
564 let id = flake_ref.id().ok_or(CommandError::CouldNotInferId(uri))?;
565
566 Ok(Change::Change {
567 id: Some(id),
568 uri: Some(final_uri),
569 ref_or_rev: None,
570 })
571}
572
573pub fn update(
574 editor: &Editor,
575 flake_edit: &mut FlakeEdit,
576 state: &AppState,
577 id: Option<String>,
578 init: bool,
579) -> Result<()> {
580 let inputs = flake_edit.list().clone();
581 let input_ids = sorted_input_ids_owned(&inputs);
582
583 if let Some(id) = id {
584 let mut updater = updater(editor, inputs);
585 updater.update_all_inputs_to_latest_semver(Some(id), init);
586 let change = updater.get_changes();
587 editor.apply_or_diff(&change, state)?;
588 } else if state.interactive {
589 if input_ids.is_empty() {
590 return Err(CommandError::NoInputs);
591 }
592
593 let display_items: Vec<String> = input_ids
594 .iter()
595 .map(|id| {
596 let input = &inputs[id];
597 let version = input
598 .url()
599 .trim_matches('"')
600 .parse::<FlakeRef>()
601 .ok()
602 .and_then(|f| f.get_ref_or_rev());
603 match version {
604 Some(v) if !v.is_empty() => format!("{} - {}", id, v),
605 _ => id.clone(),
606 }
607 })
608 .collect();
609
610 interactive_multi_select(
611 editor,
612 state,
613 "Update",
614 "Space select, U all, ^D diff",
615 display_items,
616 |selected| {
617 let ids: Vec<String> = selected
619 .iter()
620 .map(|s| s.split(" - ").next().unwrap_or(s).to_string())
621 .collect();
622 let mut updater = updater(editor, inputs.clone());
623 for id in &ids {
624 updater.update_all_inputs_to_latest_semver(Some(id.clone()), init);
625 }
626 updater.get_changes()
627 },
628 )?;
629 } else {
630 let mut updater = updater(editor, inputs);
631 for id in &input_ids {
632 updater.update_all_inputs_to_latest_semver(Some(id.clone()), init);
633 }
634 let change = updater.get_changes();
635 editor.apply_or_diff(&change, state)?;
636 }
637
638 Ok(())
639}
640
641pub fn pin(
642 editor: &Editor,
643 flake_edit: &mut FlakeEdit,
644 state: &AppState,
645 id: Option<String>,
646 rev: Option<String>,
647) -> Result<()> {
648 let inputs = flake_edit.list().clone();
649 let input_ids = sorted_input_ids_owned(&inputs);
650
651 if let Some(id) = id {
652 let lock = FlakeLock::from_default_path().map_err(|_| CommandError::NoLock)?;
653 let target_rev = if let Some(rev) = rev {
654 rev
655 } else {
656 lock.rev_for(&id)
657 .map_err(|_| CommandError::InputNotFound(id.clone()))?
658 };
659 let mut updater = updater(editor, inputs);
660 updater.pin_input_to_ref(&id, &target_rev);
661 let change = updater.get_changes();
662 editor.apply_or_diff(&change, state)?;
663 if !state.diff {
664 println!("Pinned input: {} to {}", id, target_rev);
665 }
666 } else if state.interactive {
667 if input_ids.is_empty() {
668 return Err(CommandError::NoInputs);
669 }
670 let lock = FlakeLock::from_default_path().map_err(|_| CommandError::NoLock)?;
671
672 interactive_single_select(
673 editor,
674 state,
675 "Pin",
676 "Select input",
677 input_ids,
678 |id| {
679 let target_rev = lock
680 .rev_for(id)
681 .map_err(|_| CommandError::InputNotFound(id.to_string()))?;
682 let mut updater = updater(editor, inputs.clone());
683 updater.pin_input_to_ref(id, &target_rev);
684 Ok((updater.get_changes(), target_rev))
685 },
686 |id, target_rev| println!("Pinned input: {} to {}", id, target_rev),
687 )?;
688 } else {
689 return Err(CommandError::NoId);
690 }
691
692 Ok(())
693}
694
695pub fn unpin(
696 editor: &Editor,
697 flake_edit: &mut FlakeEdit,
698 state: &AppState,
699 id: Option<String>,
700) -> Result<()> {
701 let inputs = flake_edit.list().clone();
702 let input_ids = sorted_input_ids_owned(&inputs);
703
704 if let Some(id) = id {
705 let mut updater = updater(editor, inputs);
706 updater.unpin_input(&id);
707 let change = updater.get_changes();
708 editor.apply_or_diff(&change, state)?;
709 if !state.diff {
710 println!("Unpinned input: {}", id);
711 }
712 } else if state.interactive {
713 if input_ids.is_empty() {
714 return Err(CommandError::NoInputs);
715 }
716
717 interactive_single_select(
718 editor,
719 state,
720 "Unpin",
721 "Select input",
722 input_ids,
723 |id| {
724 let mut updater = updater(editor, inputs.clone());
725 updater.unpin_input(id);
726 Ok((updater.get_changes(), ()))
727 },
728 |id, ()| println!("Unpinned input: {}", id),
729 )?;
730 } else {
731 return Err(CommandError::NoId);
732 }
733
734 Ok(())
735}
736
737pub fn list(flake_edit: &mut FlakeEdit, format: &crate::cli::ListFormat) -> Result<()> {
738 let inputs = flake_edit.list();
739 crate::app::handler::list_inputs(inputs, format);
740 Ok(())
741}
742
743pub fn follow(
744 editor: &Editor,
745 flake_edit: &mut FlakeEdit,
746 state: &AppState,
747 input: Option<String>,
748 target: Option<String>,
749 auto: bool,
750) -> Result<()> {
751 if auto {
752 return follow_auto(editor, flake_edit, state);
753 }
754
755 let change = if let (Some(input_val), Some(target_val)) = (input.clone(), target) {
756 Change::Follows {
758 input: input_val.into(),
759 target: target_val,
760 }
761 } else if state.interactive {
762 let Some(ctx) = load_follow_context(flake_edit, state)? else {
764 return Ok(());
765 };
766 let top_level_vec: Vec<String> = ctx.top_level_inputs.into_iter().collect();
767
768 let tui_app = if let Some(input_val) = input {
769 tui::App::follow_target("Follow", editor.text(), input_val, top_level_vec)
770 } else {
771 tui::App::follow("Follow", editor.text(), ctx.nested_inputs, top_level_vec)
772 };
773
774 let Some(tui::AppResult::Change(tui_change)) = tui::run(tui_app)? else {
775 return Ok(());
776 };
777 tui_change
778 } else {
779 return Err(CommandError::NoId);
780 };
781
782 apply_change(editor, flake_edit, state, change)
783}
784
785fn follow_auto(editor: &Editor, flake_edit: &mut FlakeEdit, state: &AppState) -> Result<()> {
791 let Some(ctx) = load_follow_context(flake_edit, state)? else {
792 return Ok(());
793 };
794
795 let to_follow: Vec<(String, String)> = ctx
797 .nested_inputs
798 .iter()
799 .filter(|nested| nested.follows.is_none())
800 .filter_map(|nested| {
801 let nested_name = nested.path.split('.').next_back().unwrap_or(&nested.path);
802 let parent = nested.path.split('.').next().unwrap_or(&nested.path);
803
804 if !ctx.top_level_inputs.contains(nested_name) {
805 return None;
806 }
807
808 if let Some(target_input) = ctx.inputs.get(nested_name)
812 && is_follows_reference_to_parent(target_input.url(), parent)
813 {
814 tracing::debug!(
815 "Skipping {} -> {}: would create cycle (target follows {}/...)",
816 nested.path,
817 nested_name,
818 parent
819 );
820 return None;
821 }
822
823 Some((nested.path.clone(), nested_name.to_string()))
824 })
825 .collect();
826
827 if to_follow.is_empty() {
828 println!("All inputs are already deduplicated.");
829 return Ok(());
830 }
831
832 let mut current_text = editor.text();
834 let mut applied: Vec<(&str, &str)> = Vec::new();
835
836 for (input_path, target) in &to_follow {
837 let change = Change::Follows {
838 input: input_path.clone().into(),
839 target: target.clone(),
840 };
841
842 let mut temp_flake_edit =
843 FlakeEdit::from_text(¤t_text).map_err(CommandError::FlakeEdit)?;
844
845 match temp_flake_edit.apply_change(change) {
846 Ok(Some(resulting_text)) => {
847 let validation = validate::validate(&resulting_text);
848 if validation.is_ok() {
849 current_text = resulting_text;
850 applied.push((input_path, target));
851 } else {
852 for err in validation.errors {
853 eprintln!("Error applying follows for {}: {}", input_path, err);
854 }
855 }
856 }
857 Ok(None) => eprintln!("Could not create follows for {}", input_path),
858 Err(e) => eprintln!("Error applying follows for {}: {}", input_path, e),
859 }
860 }
861
862 if applied.is_empty() {
863 return Ok(());
864 }
865
866 if state.diff {
867 let original = editor.text();
868 let diff = crate::diff::Diff::new(&original, ¤t_text);
869 diff.compare();
870 } else {
871 editor.apply_or_diff(¤t_text, state)?;
872 println!(
873 "Deduplicated {} {}.",
874 applied.len(),
875 if applied.len() == 1 {
876 "input"
877 } else {
878 "inputs"
879 }
880 );
881 for (input_path, target) in &applied {
882 let nested_name = input_path.split('.').next_back().unwrap_or(input_path);
883 let parent = input_path.split('.').next().unwrap_or(input_path);
884 println!(" {}.{} → {}", parent, nested_name, target);
885 }
886 }
887
888 Ok(())
889}
890
891fn apply_change(
892 editor: &Editor,
893 flake_edit: &mut FlakeEdit,
894 state: &AppState,
895 change: Change,
896) -> Result<()> {
897 match flake_edit.apply_change(change.clone()) {
898 Ok(Some(resulting_change)) => {
899 let validation = validate::validate(&resulting_change);
900 if validation.has_errors() {
901 eprintln!("There are errors in the changes:");
902 for e in &validation.errors {
903 tracing::error!("Error: {e}");
904 }
905 eprintln!("{}", resulting_change);
906 eprintln!("There were errors in the changes, the changes have not been applied.");
907 std::process::exit(1);
908 }
909
910 editor.apply_or_diff(&resulting_change, state)?;
911
912 if !state.diff {
913 if let Change::Add {
915 id: Some(id),
916 uri: Some(uri),
917 ..
918 } = &change
919 {
920 let mut cache = crate::cache::Cache::load();
921 cache.add_entry(id.clone(), uri.clone());
922 if let Err(e) = cache.commit() {
923 tracing::debug!("Could not write to cache: {}", e);
924 }
925 }
926
927 for msg in change.success_messages() {
928 println!("{}", msg);
929 }
930 }
931 }
932 Err(e) => {
933 return Err(e.into());
934 }
935 Ok(None) => {
936 if change.is_remove() {
937 return Err(CommandError::CouldNotRemove(
938 change.id().map(|id| id.to_string()).unwrap_or_default(),
939 ));
940 }
941 if change.is_follows() {
942 let id = change.id().map(|id| id.to_string()).unwrap_or_default();
943 eprintln!("The follows relationship for {} could not be created.", id);
944 eprintln!(
945 "\nPlease check that the input exists in the flake.nix file.\n\
946 Use dot notation: `flake-edit follow <input>.<nested-input> <target>`\n\
947 Example: `flake-edit follow rust-overlay.nixpkgs nixpkgs`"
948 );
949 std::process::exit(1);
950 }
951 println!("Nothing changed.");
952 }
953 }
954
955 Ok(())
956}
957
958#[cfg(test)]
959mod tests {
960 use super::*;
961
962 #[test]
963 fn test_is_follows_reference_to_parent() {
964 assert!(is_follows_reference_to_parent(
967 "\"clan-core/treefmt-nix\"",
968 "clan-core"
969 ));
970
971 assert!(is_follows_reference_to_parent(
973 "clan-core/treefmt-nix",
974 "clan-core"
975 ));
976
977 assert!(is_follows_reference_to_parent(
979 "\"some-input/nixpkgs\"",
980 "some-input"
981 ));
982
983 assert!(!is_follows_reference_to_parent(
985 "\"github:nixos/nixpkgs\"",
986 "clan-core"
987 ));
988
989 assert!(!is_follows_reference_to_parent(
991 "\"github:foo/clan-core-utils\"",
992 "clan-core"
993 ));
994
995 assert!(!is_follows_reference_to_parent(
997 "\"clan-core-extended\"",
998 "clan-core"
999 ));
1000
1001 assert!(!is_follows_reference_to_parent("", "clan-core"));
1003
1004 assert!(!is_follows_reference_to_parent("\"\"", "clan-core"));
1006 }
1007}