Skip to main content

tryme_core/
scripts.rs

1//! Script builders — ports of `script_cd` / `script_mkdir_cd` /
2//! `script_clone` / `script_worktree` / `script_delete` / `script_ascend` /
3//! `script_rename` (`try.rb:1411-1467`). Every byte matters: the emitted
4//! strings are pinned by `test_05`/`07`/`12`/`16`/`31`/`37`.
5
6use crate::emit::q;
7use std::path::Path;
8
9fn p(path: &Path) -> String {
10    path.to_string_lossy().into_owned()
11}
12
13/// `script_cd` (`try.rb:1411-1413`). The leading `touch` bumps mtime, which
14/// feeds `base_score` recency — selecting a try boosts its future ranking.
15/// That feedback loop is a feature, not an accident.
16#[must_use]
17pub fn script_cd(path: &Path) -> Vec<String> {
18    let qp = q(&p(path));
19    vec![
20        format!("touch {qp}"),
21        format!("echo {qp}"),
22        format!("cd {qp}"),
23    ]
24}
25
26/// `script_mkdir_cd` (`try.rb:1415-1417`).
27#[must_use]
28pub fn script_mkdir_cd(path: &Path) -> Vec<String> {
29    let mut cmds = vec![format!("mkdir -p {}", q(&p(path)))];
30    cmds.extend(script_cd(path));
31    cmds
32}
33
34/// `script_clone` (`try.rb:1419-1421`). Upstream hardcodes single quotes
35/// around the URI (`git clone '#{uri}'`) instead of `q()` — preserved.
36#[must_use]
37pub fn script_clone(path: &Path, uri: &str) -> Vec<String> {
38    let qp = q(&p(path));
39    let mut cmds = vec![
40        format!("mkdir -p {qp}"),
41        format!(
42            "echo {}",
43            q(&format!("Using git clone to create this trial from {uri}."))
44        ),
45        format!("git clone '{uri}' {qp}"),
46    ];
47    cmds.extend(script_cd(path));
48    cmds
49}
50
51/// `script_worktree` (`try.rb:1423-1432`). The inner `sh -c` guard adds the
52/// worktree (detached) only when inside a work tree and **always exits 0**,
53/// so a non-git source still mkdir+cds. `repo = None` is the
54/// current-directory variant (no `-C`); `src` is what the echo names.
55#[must_use]
56pub fn script_worktree(path: &Path, repo: Option<&Path>, cwd: &Path) -> Vec<String> {
57    let qp = q(&p(path));
58    let worktree_cmd = match repo {
59        Some(r) => {
60            let qr = q(&p(r));
61            format!(
62                "/usr/bin/env sh -c 'if git -C {qr} rev-parse --is-inside-work-tree >/dev/null 2>&1; then repo=$(git -C {qr} rev-parse --show-toplevel); git -C \"$repo\" worktree add --detach {qp} >/dev/null 2>&1 || true; fi; exit 0'"
63            )
64        }
65        None => format!(
66            "/usr/bin/env sh -c 'if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then repo=$(git rev-parse --show-toplevel); git -C \"$repo\" worktree add --detach {qp} >/dev/null 2>&1 || true; fi; exit 0'"
67        ),
68    };
69    let src = repo.unwrap_or(cwd);
70    let mut cmds = vec![
71        format!("mkdir -p {qp}"),
72        format!(
73            "echo {}",
74            q(&format!(
75                "Using git worktree to create this trial from {}.",
76                p(src)
77            ))
78        ),
79        worktree_cmd,
80    ];
81    cmds.extend(script_cd(path));
82    cmds
83}
84
85/// `script_delete` (`try.rb:1434-1439`): cd into the base, `test -d` guard
86/// per basename, then restore the original PWD with a base-path fallback.
87#[must_use]
88pub fn script_delete(basenames: &[String], base_path: &Path, original_pwd: &Path) -> Vec<String> {
89    let mut cmds = vec![format!("cd {}", q(&p(base_path)))];
90    for name in basenames {
91        let qn = q(name);
92        cmds.push(format!("test -d {qn} && rm -rf {qn}"));
93    }
94    cmds.push(format!(
95        "cd {} 2>/dev/null || cd {}",
96        q(&p(original_pwd)),
97        q(&p(base_path))
98    ));
99    cmds
100}
101
102/// `script_ascend` (`try.rb:1441-1457`): `git worktree move` when the source
103/// has a `.git` **file** (worktree marker), plain `mv` otherwise; then a
104/// symlink back into the tries dir, the `Graduated:` echo (with a literal
105/// `→`), and a cd to the destination.
106#[must_use]
107pub fn script_ascend(source: &Path, dest: &Path, basename: &str, base_path: &Path) -> Vec<String> {
108    let symlink_path = base_path.join(basename);
109    let is_worktree = source.join(".git").is_file();
110
111    let mut cmds = Vec::new();
112    if is_worktree {
113        cmds.push(format!(
114            "git worktree move {} {}",
115            q(&p(source)),
116            q(&p(dest))
117        ));
118    } else {
119        cmds.push(format!("mv {} {}", q(&p(source)), q(&p(dest))));
120    }
121    cmds.push(format!("ln -s {} {}", q(&p(dest)), q(&p(&symlink_path))));
122    cmds.push(format!(
123        "echo {}",
124        q(&format!("Graduated: {basename} → {}", p(dest)))
125    ));
126    cmds.extend(script_cd(dest));
127    cmds
128}
129
130/// `script_rename` (`try.rb:1459-1467`).
131#[must_use]
132pub fn script_rename(base_path: &Path, old_name: &str, new_name: &str) -> Vec<String> {
133    let new_path = base_path.join(new_name);
134    vec![
135        format!("cd {}", q(&p(base_path))),
136        format!("mv {} {}", q(old_name), q(new_name)),
137        format!("echo {}", q(&p(&new_path))),
138        format!("cd {}", q(&p(&new_path))),
139    ]
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use std::path::PathBuf;
146
147    #[test]
148    fn cd_script_touch_echo_cd() {
149        assert_eq!(
150            script_cd(&PathBuf::from("/t/2026-07-10-x")),
151            vec![
152                "touch '/t/2026-07-10-x'",
153                "echo '/t/2026-07-10-x'",
154                "cd '/t/2026-07-10-x'"
155            ]
156        );
157    }
158
159    #[test]
160    fn clone_script_hardcoded_uri_quotes() {
161        let cmds = script_clone(&PathBuf::from("/t/d"), "https://github.com/u/r");
162        assert_eq!(cmds[2], "git clone 'https://github.com/u/r' '/t/d'");
163        assert_eq!(
164            cmds[1],
165            "echo 'Using git clone to create this trial from https://github.com/u/r.'"
166        );
167    }
168
169    #[test]
170    fn worktree_detach_and_always_exit_zero() {
171        let cmds = script_worktree(
172            &PathBuf::from("/t/w"),
173            Some(&PathBuf::from("/repo")),
174            &PathBuf::from("/cwd"),
175        );
176        assert!(cmds[2].contains("worktree add --detach '/t/w'"));
177        assert!(cmds[2].ends_with("fi; exit 0'"));
178        assert!(cmds[2].contains("git -C '/repo' rev-parse"));
179        // cwd variant drops -C
180        let cmds = script_worktree(&PathBuf::from("/t/w"), None, &PathBuf::from("/cwd"));
181        assert!(cmds[2].contains("if git rev-parse"));
182        assert!(cmds[1].contains("from /cwd."));
183    }
184
185    #[test]
186    fn delete_uses_basenames_and_pwd_fallback() {
187        let cmds = script_delete(
188            &["a".to_string(), "b".to_string()],
189            &PathBuf::from("/t"),
190            &PathBuf::from("/orig"),
191        );
192        assert_eq!(cmds[0], "cd '/t'");
193        assert_eq!(cmds[1], "test -d 'a' && rm -rf 'a'");
194        assert_eq!(cmds[3], "cd '/orig' 2>/dev/null || cd '/t'");
195    }
196
197    #[test]
198    fn ascend_mv_symlink_echo_cd() {
199        let tmp = tempfile::tempdir().unwrap();
200        let src = tmp.path().join("2026-07-10-exp");
201        std::fs::create_dir(&src).unwrap();
202        let cmds = script_ascend(
203            &src,
204            &PathBuf::from("/proj/exp"),
205            "2026-07-10-exp",
206            tmp.path(),
207        );
208        assert!(cmds[0].starts_with("mv "));
209        assert!(cmds[1].starts_with("ln -s '/proj/exp' "));
210        assert!(cmds[2].contains("Graduated: 2026-07-10-exp → /proj/exp"));
211        assert_eq!(cmds.len(), 6);
212    }
213
214    #[test]
215    fn rename_cd_mv_echo_cd() {
216        let cmds = script_rename(&PathBuf::from("/t"), "old", "new");
217        assert_eq!(
218            cmds,
219            vec!["cd '/t'", "mv 'old' 'new'", "echo '/t/new'", "cd '/t/new'"]
220        );
221    }
222}