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 inputs.push(UpdateInput { input });
180 }
181 Self {
182 inputs,
183 text,
184 offset: 0,
185 }
186 }
187 fn get_index(&self, id: &str) -> usize {
188 self.inputs.iter().position(|n| n.input.id == id).unwrap()
189 }
190 pub fn pin_input_to_ref(&mut self, id: &str, rev: &str) {
192 self.sort();
193 let inputs = self.inputs.clone();
194 if let Some(input) = inputs.get(self.get_index(id)) {
195 tracing::debug!("Input: {:?}", input);
196 self.change_input_to_rev(input, rev);
197 }
198 }
199 pub fn unpin_input(&mut self, id: &str) {
201 self.sort();
202 let inputs = self.inputs.clone();
203 if let Some(input) = inputs.get(self.get_index(id)) {
204 tracing::debug!("Input: {:?}", input);
205 self.remove_ref_and_rev(input);
206 }
207 }
208 pub fn update_all_inputs_to_latest_semver(&mut self, id: Option<String>, init: bool) {
211 self.sort();
212 let inputs = self.inputs.clone();
213 for input in inputs.iter() {
214 if let Some(ref input_id) = id {
215 if input.input.id == *input_id {
216 self.query_and_update_all_inputs(input, init);
217 }
218 } else {
219 self.query_and_update_all_inputs(input, init);
220 }
221 }
222 }
223 pub fn get_changes(&self) -> String {
224 self.text.to_string()
225 }
226
227 fn get_input_text(&self, input: &UpdateInput) -> String {
228 self.text
229 .slice(
230 ((input.input.range.start as i32) + 1 + self.offset) as usize
231 ..((input.input.range.end as i32) + self.offset - 1) as usize,
232 )
233 .to_string()
234 }
235
236 pub fn change_input_to_rev(&mut self, input: &UpdateInput, rev: &str) {
238 let uri = self.get_input_text(input);
239 match uri.parse::<FlakeRef>() {
240 Ok(mut parsed) => {
241 let _ = parsed.set_rev(Some(rev.into()));
243 self.update_input(input.clone(), &parsed.to_string());
244 }
245 Err(e) => {
246 tracing::error!("Error while changing input: {}", e);
247 }
248 }
249 }
250 fn remove_ref_and_rev(&mut self, input: &UpdateInput) {
251 let uri = self.get_input_text(input);
252 match uri.parse::<FlakeRef>() {
253 Ok(mut parsed) => {
254 if parsed.ref_source_location() == RefLocation::None {
255 return;
256 }
257 let _ = parsed.set_ref(None);
259 let _ = parsed.set_rev(None);
260 self.update_input(input.clone(), &parsed.to_string());
261 }
262 Err(e) => {
263 tracing::error!("Error while changing input: {}", e);
264 }
265 }
266 }
267 pub fn query_and_update_all_inputs(&mut self, input: &UpdateInput, init: bool) {
269 let uri = self.get_input_text(input);
270
271 let parsed = match uri.parse::<FlakeRef>() {
272 Ok(parsed) => parsed,
273 Err(e) => {
274 tracing::error!("Failed to parse URI: {}", e);
275 return;
276 }
277 };
278
279 let owner = match parsed.r#type.get_owner() {
280 Some(o) => o,
281 None => {
282 tracing::debug!("Skipping input without owner");
283 return;
284 }
285 };
286
287 let repo = match parsed.r#type.get_repo() {
288 Some(r) => r,
289 None => {
290 tracing::debug!("Skipping input without repo");
291 return;
292 }
293 };
294
295 let strategy = detect_strategy(&owner, &repo);
296 tracing::debug!("Update strategy for {}/{}: {:?}", owner, repo, strategy);
297
298 match strategy {
299 UpdateStrategy::NixpkgsChannel
300 | UpdateStrategy::HomeManagerChannel
301 | UpdateStrategy::NixDarwinChannel => {
302 self.update_channel_input(input, &parsed);
303 }
304 UpdateStrategy::SemverTags => {
305 self.update_semver_input(input, init);
306 }
307 }
308 }
309
310 fn update_channel_input(&mut self, input: &UpdateInput, parsed: &FlakeRef) {
312 let owner = parsed.r#type.get_owner().unwrap();
313 let repo = parsed.r#type.get_repo().unwrap();
314 let domain = parsed.r#type.get_domain();
315
316 let current_ref = parsed.get_ref_or_rev().unwrap_or_default();
317
318 if current_ref.is_empty() {
319 tracing::debug!("Skipping unpinned channel input: {}", input.input.id);
320 return;
321 }
322
323 let has_refs_heads_prefix = current_ref.starts_with("refs/heads/");
324
325 let latest = match find_latest_channel(¤t_ref, &owner, &repo, domain.as_deref()) {
326 Some(latest) => latest,
327 None => return,
329 };
330
331 let final_ref = if has_refs_heads_prefix {
332 format!("refs/heads/{}", latest)
333 } else {
334 latest.clone()
335 };
336
337 let mut parsed = parsed.clone();
338 let _ = parsed.set_ref(Some(final_ref.clone()));
339 let updated_uri = parsed.to_string();
340
341 if Self::print_update_status(&input.input.id, ¤t_ref, &final_ref) {
342 self.update_input(input.clone(), &updated_uri);
343 }
344 }
345
346 fn update_semver_input(&mut self, input: &UpdateInput, init: bool) {
348 let target = match self.parse_update_target(input, init) {
349 Some(target) => target,
350 None => return,
351 };
352
353 let tags = match self.fetch_tags(&target) {
354 Some(tags) => tags,
355 None => return,
356 };
357
358 self.apply_update(input, &target, tags, init);
359 }
360
361 fn sort(&mut self) {
363 self.inputs.sort();
364 }
365 fn update_input(&mut self, input: UpdateInput, change: &str) {
366 self.text.remove(
367 (input.input.range.start as i32 + 1 + self.offset) as usize
368 ..(input.input.range.end as i32 - 1 + self.offset) as usize,
369 );
370 self.text.insert(
371 (input.input.range.start as i32 + 1 + self.offset) as usize,
372 change,
373 );
374 self.update_offset(input.clone(), change);
375 }
376 fn update_offset(&mut self, input: UpdateInput, change: &str) {
377 let previous_len = input.input.range.end as i32 - input.input.range.start as i32 - 2;
378 let len = change.len() as i32;
379 let offset = len - previous_len;
380 self.offset += offset;
381 }
382}
383
384#[derive(Debug, Clone)]
386pub struct UpdateInput {
387 input: Input,
388}
389
390impl Ord for UpdateInput {
391 fn cmp(&self, other: &Self) -> Ordering {
392 (self.input.range.start).cmp(&(other.input.range.start))
393 }
394}
395
396impl PartialEq for UpdateInput {
397 fn eq(&self, other: &Self) -> bool {
398 self.input.range.start == other.input.range.start
399 }
400}
401
402impl PartialOrd for UpdateInput {
403 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
404 Some(self.cmp(other))
405 }
406}
407
408impl Eq for UpdateInput {}