Skip to main content

wip_git/commands/
save.rs

1use crate::git;
2use crate::metadata::WipMetadata;
3use crate::ref_name;
4
5pub struct SaveResult {
6    pub name: String,
7    pub wip_ref: String,
8    pub sha: String,
9    pub files: usize,
10    pub untracked: usize,
11    pub task: Option<String>,
12    pub clean: bool,
13    pub stashed: bool,
14}
15
16/// Find the next auto-increment suffix for a base name.
17/// Scans `refs/wip/<user>/<base>-*` on the remote, parses `-NN` suffixes,
18/// and returns `<base>-{max+1:02}`.
19fn next_increment(base: &str, user: &str, remote: &str) -> Result<String, String> {
20    let pattern = ref_name::wip_ref(&format!("{base}-*"), user);
21    let ls = git::git_stdout(&["ls-remote", remote, &pattern])
22        .map_err(|e| format!("could not reach remote '{}': {}", remote, e))?;
23
24    let prefix = format!("refs/wip/{user}/{base}-");
25    let max_n = ls
26        .lines()
27        .filter_map(|line| {
28            let refname = line.split_whitespace().nth(1)?;
29            let suffix = refname.strip_prefix(&prefix)?;
30            suffix.parse::<u32>().ok()
31        })
32        .max()
33        .unwrap_or(0);
34
35    Ok(format!("{base}-{:02}", max_n + 1))
36}
37
38pub fn run(
39    name: Option<String>,
40    message: String,
41    task: Option<String>,
42    force: bool,
43    include_ignored: bool,
44    stash: bool,
45    remote: String,
46) -> Result<SaveResult, String> {
47    let user = ref_name::user()?;
48    let base = ref_name::resolve_name(name)?;
49    let (name, wip_ref) = if force {
50        // --force: exact name, overwrite (backward compat)
51        let wip_ref = ref_name::wip_ref(&base, &user);
52        (base, wip_ref)
53    } else {
54        // auto-increment
55        let name = next_increment(&base, &user, &remote)?;
56        let wip_ref = ref_name::wip_ref(&name, &user);
57        (name, wip_ref)
58    };
59
60    // Remember current state
61    let original_head = git::git_stdout(&["rev-parse", "HEAD"])?;
62    let branch = git::git_stdout(&["rev-parse", "--abbrev-ref", "HEAD"])?;
63
64    // Stage everything
65    if include_ignored {
66        git::git(&["add", "-A", "--force"])?;
67    } else {
68        git::git(&["add", "-A"])?;
69    }
70
71    // Count what we're saving
72    let status = git::git_stdout(&["status", "--porcelain"])?;
73    if status.is_empty() {
74        return Ok(SaveResult {
75            name,
76            wip_ref,
77            sha: String::new(),
78            files: 0,
79            untracked: 0,
80            task,
81            clean: true,
82            stashed: false,
83        });
84    }
85
86    let files = status.lines().count();
87    let untracked = status
88        .lines()
89        .filter(|l| l.starts_with("A ") || l.starts_with("??"))
90        .count();
91
92    // Build commit message with metadata
93    let meta = WipMetadata {
94        message: message.clone(),
95        branch: branch.clone(),
96        task: task.clone(),
97        files,
98        untracked,
99    };
100
101    // Create detached commit
102    let commit_msg = meta.to_commit_message();
103    git::git(&["commit", "--allow-empty", "-m", &commit_msg])?;
104    let wip_sha = git::git_stdout(&["rev-parse", "HEAD"])?;
105
106    // Push to hidden ref
107    let refspec = format!("{wip_sha}:{wip_ref}");
108    let push_result = git::git(&["push", &remote, &refspec, "--force"]);
109
110    // Reset back regardless of push result
111    if stash {
112        git::git(&["reset", &original_head, "--hard"])?;
113        git::git(&["clean", "-fd"])?;
114    } else {
115        git::git(&["reset", &original_head, "--mixed"])?;
116    }
117
118    // Now check if push succeeded
119    push_result?;
120
121    Ok(SaveResult {
122        name,
123        wip_ref,
124        sha: wip_sha,
125        files,
126        untracked,
127        task,
128        clean: false,
129        stashed: stash,
130    })
131}