use crate::api::{Branches, branch_exists, get_branches};
#[derive(Debug, Clone, PartialEq)]
pub enum UpdateStrategy {
SemverTags,
NixpkgsChannel,
HomeManagerChannel,
NixDarwinChannel,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ChannelType {
NixosStable { year: u32, month: u32 },
NixpkgsStable { year: u32, month: u32 },
Unstable,
HomeManagerRelease { year: u32, month: u32 },
NixDarwinStable { year: u32, month: u32 },
BareVersion { year: u32, month: u32 },
Unknown,
}
impl ChannelType {
pub fn is_unstable(&self) -> bool {
matches!(self, ChannelType::Unstable)
}
pub fn version(&self) -> Option<(u32, u32)> {
match self {
ChannelType::NixosStable { year, month }
| ChannelType::NixpkgsStable { year, month }
| ChannelType::HomeManagerRelease { year, month }
| ChannelType::NixDarwinStable { year, month }
| ChannelType::BareVersion { year, month } => Some((*year, *month)),
_ => None,
}
}
pub fn prefix(&self) -> Option<&'static str> {
match self {
ChannelType::NixosStable { .. } => Some("nixos-"),
ChannelType::NixpkgsStable { .. } => Some("nixpkgs-"),
ChannelType::HomeManagerRelease { .. } => Some("release-"),
ChannelType::NixDarwinStable { .. } => Some("nix-darwin-"),
ChannelType::BareVersion { .. } => Some(""),
_ => None,
}
}
}
pub fn detect_strategy(owner: &str, repo: &str) -> UpdateStrategy {
match (owner.to_lowercase().as_str(), repo.to_lowercase().as_str()) {
("nixos", "nixpkgs") => UpdateStrategy::NixpkgsChannel,
("nix-community", "home-manager") => UpdateStrategy::HomeManagerChannel,
("lnl7", "nix-darwin") | ("nix-community", "nix-darwin") => {
UpdateStrategy::NixDarwinChannel
}
_ => UpdateStrategy::SemverTags,
}
}
pub fn parse_channel_ref(ref_str: &str) -> ChannelType {
let ref_str = ref_str.strip_prefix("refs/heads/").unwrap_or(ref_str);
if ref_str == "nixos-unstable" || ref_str == "nixpkgs-unstable" {
return ChannelType::Unstable;
}
if ref_str == "master" || ref_str == "main" {
return ChannelType::Unstable;
}
if let Some(version) = ref_str.strip_prefix("nixos-")
&& let Some((year, month)) = parse_version(version)
{
return ChannelType::NixosStable { year, month };
}
if let Some(version) = ref_str.strip_prefix("nixpkgs-")
&& let Some((year, month)) = parse_version(version)
{
return ChannelType::NixpkgsStable { year, month };
}
if let Some(version) = ref_str.strip_prefix("release-")
&& let Some((year, month)) = parse_version(version)
{
return ChannelType::HomeManagerRelease { year, month };
}
if let Some(version) = ref_str.strip_prefix("nix-darwin-")
&& let Some((year, month)) = parse_version(version)
{
return ChannelType::NixDarwinStable { year, month };
}
if let Some((year, month)) = parse_version(ref_str) {
return ChannelType::BareVersion { year, month };
}
ChannelType::Unknown
}
fn parse_version(version: &str) -> Option<(u32, u32)> {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() == 2 {
let year = parts[0].parse::<u32>().ok()?;
let month = parts[1].parse::<u32>().ok()?;
if (20..=99).contains(&year) && (month == 5 || month == 11) {
return Some((year, month));
}
}
None
}
pub fn find_latest_channel(
current_ref: &str,
owner: &str,
repo: &str,
domain: Option<&str>,
) -> Option<String> {
let current_channel = parse_channel_ref(current_ref);
if current_channel.is_unstable() {
tracing::debug!("Skipping update for unstable channel: {}", current_ref);
return None;
}
let prefix = current_channel.prefix()?;
let current_version = current_channel.version()?;
if let Some(latest) = find_latest_channel_targeted(prefix, current_version, owner, repo, domain)
{
if latest != current_ref {
return Some(latest);
} else {
tracing::debug!("{} is already on the latest channel", current_ref);
return None;
}
}
tracing::debug!("Targeted lookup failed, falling back to listing all branches");
let branches = match get_branches(repo, owner, domain) {
Ok(b) => b,
Err(e) => {
tracing::error!("Failed to fetch branches: {}", e);
return None;
}
};
let latest = find_latest_matching_branch(&branches, prefix, current_version);
if let Some(ref latest_branch) = latest
&& latest_branch == current_ref
{
tracing::debug!("{} is already on the latest channel", current_ref);
return None;
}
latest
}
fn generate_candidate_channels(prefix: &str, current_version: (u32, u32)) -> Vec<String> {
let (current_year, current_month) = current_version;
let mut candidates = Vec::new();
let mut year = current_year;
let mut month = current_month;
for _ in 0..10 {
if month == 5 {
month = 11;
} else {
month = 5;
year += 1;
}
candidates.push(format!("{}{}.{:02}", prefix, year, month));
}
candidates.reverse();
candidates
}
fn find_latest_channel_targeted(
prefix: &str,
current_version: (u32, u32),
owner: &str,
repo: &str,
domain: Option<&str>,
) -> Option<String> {
let candidates = generate_candidate_channels(prefix, current_version);
tracing::debug!(
"Checking candidate channels (newest first): {:?}",
candidates
);
for candidate in &candidates {
tracing::debug!("Checking if branch exists: {}", candidate);
if branch_exists(repo, owner, candidate, domain) {
tracing::debug!("Found existing channel: {}", candidate);
return Some(candidate.clone());
}
}
let current_branch = format!("{}{}.{:02}", prefix, current_version.0, current_version.1);
tracing::debug!("No newer channel, checking current: {}", current_branch);
if branch_exists(repo, owner, ¤t_branch, domain) {
return Some(current_branch);
}
None
}
fn find_latest_matching_branch(
branches: &Branches,
prefix: &str,
current_version: (u32, u32),
) -> Option<String> {
let mut best: Option<(u32, u32, String)> = None;
for branch_name in &branches.names {
if let Some(version_str) = branch_name.strip_prefix(prefix) {
if version_str == "unstable" {
continue;
}
if let Some((year, month)) = parse_version(version_str) {
if (year, month) >= current_version {
match &best {
None => {
best = Some((year, month, branch_name.clone()));
}
Some((best_year, best_month, _)) => {
if (year, month) > (*best_year, *best_month) {
best = Some((year, month, branch_name.clone()));
}
}
}
}
}
}
}
best.map(|(_, _, name)| name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_strategy() {
assert_eq!(
detect_strategy("nixos", "nixpkgs"),
UpdateStrategy::NixpkgsChannel
);
assert_eq!(
detect_strategy("NixOS", "nixpkgs"),
UpdateStrategy::NixpkgsChannel
);
assert_eq!(
detect_strategy("nix-community", "home-manager"),
UpdateStrategy::HomeManagerChannel
);
assert_eq!(
detect_strategy("LnL7", "nix-darwin"),
UpdateStrategy::NixDarwinChannel
);
assert_eq!(
detect_strategy("nix-community", "nix-darwin"),
UpdateStrategy::NixDarwinChannel
);
assert_eq!(
detect_strategy("some-user", "some-repo"),
UpdateStrategy::SemverTags
);
}
#[test]
fn test_parse_channel_ref_nixos() {
assert_eq!(
parse_channel_ref("nixos-24.11"),
ChannelType::NixosStable {
year: 24,
month: 11
}
);
assert_eq!(
parse_channel_ref("nixos-25.05"),
ChannelType::NixosStable { year: 25, month: 5 }
);
assert_eq!(parse_channel_ref("nixos-unstable"), ChannelType::Unstable);
}
#[test]
fn test_parse_channel_ref_nixpkgs() {
assert_eq!(
parse_channel_ref("nixpkgs-24.11"),
ChannelType::NixpkgsStable {
year: 24,
month: 11
}
);
assert_eq!(parse_channel_ref("nixpkgs-unstable"), ChannelType::Unstable);
}
#[test]
fn test_parse_channel_ref_home_manager() {
assert_eq!(
parse_channel_ref("release-24.11"),
ChannelType::HomeManagerRelease {
year: 24,
month: 11
}
);
assert_eq!(parse_channel_ref("master"), ChannelType::Unstable);
}
#[test]
fn test_parse_channel_ref_nix_darwin() {
assert_eq!(
parse_channel_ref("nix-darwin-24.11"),
ChannelType::NixDarwinStable {
year: 24,
month: 11
}
);
assert_eq!(parse_channel_ref("main"), ChannelType::Unstable);
}
#[test]
fn test_parse_channel_ref_bare_version() {
assert_eq!(
parse_channel_ref("24.11"),
ChannelType::BareVersion {
year: 24,
month: 11
}
);
assert_eq!(
parse_channel_ref("25.05"),
ChannelType::BareVersion { year: 25, month: 5 }
);
}
#[test]
fn test_parse_channel_ref_with_refs_heads_prefix() {
assert_eq!(
parse_channel_ref("refs/heads/nixos-24.11"),
ChannelType::NixosStable {
year: 24,
month: 11
}
);
}
#[test]
fn test_parse_channel_ref_unknown() {
assert_eq!(parse_channel_ref("v1.0.0"), ChannelType::Unknown);
assert_eq!(parse_channel_ref("nixos-invalid"), ChannelType::Unknown);
assert_eq!(parse_channel_ref("feature-branch"), ChannelType::Unknown);
}
#[test]
fn test_channel_type_is_unstable() {
assert!(ChannelType::Unstable.is_unstable());
assert!(parse_channel_ref("master").is_unstable());
assert!(parse_channel_ref("main").is_unstable());
assert!(parse_channel_ref("nixos-unstable").is_unstable());
assert!(
!ChannelType::NixosStable {
year: 24,
month: 11
}
.is_unstable()
);
}
#[test]
fn test_find_latest_matching_branch() {
let branches = Branches {
names: vec![
"nixos-23.11".to_string(),
"nixos-24.05".to_string(),
"nixos-24.11".to_string(),
"nixos-unstable".to_string(),
"master".to_string(),
],
};
let result = find_latest_matching_branch(&branches, "nixos-", (24, 5));
assert_eq!(result, Some("nixos-24.11".to_string()));
let result = find_latest_matching_branch(&branches, "nixos-", (24, 11));
assert_eq!(result, Some("nixos-24.11".to_string()));
let result = find_latest_matching_branch(&branches, "nixos-", (25, 5));
assert_eq!(result, None);
}
#[test]
fn test_generate_candidate_channels() {
let candidates = generate_candidate_channels("nixos-", (24, 5));
assert_eq!(candidates.len(), 10);
assert_eq!(candidates[0], "nixos-29.05");
assert_eq!(candidates[9], "nixos-24.11");
let candidates = generate_candidate_channels("nixpkgs-", (24, 11));
assert_eq!(candidates[0], "nixpkgs-29.11"); assert_eq!(candidates[9], "nixpkgs-25.05"); }
}