Skip to content

Commit 03b3755

Browse files
authored
Merge pull request #37 from uroybd/autoclean
feat: Autoclean
2 parents 9ef0f1e + 6e0879d commit 03b3755

17 files changed

+615
-67
lines changed

README.md

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ For detailed documentation, guides, and examples, visit the [DotR Wiki](https://
8484
- Profile-based deployments for different machines/environments
8585
- Directory structure preservation
8686

87+
### 🧹 Clean Mode
88+
- **Clean by default** - automatically removes files in destination that don't exist in source
89+
- Keeps your deployed configurations synchronized with your repository
90+
- **Package-level control** - disable cleaning per package with `clean = false` in config
91+
- **CLI override** - use `--clean=false` to skip cleaning for a single operation
92+
- Perfect for maintaining tidy config directories without manual cleanup
93+
- Respects ignore patterns - won't remove files matching your ignore rules
94+
8795
## Quick Start
8896

8997
1. **Initialize** a dotfiles repository:
@@ -315,14 +323,49 @@ dotr update --dry-run
315323

316324
# Combine with other options
317325
dotr deploy --dry-run --profile work --packages nvim,bashrc
318-
dotr update --dry-run --clean
326+
327+
# Preview deploy without cleaning extra files
328+
dotr deploy --dry-run --clean=false
319329
```
320330

321331
Dry run mode shows what would happen during deploy or update operations without:
322332
- Creating or modifying any files
323333
- Creating backups
324334
- Executing pre/post actions
325-
- Removing files (when using --clean)
335+
- Removing extra files (clean operations run by default)
336+
337+
## Clean Mode
338+
339+
By default, DotR cleans up destination directories by removing files that don't exist in your dotfiles repository. This keeps your configurations synchronized.
340+
341+
```bash
342+
# Deploy with cleaning (default behavior)
343+
dotr deploy
344+
345+
# Deploy without cleaning extra files
346+
dotr deploy --clean=false
347+
348+
# Update with cleaning (default)
349+
dotr update
350+
351+
# Update without cleaning
352+
dotr update --clean=false
353+
```
354+
355+
**Package-level configuration:**
356+
```toml
357+
[packages.nvim]
358+
src = "dotfiles/nvim"
359+
dest = "~/.config/nvim/"
360+
clean = false # Disable cleaning for this package
361+
```
362+
363+
**How it works:**
364+
- Files in destination that aren't in source are removed during deploy/update
365+
- Backup files (`.dotrbak` extension) are never removed
366+
- Files matching ignore patterns are preserved
367+
- CLI `--clean` flag overrides package-level settings
368+
- Use `--clean=false` to keep extra files in destination
326369

327370
## Installation
328371

src/cli/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ pub struct UpdateArgs {
101101
#[arg(long, default_value_t = false)]
102102
pub ignore_errors: bool,
103103

104-
#[arg(long, default_value_t = false)]
105-
pub clean: bool,
104+
#[arg(long)]
105+
pub clean: Option<bool>,
106106

107107
#[arg(long, default_value_t = false)]
108108
pub dry_run: bool,
@@ -120,8 +120,8 @@ pub struct DeployArgs {
120120
#[arg(long, default_value_t = false)]
121121
pub ignore_errors: bool,
122122

123-
#[arg(long, default_value_t = false)]
124-
pub clean: bool,
123+
#[arg(long)]
124+
pub clean: Option<bool>,
125125

126126
#[arg(long, default_value_t = false)]
127127
pub dry_run: bool,

src/config/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ impl Config {
145145
packages: None,
146146
profile: Some(profile_name.clone()),
147147
ignore_errors: false,
148-
clean: false,
148+
clean: Some(false),
149149
dry_run: false,
150150
};
151151
package.backup(ctx, &backup_args)?;
@@ -167,7 +167,7 @@ impl Config {
167167
packages: Some(vec![pkg_name.clone()]),
168168
profile: Some(profile_name),
169169
ignore_errors: false,
170-
clean: false,
170+
clean: Some(false),
171171
dry_run: false,
172172
},
173173
)?;

src/package/mod.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ pub struct Package {
4343
pub ignore: Vec<String>, // Patterns to ignore during deployment
4444
// Symlinks defaults to false
4545
pub symlink: bool,
46+
#[serde(default = "default_clean")]
47+
pub clean: bool,
48+
}
49+
50+
fn default_clean() -> bool {
51+
true
4652
}
4753

4854
impl Default for Package {
@@ -60,6 +66,7 @@ impl Default for Package {
6066
prompts: HashMap::new(),
6167
ignore: Vec::new(),
6268
symlink: false,
69+
clean: true,
6370
}
6471
}
6572
}
@@ -86,6 +93,7 @@ impl Package {
8693
prompts: HashMap::new(),
8794
ignore: Vec::new(),
8895
symlink: false,
96+
clean: true,
8997
}
9098
}
9199

@@ -147,6 +155,10 @@ impl Package {
147155
.get("symlink")
148156
.and_then(|v| v.as_bool())
149157
.unwrap_or(false);
158+
let clean = pkg_val
159+
.get("clean")
160+
.and_then(|v| v.as_bool())
161+
.unwrap_or(true);
150162

151163
Ok(Self {
152164
name: pkg_name.to_string(),
@@ -161,6 +173,7 @@ impl Package {
161173
prompts,
162174
ignore,
163175
symlink,
176+
clean,
164177
})
165178
}
166179

@@ -210,6 +223,9 @@ impl Package {
210223
if self.symlink {
211224
pkg_table.insert("symlink".to_string(), toml::Value::Boolean(true));
212225
}
226+
if !self.clean {
227+
pkg_table.insert("clean".to_string(), toml::Value::Boolean(false));
228+
}
213229
pkg_table
214230
}
215231

@@ -318,7 +334,7 @@ impl Package {
318334
all_copied_paths.push(dest_path);
319335
}
320336
}
321-
if args.clean {
337+
if self.should_clean(args.clean) {
322338
// Remove any files in copy_to that were not copied in this operation
323339
clean(&copy_to, &all_copied_paths, &self.ignore, args.dry_run)?;
324340
}
@@ -328,6 +344,13 @@ impl Package {
328344
Ok(BackupDeployResult::Success)
329345
}
330346

347+
pub fn should_clean(&self, arg_val: Option<bool>) -> bool {
348+
match arg_val {
349+
Some(v) => v,
350+
None => self.clean,
351+
}
352+
}
353+
331354
pub fn resolve_dest(&self, ctx: &Context) -> anyhow::Result<PathBuf> {
332355
let mut dest = match self.targets.get(ctx.profile.name.as_str()) {
333356
Some(d) => d.clone(),
@@ -515,7 +538,7 @@ impl Package {
515538
}
516539
all_deployed_paths.push(dest_path);
517540
}
518-
if args.clean {
541+
if self.should_clean(args.clean) {
519542
// Remove any files in copy_to that were not deployed in this operation
520543
clean(&copy_to, &all_deployed_paths, &self.ignore, args.dry_run)?;
521544
}
@@ -699,6 +722,11 @@ pub fn clean(
699722
ignore: &[String],
700723
dry_run: bool,
701724
) -> anyhow::Result<()> {
725+
// If the operation path doesn't exist, there's nothing to clean
726+
if !operation_path.exists() {
727+
return Ok(());
728+
}
729+
702730
let mut dirs = vec![];
703731
for entry in walkdir::WalkDir::new(operation_path) {
704732
let entry = entry?;

tests/actions_tests.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ impl TestFixture {
4646
packages,
4747
profile: None,
4848
ignore_errors: false,
49-
clean: false,
49+
clean: Some(false),
5050
dry_run: false,
5151
}))),
5252
)
@@ -587,7 +587,7 @@ fn test_pre_action_failure() {
587587
packages: Some(vec!["f_pre_fail".to_string()]),
588588
profile: None,
589589
ignore_errors: false,
590-
clean: false,
590+
clean: Some(false),
591591
dry_run: false,
592592
}))),
593593
);
@@ -641,7 +641,7 @@ fn test_post_action_failure() {
641641
packages: Some(vec!["f_post_fail".to_string()]),
642642
profile: None,
643643
ignore_errors: false,
644-
clean: false,
644+
clean: Some(false),
645645
dry_run: false,
646646
}))),
647647
);
@@ -677,7 +677,7 @@ fn test_action_with_nonexistent_command() {
677677
packages: Some(vec!["f_bad_cmd".to_string()]),
678678
profile: None,
679679
ignore_errors: false,
680-
clean: false,
680+
clean: Some(false),
681681
dry_run: false,
682682
}))),
683683
);
@@ -716,7 +716,7 @@ fn test_action_failure_with_error_message() {
716716
packages: Some(vec!["f_err_msg".to_string()]),
717717
profile: None,
718718
ignore_errors: false,
719-
clean: false,
719+
clean: Some(false),
720720
dry_run: false,
721721
}))),
722722
);

0 commit comments

Comments
 (0)