purple_ssh/ssh_config/
repair.rs1use super::model::{ConfigElement, SshConfigFile};
14
15pub(super) fn provider_group_display_name(name: &str) -> &str {
19 match name {
20 "digitalocean" => "DigitalOcean",
21 "vultr" => "Vultr",
22 "linode" => "Linode",
23 "hetzner" => "Hetzner",
24 "upcloud" => "UpCloud",
25 "proxmox" => "Proxmox VE",
26 "aws" => "AWS EC2",
27 "scaleway" => "Scaleway",
28 "gcp" => "GCP",
29 "azure" => "Azure",
30 "tailscale" => "Tailscale",
31 "oracle" => "Oracle Cloud",
32 other => other,
33 }
34}
35
36impl SshConfigFile {
37 pub fn remove_all_orphaned_group_headers(&mut self) -> usize {
40 let active_providers: std::collections::HashSet<String> = self
41 .elements
42 .iter()
43 .filter_map(|e| {
44 if let ConfigElement::HostBlock(block) = e {
45 block
46 .provider()
47 .map(|(name, _)| provider_group_display_name(&name).to_string())
48 } else {
49 None
50 }
51 })
52 .collect();
53
54 let mut removed = 0;
55 self.elements.retain(|e| {
56 if let ConfigElement::GlobalLine(line) = e {
57 if let Some(rest) = line.trim().strip_prefix("# purple:group ") {
58 if !active_providers.contains(rest.trim()) {
59 removed += 1;
60 return false;
61 }
62 }
63 }
64 true
65 });
66 removed
67 }
68
69 pub fn repair_absorbed_group_comments(&mut self) -> usize {
73 let mut repaired = 0;
74 let mut idx = 0;
75 while idx < self.elements.len() {
76 let needs_repair = if let ConfigElement::HostBlock(block) = &self.elements[idx] {
77 block
78 .directives
79 .iter()
80 .any(|d| d.is_non_directive && d.raw_line.trim().starts_with("# purple:group "))
81 } else {
82 false
83 };
84
85 if !needs_repair {
86 idx += 1;
87 continue;
88 }
89
90 let block = if let ConfigElement::HostBlock(block) = &mut self.elements[idx] {
91 block
92 } else {
93 unreachable!()
94 };
95
96 let group_idx = block
97 .directives
98 .iter()
99 .position(|d| {
100 d.is_non_directive && d.raw_line.trim().starts_with("# purple:group ")
101 })
102 .unwrap();
103
104 let mut keep_end = group_idx;
105 while keep_end > 0
106 && block.directives[keep_end - 1].is_non_directive
107 && block.directives[keep_end - 1].raw_line.trim().is_empty()
108 {
109 keep_end -= 1;
110 }
111
112 let extracted: Vec<ConfigElement> = block
113 .directives
114 .drain(keep_end..)
115 .map(|d| ConfigElement::GlobalLine(d.raw_line))
116 .collect();
117
118 let insert_at = idx + 1;
119 for (i, elem) in extracted.into_iter().enumerate() {
120 self.elements.insert(insert_at + i, elem);
121 }
122
123 repaired += 1;
124 idx = insert_at;
125 while idx < self.elements.len() {
126 if let ConfigElement::HostBlock(_) = &self.elements[idx] {
127 break;
128 }
129 idx += 1;
130 }
131 }
132 repaired
133 }
134
135 pub(super) fn remove_orphaned_group_header(&mut self, provider_name: &str) {
138 if self.find_hosts_by_provider(provider_name).is_empty() {
139 let display = provider_group_display_name(provider_name);
140 let header = format!("# purple:group {}", display);
141 self.elements
142 .retain(|e| !matches!(e, ConfigElement::GlobalLine(line) if *line == header));
143 }
144 }
145}