use nix_uri::{FlakeRef, RefLocation};
use ropey::Rope;
use std::cmp::Ordering;
use crate::channel::{UpdateStrategy, detect_strategy, find_latest_channel};
use crate::edit::InputMap;
use crate::input::Input;
use crate::uri::is_git_url;
use crate::version::parse_ref;
#[derive(Default, Debug)]
pub struct Updater {
text: Rope,
inputs: Vec<UpdateInput>,
offset: i32,
}
enum UpdateTarget {
GitUrl {
parsed: Box<FlakeRef>,
owner: String,
repo: String,
domain: String,
parsed_ref: crate::version::ParsedRef,
},
ForgeRef {
parsed: Box<FlakeRef>,
owner: String,
repo: String,
parsed_ref: crate::version::ParsedRef,
},
}
impl Updater {
fn print_update_status(id: &str, previous_version: &str, final_change: &str) -> bool {
let is_up_to_date = previous_version == final_change;
let initialized = previous_version.is_empty();
if is_up_to_date {
println!(
"{} is already on the latest version: {previous_version}.",
id
);
return false;
}
if initialized {
println!("Initialized {} version pin at {final_change}.", id);
} else {
println!("Updated {} from {previous_version} to {final_change}.", id);
}
true
}
fn parse_update_target(&self, input: &UpdateInput, init: bool) -> Option<UpdateTarget> {
let uri = self.get_input_text(input);
let is_git_url = is_git_url(&uri);
let parsed = match uri.parse::<FlakeRef>() {
Ok(parsed) => parsed,
Err(e) => {
tracing::error!("Failed to parse URI: {}", e);
return None;
}
};
let maybe_version = parsed.get_ref_or_rev().unwrap_or_default();
let parsed_ref = parse_ref(&maybe_version, init);
if !init && let Err(e) = semver::Version::parse(&parsed_ref.normalized_for_semver) {
tracing::debug!("Skip non semver version: {}: {}", maybe_version, e);
return None;
}
let owner = match parsed.r#type.get_owner() {
Some(o) => o,
None => {
tracing::debug!("Skipping input without owner");
return None;
}
};
let repo = match parsed.r#type.get_repo() {
Some(r) => r,
None => {
tracing::debug!("Skipping input without repo");
return None;
}
};
if is_git_url {
let domain = parsed.r#type.get_domain()?;
return Some(UpdateTarget::GitUrl {
parsed: Box::new(parsed),
owner,
repo,
domain,
parsed_ref,
});
}
Some(UpdateTarget::ForgeRef {
parsed: Box::new(parsed),
owner,
repo,
parsed_ref,
})
}
fn fetch_tags(&self, target: &UpdateTarget) -> Option<crate::api::Tags> {
match target {
UpdateTarget::GitUrl {
owner,
repo,
domain,
..
} => match crate::api::get_tags(repo, owner, Some(domain)) {
Ok(tags) => Some(tags),
Err(_) => {
tracing::error!("Failed to fetch tags for {}/{} on {}", owner, repo, domain);
None
}
},
UpdateTarget::ForgeRef { owner, repo, .. } => {
match crate::api::get_tags(repo, owner, None) {
Ok(tags) => Some(tags),
Err(_) => {
tracing::error!("Failed to fetch tags for {}/{}", owner, repo);
None
}
}
}
}
}
fn apply_update(
&mut self,
input: &UpdateInput,
target: &UpdateTarget,
mut tags: crate::api::Tags,
_init: bool,
) {
tags.sort();
if let Some(change) = tags.get_latest_tag() {
let (parsed, parsed_ref) = match target {
UpdateTarget::GitUrl {
parsed, parsed_ref, ..
} => (parsed, parsed_ref),
UpdateTarget::ForgeRef {
parsed, parsed_ref, ..
} => (parsed, parsed_ref),
};
let final_change = if parsed_ref.has_refs_tags_prefix {
format!("refs/tags/{}", change)
} else {
change.clone()
};
let mut parsed = parsed.clone();
let _ = parsed.set_ref(Some(final_change.clone()));
let updated_uri = parsed.to_string();
if !Self::print_update_status(&input.input.id, &parsed_ref.previous_ref, &final_change)
{
return;
}
self.update_input(input.clone(), &updated_uri);
} else {
tracing::error!("Could not find latest version for Input: {:?}", input);
}
}
pub fn new(text: Rope, map: InputMap) -> Self {
let mut inputs = vec![];
for (_id, input) in map {
if input.url.is_empty() || input.range.start == 0 && input.range.end == 0 {
continue;
}
inputs.push(UpdateInput { input });
}
Self {
inputs,
text,
offset: 0,
}
}
fn get_index(&self, id: &str) -> Option<usize> {
let bare = id
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(id);
self.inputs.iter().position(|n| n.input.bare_id() == bare)
}
pub fn pin_input_to_ref(&mut self, id: &str, rev: &str) -> Result<(), String> {
self.sort();
let idx = self.get_index(id).ok_or_else(|| id.to_string())?;
let input = self.inputs[idx].clone();
tracing::debug!("Input: {:?}", input);
self.change_input_to_rev(&input, rev);
Ok(())
}
pub fn unpin_input(&mut self, id: &str) -> Result<(), String> {
self.sort();
let idx = self.get_index(id).ok_or_else(|| id.to_string())?;
let input = self.inputs[idx].clone();
tracing::debug!("Input: {:?}", input);
self.remove_ref_and_rev(&input);
Ok(())
}
pub fn update_all_inputs_to_latest_semver(&mut self, id: Option<String>, init: bool) {
self.sort();
let inputs = self.inputs.clone();
for input in inputs.iter() {
if let Some(ref input_id) = id {
if input.input.id == *input_id {
self.query_and_update_all_inputs(input, init);
}
} else {
self.query_and_update_all_inputs(input, init);
}
}
}
pub fn get_changes(&self) -> String {
self.text.to_string()
}
fn get_input_text(&self, input: &UpdateInput) -> String {
self.text
.slice(
((input.input.range.start as i32) + 1 + self.offset) as usize
..((input.input.range.end as i32) + self.offset - 1) as usize,
)
.to_string()
}
pub fn change_input_to_rev(&mut self, input: &UpdateInput, rev: &str) {
let uri = self.get_input_text(input);
match uri.parse::<FlakeRef>() {
Ok(mut parsed) => {
let _ = parsed.set_rev(Some(rev.into()));
self.update_input(input.clone(), &parsed.to_string());
}
Err(e) => {
tracing::error!("Error while changing input: {}", e);
}
}
}
fn remove_ref_and_rev(&mut self, input: &UpdateInput) {
let uri = self.get_input_text(input);
match uri.parse::<FlakeRef>() {
Ok(mut parsed) => {
if parsed.ref_source_location() == RefLocation::None {
return;
}
let _ = parsed.set_ref(None);
let _ = parsed.set_rev(None);
self.update_input(input.clone(), &parsed.to_string());
}
Err(e) => {
tracing::error!("Error while changing input: {}", e);
}
}
}
pub fn query_and_update_all_inputs(&mut self, input: &UpdateInput, init: bool) {
let uri = self.get_input_text(input);
let parsed = match uri.parse::<FlakeRef>() {
Ok(parsed) => parsed,
Err(e) => {
tracing::error!("Failed to parse URI: {}", e);
return;
}
};
let owner = match parsed.r#type.get_owner() {
Some(o) => o,
None => {
tracing::debug!("Skipping input without owner");
return;
}
};
let repo = match parsed.r#type.get_repo() {
Some(r) => r,
None => {
tracing::debug!("Skipping input without repo");
return;
}
};
let strategy = detect_strategy(&owner, &repo);
tracing::debug!("Update strategy for {}/{}: {:?}", owner, repo, strategy);
match strategy {
UpdateStrategy::NixpkgsChannel
| UpdateStrategy::HomeManagerChannel
| UpdateStrategy::NixDarwinChannel => {
self.update_channel_input(input, &parsed);
}
UpdateStrategy::SemverTags => {
self.update_semver_input(input, init);
}
}
}
fn update_channel_input(&mut self, input: &UpdateInput, parsed: &FlakeRef) {
let owner = parsed.r#type.get_owner().unwrap();
let repo = parsed.r#type.get_repo().unwrap();
let domain = parsed.r#type.get_domain();
let current_ref = parsed.get_ref_or_rev().unwrap_or_default();
if current_ref.is_empty() {
tracing::debug!("Skipping unpinned channel input: {}", input.input.id);
return;
}
let has_refs_heads_prefix = current_ref.starts_with("refs/heads/");
let latest = match find_latest_channel(¤t_ref, &owner, &repo, domain.as_deref()) {
Some(latest) => latest,
None => return,
};
let final_ref = if has_refs_heads_prefix {
format!("refs/heads/{}", latest)
} else {
latest.clone()
};
let mut parsed = parsed.clone();
let _ = parsed.set_ref(Some(final_ref.clone()));
let updated_uri = parsed.to_string();
if Self::print_update_status(&input.input.id, ¤t_ref, &final_ref) {
self.update_input(input.clone(), &updated_uri);
}
}
fn update_semver_input(&mut self, input: &UpdateInput, init: bool) {
let target = match self.parse_update_target(input, init) {
Some(target) => target,
None => return,
};
let tags = match self.fetch_tags(&target) {
Some(tags) => tags,
None => return,
};
self.apply_update(input, &target, tags, init);
}
fn sort(&mut self) {
self.inputs.sort();
}
fn update_input(&mut self, input: UpdateInput, change: &str) {
self.text.remove(
(input.input.range.start as i32 + 1 + self.offset) as usize
..(input.input.range.end as i32 - 1 + self.offset) as usize,
);
self.text.insert(
(input.input.range.start as i32 + 1 + self.offset) as usize,
change,
);
self.update_offset(input.clone(), change);
}
fn update_offset(&mut self, input: UpdateInput, change: &str) {
let previous_len = input.input.range.end as i32 - input.input.range.start as i32 - 2;
let len = change.len() as i32;
let offset = len - previous_len;
self.offset += offset;
}
}
#[derive(Debug, Clone)]
pub struct UpdateInput {
input: Input,
}
impl Ord for UpdateInput {
fn cmp(&self, other: &Self) -> Ordering {
(self.input.range.start).cmp(&(other.input.range.start))
}
}
impl PartialEq for UpdateInput {
fn eq(&self, other: &Self) -> bool {
self.input.range.start == other.input.range.start
}
}
impl PartialOrd for UpdateInput {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Eq for UpdateInput {}