Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/cli/commands/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use crate::sync::{GitBackend, MachineState, SyncEngine, SyncState};
use anyhow::Result;
use comfy_table::{Attribute, Cell, Color};
use owo_colors::OwoColorize;
use sha2::{Digest, Sha256};
use std::collections::{HashMap, HashSet};

pub async fn run(machine: Option<&str>) -> Result<()> {
Expand Down Expand Up @@ -103,7 +102,7 @@ fn show_dotfile_diff(
(true, true) => {
// Both exist - check if different
let local_content = std::fs::read(&local_path)?;
let local_hash = format!("{:x}", Sha256::digest(&local_content));
let local_hash = crate::sha256_hex(&local_content);

let is_different = state
.files
Expand Down Expand Up @@ -389,7 +388,7 @@ fn build_current_machine_state(
let path = home.join(file);
if path.exists() {
let content = std::fs::read(&path)?;
let hash = format!("{:x}", sha2::Sha256::digest(&content));
let hash = crate::sha256_hex(&content);
machine.files.insert(file.to_string(), hash);
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/cli/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,10 @@ fn assign_profile_during_init(config: &mut Config) -> Result<()> {
// No profiles exist yet — v1->v2 migration should have created "dev"
// but if it hasn't (e.g., fresh init), create it now
config.migrate_v1_to_v2();
config
.machine_profiles
.insert(machine_id.clone(), "dev".to_string());
config.machine_profiles.insert(
machine_id.clone(),
crate::config::DEFAULT_PROFILE.to_string(),
);
return Ok(());
}

Expand Down
5 changes: 2 additions & 3 deletions src/cli/commands/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use crate::config::Config;
use crate::sync::{ConflictResolution, ConflictState, FileConflict, SyncEngine};
use anyhow::Result;
use owo_colors::OwoColorize;
use sha2::{Digest, Sha256};

pub async fn run(file: Option<&str>) -> Result<()> {
let config = Config::load()?;
Expand Down Expand Up @@ -94,9 +93,9 @@ pub async fn run(file: Option<&str>) -> Result<()> {

let conflict = FileConflict {
file_path: pending.file_path.clone(),
local_hash: format!("{:x}", Sha256::digest(&local_content)),
local_hash: crate::sha256_hex(&local_content),
last_synced_hash: None,
remote_hash: format!("{:x}", Sha256::digest(&remote_content)),
remote_hash: crate::sha256_hex(&remote_content),
local_content,
remote_content,
};
Expand Down
54 changes: 25 additions & 29 deletions src/cli/commands/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use crate::sync::{
import_packages, sync_packages, GitBackend, MachineState, SyncEngine, SyncState,
};
use anyhow::Result;
use sha2::{Digest, Sha256};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

Expand Down Expand Up @@ -103,11 +102,12 @@ pub async fn run(dry_run: bool, _force: bool, rediscover: bool) -> Result<()> {

let mut state = SyncState::load()?;

// Auto-assign machine to "dev" profile on first run after v2 migration
// Auto-assign machine to default profile on first run after v2 migration
if !config.profiles.is_empty() && !config.machine_profiles.contains_key(&state.machine_id) {
config
.machine_profiles
.insert(state.machine_id.clone(), "dev".to_string());
config.machine_profiles.insert(
state.machine_id.clone(),
crate::config::DEFAULT_PROFILE.to_string(),
);
config.save()?;
}

Expand Down Expand Up @@ -184,7 +184,7 @@ pub async fn run(dry_run: bool, _force: bool, rediscover: bool) -> Result<()> {

if source.exists() {
if let Ok(content) = std::fs::read(&source) {
let hash = format!("{:x}", Sha256::digest(&content));
let hash = crate::sha256_hex(&content);

let file_changed = state
.files
Expand Down Expand Up @@ -535,7 +535,7 @@ pub fn sync_collab_secrets(config: &Config, home: &Path, state: &mut SyncState)
let state_key =
format!("collab-secret:{}/{}/{}", collab_name, project_url, filename);
let last_synced_hash = state.files.get(&state_key).map(|f| f.hash.as_str());
let remote_hash = format!("{:x}", Sha256::digest(&decrypted));
let remote_hash = crate::sha256_hex(&decrypted);

// Write to all checkouts of this project
for local_project in checkouts {
Expand Down Expand Up @@ -570,7 +570,7 @@ pub fn sync_collab_secrets(config: &Config, home: &Path, state: &mut SyncState)

let should_write = if dest.exists() {
let existing = std::fs::read(&dest).unwrap_or_default();
let local_hash = format!("{:x}", Sha256::digest(&existing));
let local_hash = crate::sha256_hex(&existing);
if local_hash == remote_hash {
false // Already in sync
} else {
Expand Down Expand Up @@ -804,10 +804,10 @@ pub fn decrypt_from_repo(
}
} else {
// No true conflict - but preserve local-only changes
let remote_hash = format!("{:x}", Sha256::digest(&plaintext));
let remote_hash = crate::sha256_hex(&plaintext);
let local_hash = std::fs::read(&local_file)
.ok()
.map(|c| format!("{:x}", Sha256::digest(&c)));
.map(|c| crate::sha256_hex(&c));

// Only write if local unchanged from last sync AND remote differs
let local_unchanged = local_hash.as_deref() == last_synced_hash;
Expand Down Expand Up @@ -895,10 +895,10 @@ pub fn decrypt_from_repo(
let state_key = format!("~/{}", rel_path_no_enc);
let last_synced_hash =
state.files.get(&state_key).map(|f| f.hash.as_str());
let remote_hash = format!("{:x}", Sha256::digest(&plaintext));
let remote_hash = crate::sha256_hex(&plaintext);
let local_hash = std::fs::read(&local_file)
.ok()
.map(|c| format!("{:x}", Sha256::digest(&c)));
.map(|c| crate::sha256_hex(&c));
let local_unchanged = local_hash.as_deref() == last_synced_hash;
if local_unchanged && local_hash.as_ref() != Some(&remote_hash) {
write_decrypted(&local_file, &plaintext)?;
Expand Down Expand Up @@ -1259,7 +1259,7 @@ fn decrypt_project_configs(
if let Ok(encrypted_content) = std::fs::read(enc_file) {
match crate::security::decrypt(&encrypted_content, key) {
Ok(plaintext) => {
let remote_hash = format!("{:x}", Sha256::digest(&plaintext));
let remote_hash = crate::sha256_hex(&plaintext);
let state_key = format!("project:{}/{}", project_name, rel_path_no_enc);
let canonical_path = crate::sync::canonical_project_file_path(
project_name,
Expand All @@ -1275,8 +1275,7 @@ fn decrypt_project_configs(
let local_file = local_repo_path.join(rel_path_no_enc);
// Read actual content (follows symlinks)
if let Ok(local_content) = std::fs::read(&local_file) {
let local_hash =
format!("{:x}", Sha256::digest(&local_content));
let local_hash = crate::sha256_hex(&local_content);
if Some(&local_hash) != last_synced_hash.as_ref()
&& local_hash != remote_hash
{
Expand All @@ -1294,9 +1293,8 @@ fn decrypt_project_configs(
} else {
// Write decrypted content to canonical location
let canonical_content = std::fs::read(&canonical_path).ok();
let canonical_hash = canonical_content
.as_ref()
.map(|c| format!("{:x}", Sha256::digest(c)));
let canonical_hash =
canonical_content.as_ref().map(|c| crate::sha256_hex(c));

if canonical_hash.as_ref() != Some(&remote_hash) {
// Backup canonical file if it exists and differs
Expand Down Expand Up @@ -1381,10 +1379,8 @@ pub fn sync_tether_config(sync_path: &Path, home: &Path) -> Result<Option<Config
let local_config_path = home.join(".tether/config.toml");
let local_content = std::fs::read(&local_config_path).ok();

let remote_hash = format!("{:x}", Sha256::digest(&plaintext));
let local_hash = local_content
.as_ref()
.map(|c| format!("{:x}", Sha256::digest(c)));
let remote_hash = crate::sha256_hex(&plaintext);
let local_hash = local_content.as_ref().map(|c| crate::sha256_hex(c));

// Check if local has changed since last sync
let state = SyncState::load().ok();
Expand Down Expand Up @@ -1432,7 +1428,7 @@ pub fn export_tether_config(sync_path: &Path, home: &Path, state: &mut SyncState
}

let content = std::fs::read(&config_path)?;
let hash = format!("{:x}", Sha256::digest(&content));
let hash = crate::sha256_hex(&content);

let dest_dir = sync_path.join("configs/tether");
std::fs::create_dir_all(&dest_dir)?;
Expand All @@ -1444,7 +1440,7 @@ pub fn export_tether_config(sync_path: &Path, home: &Path, state: &mut SyncState
let key = crate::security::get_encryption_key().ok()?;
crate::security::decrypt(&enc, &key)
.ok()
.map(|plain| format!("{:x}", Sha256::digest(&plain)))
.map(|plain| crate::sha256_hex(&plain))
});

if file_hash.as_ref() != Some(&hash) {
Expand Down Expand Up @@ -1490,7 +1486,7 @@ pub fn sync_directories(

if expanded_path.is_file() {
if let Ok(content) = std::fs::read(&expanded_path) {
let hash = format!("{:x}", Sha256::digest(&content));
let hash = crate::sha256_hex(&content);
let file_changed = state
.files
.get(dir_path)
Expand Down Expand Up @@ -1534,7 +1530,7 @@ pub fn sync_directories(
let state_key = format!("~/{}", rel_to_home.display());

if let Ok(content) = std::fs::read(file_path) {
let hash = format!("{:x}", Sha256::digest(&content));
let hash = crate::sha256_hex(&content);
let file_changed = state
.files
.get(&state_key)
Expand Down Expand Up @@ -1675,7 +1671,7 @@ pub fn sync_project_configs(
}

if let Ok(content) = std::fs::read(file_path) {
let hash = format!("{:x}", Sha256::digest(&content));
let hash = crate::sha256_hex(&content);

let rel_to_repo = file_path
.strip_prefix(&repo_path)
Expand Down Expand Up @@ -2046,14 +2042,14 @@ pub fn sync_team_project_secrets(
format!("team-secret:{}/{}", normalized_url, rel_file_no_age);
let last_synced_hash =
state.files.get(&state_key).map(|f| f.hash.as_str());
let remote_hash = format!("{:x}", Sha256::digest(&decrypted));
let remote_hash = crate::sha256_hex(&decrypted);

for local_project in checkouts {
let local_file = local_project.join(rel_file_no_age);

let should_write = if local_file.exists() {
let existing = std::fs::read(&local_file).unwrap_or_default();
let local_hash = format!("{:x}", Sha256::digest(&existing));
let local_hash = crate::sha256_hex(&existing);
if local_hash == remote_hash {
false // Already in sync
} else {
Expand Down
12 changes: 7 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use std::path::PathBuf;
/// packages (Vec<String>). Old ProfilePackagesConfig removed.
/// Migration: creates "dev" profile from global dotfiles/dirs/packages.
pub const CURRENT_CONFIG_VERSION: u32 = 2;
pub const DEFAULT_PROFILE: &str = "dev";

fn default_config_version() -> u32 {
1
Expand Down Expand Up @@ -694,7 +695,7 @@ impl Config {
self.machine_profiles
.get(machine_id)
.map(|s| s.as_str())
.unwrap_or("dev")
.unwrap_or(DEFAULT_PROFILE)
}

/// Get the profile assigned to a machine, if any
Expand Down Expand Up @@ -910,9 +911,10 @@ impl Config {
packages,
};

self.profiles.insert("dev".to_string(), dev_profile);
self.profiles
.insert(DEFAULT_PROFILE.to_string(), dev_profile);

// Assign all unassigned machines to "dev"
// Assign all unassigned machines to default profile
// (machines already in machine_profiles keep their existing assignment)
}

Expand Down Expand Up @@ -1533,7 +1535,7 @@ files = [".zshrc"]
#[test]
fn test_profile_name_defaults_to_dev() {
let config = Config::default();
assert_eq!(config.profile_name("any-machine"), "dev");
assert_eq!(config.profile_name("any-machine"), DEFAULT_PROFILE);
}

#[test]
Expand All @@ -1543,7 +1545,7 @@ files = [".zshrc"]
.machine_profiles
.insert("my-server".to_string(), "server".to_string());
assert_eq!(config.profile_name("my-server"), "server");
assert_eq!(config.profile_name("other"), "dev");
assert_eq!(config.profile_name("other"), DEFAULT_PROFILE);
}

#[test]
Expand Down
17 changes: 7 additions & 10 deletions src/daemon/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use crate::sync::{
};
use anyhow::Result;
use chrono::Local;
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, SystemTime};
Expand Down Expand Up @@ -228,11 +227,12 @@ impl DaemonServer {
// Load state and machine state
let mut state = SyncState::load()?;

// Auto-assign machine to "dev" profile on first run after v2 migration
// Auto-assign machine to default profile on first run after v2 migration
if !config.profiles.is_empty() && !config.machine_profiles.contains_key(&state.machine_id) {
config
.machine_profiles
.insert(state.machine_id.clone(), "dev".to_string());
config.machine_profiles.insert(
state.machine_id.clone(),
crate::config::DEFAULT_PROFILE.to_string(),
);
let _ = config.save();
}

Expand Down Expand Up @@ -279,7 +279,7 @@ impl DaemonServer {
let source = home.join(&file);
if source.exists() {
if let Ok(content) = std::fs::read(&source) {
let hash = format!("{:x}", Sha256::digest(&content));
let hash = crate::sha256_hex(&content);
let file_changed = state
.files
.get(&file)
Expand Down Expand Up @@ -405,10 +405,7 @@ impl DaemonServer {
state.deferred_casks.sort();

// Only notify if list changed (avoid repeated notifications)
let hash = format!(
"{:x}",
Sha256::digest(state.deferred_casks.join(",").as_bytes())
);
let hash = crate::sha256_hex(state.deferred_casks.join(",").as_bytes());
if state.deferred_casks_hash.as_ref() != Some(&hash) {
notify_deferred_casks(&state.deferred_casks).ok();
state.deferred_casks_hash = Some(hash);
Expand Down
33 changes: 33 additions & 0 deletions src/dashboard/config_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,39 @@ pub fn toggle_profile_dotfile_shared(config: &mut Config, machine_id: &str, path
config.save().is_ok()
}

/// Add a dotfile to the machine's profile. Returns false on duplicate or save failure.
pub fn add_profile_dotfile(config: &mut Config, machine_id: &str, path: &str) -> bool {
use crate::config::ProfileDotfileEntry;

let profile_name = config.profile_name(machine_id).to_string();
let profile = match config.profiles.get_mut(&profile_name) {
Some(p) => p,
None => return false,
};
if profile.dotfiles.iter().any(|e| e.path() == path) {
return false;
}
profile
.dotfiles
.push(ProfileDotfileEntry::Simple(path.to_string()));
config.save().is_ok()
}

/// Remove a dotfile from the machine's profile by path. Returns false if not found or save failure.
pub fn remove_profile_dotfile(config: &mut Config, machine_id: &str, path: &str) -> bool {
let profile_name = config.profile_name(machine_id).to_string();
let profile = match config.profiles.get_mut(&profile_name) {
Some(p) => p,
None => return false,
};
let before = profile.dotfiles.len();
profile.dotfiles.retain(|e| e.path() != path);
if profile.dotfiles.len() == before {
return false;
}
config.save().is_ok()
}

/// Validate interval format: number followed by s/m/h (e.g. "5m", "30s", "1h")
fn is_valid_interval(val: &str) -> bool {
if val.len() < 2 {
Expand Down
Loading
Loading