Skip to main content

ktrs_cli/
migrate.rs

1//! `ktrs migrate`: the build-file rewrite planned by `ktrs_project::migrate`, shown as a diff or written.
2
3use std::io::Write;
4use std::path::{Path, PathBuf};
5
6use ktrs_project::migrate::{Migration, plan};
7
8pub const HELP: &str = "\
9Usage: ktrs migrate [--write] [PATH ...]
10
11Switches the build's ktfmt / ktlint setup to the ktrs drop-ins (default PATH: .): ktfmt-gradle and
12ktlint-gradle plugin ids (plugins {}, the version catalog, buildscript or convention-build
13dependencies) and the plugin repository, gantsign's ktlint-maven-plugin, and Spotless's ktfmt/ktlint
14steps (Gradle and Maven). Edits only the ids, coordinates and versions, plus the lines the README
15adds; setups with no drop-in (kotlinter, ktlint run from its jar) get a `note:` on stderr.
16
17Without --write, prints the edits as a unified diff and exits 1 if there are any (0 if none).
18
19Options:
20  --write    Apply the edits and list the files changed";
21
22pub fn run(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> i32 {
23    let mut write = false;
24    let mut paths = Vec::new();
25    for arg in args {
26        match arg.as_str() {
27            "--write" => write = true,
28            "-h" | "--help" => {
29                let _ = writeln!(out, "{HELP}");
30                return 0;
31            }
32            _ if arg.starts_with('-') => {
33                let _ = writeln!(err, "error: unknown option {arg}\n\n{HELP}");
34                return 2;
35            }
36            _ => paths.push(PathBuf::from(arg)),
37        }
38    }
39    if paths.is_empty() {
40        paths.push(PathBuf::from("."));
41    }
42    let mut migrations: Vec<Migration> = Vec::new();
43    for path in &paths {
44        let m = plan(path, env!("CARGO_PKG_VERSION"));
45        if !migrations.iter().any(|seen| seen.root == m.root) {
46            migrations.push(m);
47        }
48    }
49    let cwd = std::env::current_dir().unwrap_or_default();
50    let mut changed = 0;
51    for m in &migrations {
52        for note in &m.notes {
53            let _ = writeln!(err, "note: {note}");
54        }
55        for change in &m.changes {
56            let label = label(&cwd, &change.path);
57            if write {
58                if let Err(e) = change.write() {
59                    let _ = writeln!(err, "error: {label}: {e}");
60                    return 2;
61                }
62                let _ = writeln!(out, "migrated {label}");
63            } else {
64                let _ = write!(out, "{}", change.unified_diff(&label));
65            }
66            changed += 1;
67        }
68    }
69    let files = if changed == 1 { "1 file".to_string() } else { format!("{changed} files") };
70    match (changed, write) {
71        (0, _) => {
72            let _ = writeln!(out, "Nothing to migrate.");
73            0
74        }
75        (_, true) => {
76            let _ = writeln!(out, "{files} changed.");
77            0
78        }
79        (_, false) => {
80            let _ = writeln!(out, "{files} would change; run `ktrs migrate --write` to apply.");
81            1
82        }
83    }
84}
85
86/// `path` relative to `cwd` when under it, with `/` separators.
87fn label(cwd: &Path, path: &Path) -> String {
88    path.strip_prefix(cwd).unwrap_or(path).to_string_lossy().replace('\\', "/")
89}