1use nix_uri::{FlakeRef, RefLocation};
2use ropey::Rope;
3use std::cmp::Ordering;
4
5use crate::channel::{UpdateStrategy, detect_strategy, find_latest_channel};
6use crate::edit::InputMap;
7use crate::input::Input;
8use crate::uri::is_git_url;
9use crate::version::parse_ref;
10
11#[derive(Default, Debug)]
12pub struct Updater {
13 text: Rope,
14 inputs: Vec<UpdateInput>,
15 offset: i32,
17}
18
19enum UpdateTarget {
20 GitUrl {
21 parsed: Box<FlakeRef>,
22 owner: String,
23 repo: String,
24 domain: String,
25 parsed_ref: crate::version::ParsedRef,
26 },
27 ForgeRef {
28 parsed: Box<FlakeRef>,
29 owner: String,
30 repo: String,
31 parsed_ref: crate::version::ParsedRef,
32 },
33}
34
35impl Updater {
36 fn print_update_status(id: &str, previous_version: &str, final_change: &str) -> bool {
37 let is_up_to_date = previous_version == final_change;
38 let initialized = previous_version.is_empty();
39
40 if is_up_to_date {
41 println!(
42 "{} is already on the latest version: {previous_version}.",
43 id
44 );
45 return false;
46 }
47
48 if initialized {
49 println!("Initialized {} version pin at {final_change}.", id);
50 } else {
51 println!("Updated {} from {previous_version} to {final_change}.", id);
52 }
53
54 true
55 }
56 fn parse_update_target(&self, input: &UpdateInput, init: bool) -> Option<UpdateTarget> {
57 let uri = self.get_input_text(input);
58 let is_git_url = is_git_url(&uri);
59
60 let parsed = match uri.parse::<FlakeRef>() {
61 Ok(parsed) => parsed,
62 Err(e) => {
63 tracing::error!("Failed to parse URI: {}", e);
64 return None;
65 }
66 };
67
68 let maybe_version = parsed.get_ref_or_rev().unwrap_or_default();
69 let parsed_ref = parse_ref(&maybe_version, init);
70
71 if !init {
72 if let Err(e) = semver::Version::parse(&parsed_ref.normalized_for_semver) {
73 tracing::debug!("Skip non semver version: {}: {}", maybe_version, e);
74 return None;
75 }
76 }
77
78 let owner = match parsed.r#type.get_owner() {
79 Some(o) => o,
80 None => {
81 tracing::debug!("Skipping input without owner");
82 return None;
83 }
84 };
85
86 let repo = match parsed.r#type.get_repo() {
87 Some(r) => r,
88 None => {
89 tracing::debug!("Skipping input without repo");
90 return None;
91 }
92 };
93
94 if is_git_url {
95 let domain = parsed.r#type.get_domain()?;
96 return Some(UpdateTarget::GitUrl {
97 parsed: Box::new(parsed),
98 owner,
99 repo,
100 domain,
101 parsed_ref,
102 });
103 }
104
105 Some(UpdateTarget::ForgeRef {
106 parsed: Box::new(parsed),
107 owner,
108 repo,
109 parsed_ref,
110 })
111 }
112
113 fn fetch_tags(&self, target: &UpdateTarget) -> Option<crate::api::Tags> {
114 match target {
115 UpdateTarget::GitUrl {
116 owner,
117 repo,
118 domain,
119 ..
120 } => match crate::api::get_tags(repo, owner, Some(domain)) {
121 Ok(tags) => Some(tags),
122 Err(_) => {
123 tracing::error!("Failed to fetch tags for {}/{} on {}", owner, repo, domain);
124 None
125 }
126 },
127 UpdateTarget::ForgeRef { owner, repo, .. } => {
128 match crate::api::get_tags(repo, owner, None) {
129 Ok(tags) => Some(tags),
130 Err(_) => {
131 tracing::error!("Failed to fetch tags for {}/{}", owner, repo);
132 None
133 }
134 }
135 }
136 }
137 }
138
139 fn apply_update(
140 &mut self,
141 input: &UpdateInput,
142 target: &UpdateTarget,
143 mut tags: crate::api::Tags,
144 _init: bool,
145 ) {
146 tags.sort();
147 if let Some(change) = tags.get_latest_tag() {
148 let (parsed, parsed_ref) = match target {
149 UpdateTarget::GitUrl {
150 parsed, parsed_ref, ..
151 } => (parsed, parsed_ref),
152 UpdateTarget::ForgeRef {
153 parsed, parsed_ref, ..
154 } => (parsed, parsed_ref),
155 };
156
157 let final_change = if parsed_ref.has_refs_tags_prefix {
158 format!("refs/tags/{}", change)
159 } else {
160 change.clone()
161 };
162
163 let mut parsed = parsed.clone();
165 let _ = parsed.set_ref(Some(final_change.clone()));
166 let updated_uri = parsed.to_string();
167
168 if !Self::print_update_status(&input.input.id, &parsed_ref.previous_ref, &final_change)
169 {
170 return;
171 }
172
173 self.update_input(input.clone(), &updated_uri);
174 } else {
175 tracing::error!("Could not find latest version for Input: {:?}", input);
176 }
177 }
178 pub fn new(text: Rope, map: InputMap) -> Self {
179 let mut inputs = vec![];
180 for (_id, input) in map {
181 inputs.push(UpdateInput { input });
182 }
183 Self {
184 inputs,
185 text,
186 offset: 0,
187 }
188 }
189 fn get_index(&self, id: &str) -> usize {
192 self.inputs.iter().position(|n| n.input.id == id).unwrap()
193 }
194 pub fn pin_input_to_ref(&mut self, id: &str, rev: &str) {
196 self.sort();
197 let inputs = self.inputs.clone();
198 if let Some(input) = inputs.get(self.get_index(id)) {
199 tracing::debug!("Input: {:?}", input);
200 self.change_input_to_rev(input, rev);
201 }
202 }
203 pub fn unpin_input(&mut self, id: &str) {
205 self.sort();
206 let inputs = self.inputs.clone();
207 if let Some(input) = inputs.get(self.get_index(id)) {
208 tracing::debug!("Input: {:?}", input);
209 self.remove_ref_and_rev(input);
210 }
211 }
212 pub fn update_all_inputs_to_latest_semver(&mut self, id: Option<String>, init: bool) {
215 self.sort();
216 let inputs = self.inputs.clone();
217 for input in inputs.iter() {
218 if let Some(ref input_id) = id {
219 if input.input.id == *input_id {
220 self.query_and_update_all_inputs(input, init);
221 }
222 } else {
223 self.query_and_update_all_inputs(input, init);
224 }
225 }
226 }
227 pub fn get_changes(&self) -> String {
228 self.text.to_string()
229 }
230
231 fn get_input_text(&self, input: &UpdateInput) -> String {
232 self.text
233 .slice(
234 ((input.input.range.start as i32) + 1 + self.offset) as usize
235 ..((input.input.range.end as i32) + self.offset - 1) as usize,
236 )
237 .to_string()
238 }
239
240 pub fn change_input_to_rev(&mut self, input: &UpdateInput, rev: &str) {
242 let uri = self.get_input_text(input);
243 match uri.parse::<FlakeRef>() {
244 Ok(mut parsed) => {
245 let _ = parsed.set_rev(Some(rev.into()));
247 self.update_input(input.clone(), &parsed.to_string());
248 }
249 Err(e) => {
250 tracing::error!("Error while changing input: {}", e);
251 }
252 }
253 }
254 fn remove_ref_and_rev(&mut self, input: &UpdateInput) {
255 let uri = self.get_input_text(input);
256 match uri.parse::<FlakeRef>() {
257 Ok(mut parsed) => {
258 if parsed.ref_source_location() == RefLocation::None {
259 return;
260 }
261 let _ = parsed.set_ref(None);
263 let _ = parsed.set_rev(None);
264 self.update_input(input.clone(), &parsed.to_string());
265 }
266 Err(e) => {
267 tracing::error!("Error while changing input: {}", e);
268 }
269 }
270 }
271 pub fn query_and_update_all_inputs(&mut self, input: &UpdateInput, init: bool) {
273 let uri = self.get_input_text(input);
274
275 let parsed = match uri.parse::<FlakeRef>() {
276 Ok(parsed) => parsed,
277 Err(e) => {
278 tracing::error!("Failed to parse URI: {}", e);
279 return;
280 }
281 };
282
283 let owner = match parsed.r#type.get_owner() {
284 Some(o) => o,
285 None => {
286 tracing::debug!("Skipping input without owner");
287 return;
288 }
289 };
290
291 let repo = match parsed.r#type.get_repo() {
292 Some(r) => r,
293 None => {
294 tracing::debug!("Skipping input without repo");
295 return;
296 }
297 };
298
299 let strategy = detect_strategy(&owner, &repo);
300 tracing::debug!("Update strategy for {}/{}: {:?}", owner, repo, strategy);
301
302 match strategy {
303 UpdateStrategy::NixpkgsChannel
304 | UpdateStrategy::HomeManagerChannel
305 | UpdateStrategy::NixDarwinChannel => {
306 self.update_channel_input(input, &parsed);
307 }
308 UpdateStrategy::SemverTags => {
309 self.update_semver_input(input, init);
310 }
311 }
312 }
313
314 fn update_channel_input(&mut self, input: &UpdateInput, parsed: &FlakeRef) {
316 let owner = parsed.r#type.get_owner().unwrap();
317 let repo = parsed.r#type.get_repo().unwrap();
318 let domain = parsed.r#type.get_domain();
319
320 let current_ref = parsed.get_ref_or_rev().unwrap_or_default();
321
322 if current_ref.is_empty() {
323 tracing::debug!("Skipping unpinned channel input: {}", input.input.id);
324 return;
325 }
326
327 let has_refs_heads_prefix = current_ref.starts_with("refs/heads/");
328
329 let latest = match find_latest_channel(¤t_ref, &owner, &repo, domain.as_deref()) {
330 Some(latest) => latest,
331 None => return,
333 };
334
335 let final_ref = if has_refs_heads_prefix {
336 format!("refs/heads/{}", latest)
337 } else {
338 latest.clone()
339 };
340
341 let mut parsed = parsed.clone();
342 let _ = parsed.set_ref(Some(final_ref.clone()));
343 let updated_uri = parsed.to_string();
344
345 if Self::print_update_status(&input.input.id, ¤t_ref, &final_ref) {
346 self.update_input(input.clone(), &updated_uri);
347 }
348 }
349
350 fn update_semver_input(&mut self, input: &UpdateInput, init: bool) {
352 let target = match self.parse_update_target(input, init) {
353 Some(target) => target,
354 None => return,
355 };
356
357 let tags = match self.fetch_tags(&target) {
358 Some(tags) => tags,
359 None => return,
360 };
361
362 self.apply_update(input, &target, tags, init);
363 }
364
365 fn sort(&mut self) {
367 self.inputs.sort();
368 }
369 fn update_input(&mut self, input: UpdateInput, change: &str) {
370 self.text.remove(
371 (input.input.range.start as i32 + 1 + self.offset) as usize
372 ..(input.input.range.end as i32 - 1 + self.offset) as usize,
373 );
374 self.text.insert(
375 (input.input.range.start as i32 + 1 + self.offset) as usize,
376 change,
377 );
378 self.update_offset(input.clone(), change);
379 }
380 fn update_offset(&mut self, input: UpdateInput, change: &str) {
381 let previous_len = input.input.range.end as i32 - input.input.range.start as i32 - 2;
382 let len = change.len() as i32;
383 let offset = len - previous_len;
384 self.offset += offset;
385 }
386}
387
388#[derive(Debug, Clone)]
390pub struct UpdateInput {
391 input: Input,
392}
393
394impl Ord for UpdateInput {
395 fn cmp(&self, other: &Self) -> Ordering {
396 (self.input.range.start).cmp(&(other.input.range.start))
397 }
398}
399
400impl PartialEq for UpdateInput {
401 fn eq(&self, other: &Self) -> bool {
402 self.input.range.start == other.input.range.start
403 }
404}
405
406impl PartialOrd for UpdateInput {
407 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
408 Some(self.cmp(other))
409 }
410}
411
412impl Eq for UpdateInput {}