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 && let Err(e) = semver::Version::parse(&parsed_ref.normalized_for_semver) {
72 tracing::debug!("Skip non semver version: {}: {}", maybe_version, e);
73 return None;
74 }
75
76 let owner = match parsed.r#type.get_owner() {
77 Some(o) => o,
78 None => {
79 tracing::debug!("Skipping input without owner");
80 return None;
81 }
82 };
83
84 let repo = match parsed.r#type.get_repo() {
85 Some(r) => r,
86 None => {
87 tracing::debug!("Skipping input without repo");
88 return None;
89 }
90 };
91
92 if is_git_url {
93 let domain = parsed.r#type.get_domain()?;
94 return Some(UpdateTarget::GitUrl {
95 parsed: Box::new(parsed),
96 owner,
97 repo,
98 domain,
99 parsed_ref,
100 });
101 }
102
103 Some(UpdateTarget::ForgeRef {
104 parsed: Box::new(parsed),
105 owner,
106 repo,
107 parsed_ref,
108 })
109 }
110
111 fn fetch_tags(&self, target: &UpdateTarget) -> Option<crate::api::Tags> {
112 match target {
113 UpdateTarget::GitUrl {
114 owner,
115 repo,
116 domain,
117 ..
118 } => match crate::api::get_tags(repo, owner, Some(domain)) {
119 Ok(tags) => Some(tags),
120 Err(_) => {
121 tracing::error!("Failed to fetch tags for {}/{} on {}", owner, repo, domain);
122 None
123 }
124 },
125 UpdateTarget::ForgeRef { owner, repo, .. } => {
126 match crate::api::get_tags(repo, owner, None) {
127 Ok(tags) => Some(tags),
128 Err(_) => {
129 tracing::error!("Failed to fetch tags for {}/{}", owner, repo);
130 None
131 }
132 }
133 }
134 }
135 }
136
137 fn apply_update(
138 &mut self,
139 input: &UpdateInput,
140 target: &UpdateTarget,
141 mut tags: crate::api::Tags,
142 _init: bool,
143 ) {
144 tags.sort();
145 if let Some(change) = tags.get_latest_tag() {
146 let (parsed, parsed_ref) = match target {
147 UpdateTarget::GitUrl {
148 parsed, parsed_ref, ..
149 } => (parsed, parsed_ref),
150 UpdateTarget::ForgeRef {
151 parsed, parsed_ref, ..
152 } => (parsed, parsed_ref),
153 };
154
155 let final_change = if parsed_ref.has_refs_tags_prefix {
156 format!("refs/tags/{}", change)
157 } else {
158 change.clone()
159 };
160
161 let mut parsed = parsed.clone();
163 let _ = parsed.set_ref(Some(final_change.clone()));
164 let updated_uri = parsed.to_string();
165
166 if !Self::print_update_status(&input.input.id, &parsed_ref.previous_ref, &final_change)
167 {
168 return;
169 }
170
171 self.update_input(input.clone(), &updated_uri);
172 } else {
173 tracing::error!("Could not find latest version for Input: {:?}", input);
174 }
175 }
176 pub fn new(text: Rope, map: InputMap) -> Self {
177 let mut inputs = vec![];
178 for (_id, input) in map {
179 if input.url.is_empty() || input.range.start == 0 && input.range.end == 0 {
182 continue;
183 }
184 inputs.push(UpdateInput { input });
185 }
186 Self {
187 inputs,
188 text,
189 offset: 0,
190 }
191 }
192 fn get_index(&self, id: &str) -> Option<usize> {
193 let bare = id
194 .strip_prefix('"')
195 .and_then(|s| s.strip_suffix('"'))
196 .unwrap_or(id);
197 self.inputs.iter().position(|n| n.input.bare_id() == bare)
198 }
199 pub fn pin_input_to_ref(&mut self, id: &str, rev: &str) -> Result<(), String> {
201 self.sort();
202 let idx = self.get_index(id).ok_or_else(|| id.to_string())?;
203 let input = self.inputs[idx].clone();
204 tracing::debug!("Input: {:?}", input);
205 self.change_input_to_rev(&input, rev);
206 Ok(())
207 }
208 pub fn unpin_input(&mut self, id: &str) -> Result<(), String> {
210 self.sort();
211 let idx = self.get_index(id).ok_or_else(|| id.to_string())?;
212 let input = self.inputs[idx].clone();
213 tracing::debug!("Input: {:?}", input);
214 self.remove_ref_and_rev(&input);
215 Ok(())
216 }
217 pub fn update_all_inputs_to_latest_semver(&mut self, id: Option<String>, init: bool) {
220 self.sort();
221 let inputs = self.inputs.clone();
222 for input in inputs.iter() {
223 if let Some(ref input_id) = id {
224 if input.input.id == *input_id {
225 self.query_and_update_all_inputs(input, init);
226 }
227 } else {
228 self.query_and_update_all_inputs(input, init);
229 }
230 }
231 }
232 pub fn get_changes(&self) -> String {
233 self.text.to_string()
234 }
235
236 fn get_input_text(&self, input: &UpdateInput) -> String {
237 self.text
238 .slice(
239 ((input.input.range.start as i32) + 1 + self.offset) as usize
240 ..((input.input.range.end as i32) + self.offset - 1) as usize,
241 )
242 .to_string()
243 }
244
245 pub fn change_input_to_rev(&mut self, input: &UpdateInput, rev: &str) {
247 let uri = self.get_input_text(input);
248 match uri.parse::<FlakeRef>() {
249 Ok(mut parsed) => {
250 let _ = parsed.set_rev(Some(rev.into()));
252 self.update_input(input.clone(), &parsed.to_string());
253 }
254 Err(e) => {
255 tracing::error!("Error while changing input: {}", e);
256 }
257 }
258 }
259 fn remove_ref_and_rev(&mut self, input: &UpdateInput) {
260 let uri = self.get_input_text(input);
261 match uri.parse::<FlakeRef>() {
262 Ok(mut parsed) => {
263 if parsed.ref_source_location() == RefLocation::None {
264 return;
265 }
266 let _ = parsed.set_ref(None);
268 let _ = parsed.set_rev(None);
269 self.update_input(input.clone(), &parsed.to_string());
270 }
271 Err(e) => {
272 tracing::error!("Error while changing input: {}", e);
273 }
274 }
275 }
276 pub fn query_and_update_all_inputs(&mut self, input: &UpdateInput, init: bool) {
278 let uri = self.get_input_text(input);
279
280 let parsed = match uri.parse::<FlakeRef>() {
281 Ok(parsed) => parsed,
282 Err(e) => {
283 tracing::error!("Failed to parse URI: {}", e);
284 return;
285 }
286 };
287
288 let owner = match parsed.r#type.get_owner() {
289 Some(o) => o,
290 None => {
291 tracing::debug!("Skipping input without owner");
292 return;
293 }
294 };
295
296 let repo = match parsed.r#type.get_repo() {
297 Some(r) => r,
298 None => {
299 tracing::debug!("Skipping input without repo");
300 return;
301 }
302 };
303
304 let strategy = detect_strategy(&owner, &repo);
305 tracing::debug!("Update strategy for {}/{}: {:?}", owner, repo, strategy);
306
307 match strategy {
308 UpdateStrategy::NixpkgsChannel
309 | UpdateStrategy::HomeManagerChannel
310 | UpdateStrategy::NixDarwinChannel => {
311 self.update_channel_input(input, &parsed);
312 }
313 UpdateStrategy::SemverTags => {
314 self.update_semver_input(input, init);
315 }
316 }
317 }
318
319 fn update_channel_input(&mut self, input: &UpdateInput, parsed: &FlakeRef) {
321 let owner = parsed.r#type.get_owner().unwrap();
322 let repo = parsed.r#type.get_repo().unwrap();
323 let domain = parsed.r#type.get_domain();
324
325 let current_ref = parsed.get_ref_or_rev().unwrap_or_default();
326
327 if current_ref.is_empty() {
328 tracing::debug!("Skipping unpinned channel input: {}", input.input.id);
329 return;
330 }
331
332 let has_refs_heads_prefix = current_ref.starts_with("refs/heads/");
333
334 let latest = match find_latest_channel(¤t_ref, &owner, &repo, domain.as_deref()) {
335 Some(latest) => latest,
336 None => return,
338 };
339
340 let final_ref = if has_refs_heads_prefix {
341 format!("refs/heads/{}", latest)
342 } else {
343 latest.clone()
344 };
345
346 let mut parsed = parsed.clone();
347 let _ = parsed.set_ref(Some(final_ref.clone()));
348 let updated_uri = parsed.to_string();
349
350 if Self::print_update_status(&input.input.id, ¤t_ref, &final_ref) {
351 self.update_input(input.clone(), &updated_uri);
352 }
353 }
354
355 fn update_semver_input(&mut self, input: &UpdateInput, init: bool) {
357 let target = match self.parse_update_target(input, init) {
358 Some(target) => target,
359 None => return,
360 };
361
362 let tags = match self.fetch_tags(&target) {
363 Some(tags) => tags,
364 None => return,
365 };
366
367 self.apply_update(input, &target, tags, init);
368 }
369
370 fn sort(&mut self) {
372 self.inputs.sort();
373 }
374 fn update_input(&mut self, input: UpdateInput, change: &str) {
375 self.text.remove(
376 (input.input.range.start as i32 + 1 + self.offset) as usize
377 ..(input.input.range.end as i32 - 1 + self.offset) as usize,
378 );
379 self.text.insert(
380 (input.input.range.start as i32 + 1 + self.offset) as usize,
381 change,
382 );
383 self.update_offset(input.clone(), change);
384 }
385 fn update_offset(&mut self, input: UpdateInput, change: &str) {
386 let previous_len = input.input.range.end as i32 - input.input.range.start as i32 - 2;
387 let len = change.len() as i32;
388 let offset = len - previous_len;
389 self.offset += offset;
390 }
391}
392
393#[derive(Debug, Clone)]
395pub struct UpdateInput {
396 input: Input,
397}
398
399impl Ord for UpdateInput {
400 fn cmp(&self, other: &Self) -> Ordering {
401 (self.input.range.start).cmp(&(other.input.range.start))
402 }
403}
404
405impl PartialEq for UpdateInput {
406 fn eq(&self, other: &Self) -> bool {
407 self.input.range.start == other.input.range.start
408 }
409}
410
411impl PartialOrd for UpdateInput {
412 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
413 Some(self.cmp(other))
414 }
415}
416
417impl Eq for UpdateInput {}