OXYGENHUB
# 79ab8528ff62 oxygen / src / cli / workspace.rs
8.3 KiB · Rust · last changed Garbage collection: operation-log retention, pruning with grace period, pack rewriting; oxy gc --now 1 month ago source blame history raw
fce44ae9 Andy McCall ↶ 1
//! Commands that act on the local repository and working copy.
2
3
use super::format::{deps_line, plural, short_id, signature_line, when};
4
use super::open;
5
use anyhow::Result;
6
use oxy_core::repo::{render_change, ChangeStatus, Repo};
7
use std::path::PathBuf;
8
9
pub fn init(path: Option<PathBuf>) -> Result<i32> {
10
    let path = path.unwrap_or(std::env::current_dir()?);
11
    let repo = Repo::init(&path)?;
12
    println!("Initialized oxy repository in {}", repo.root().display());
13
    println!("Bookmark: {}   Author: {} <{}>", repo.config.bookmark, repo.config.author_name, repo.config.author_email);
14
    Ok(0)
15
}
16
17
fn status_tag(s: ChangeStatus) -> &'static str {
18
    match s {
19
        ChangeStatus::Added => "A",
20
        ChangeStatus::Modified => "M",
21
        ChangeStatus::Removed => "D",
22
        ChangeStatus::ModeChanged => "X",
23
        ChangeStatus::ConflictEdited => "C!",
24
        ChangeStatus::Resolved => "R",
25
    }
26
}
27
28
pub fn status() -> Result<i32> {
29
    let repo = open()?;
30
    let snap = repo.snapshot("oxy status")?;
31
    let heads = repo.bookmark_heads()?;
32
    println!("Working copy: {} on {} ({})", snap.wc.change_id.short(8), repo.config.bookmark, plural(heads.len(), "head"));
33
    if snap.statuses.is_empty() {
34
        println!("The working copy is clean.");
35
    }
36
    for s in &snap.statuses {
37
        println!("{:2} {}", status_tag(s.status), s.path);
38
    }
39
    if !snap.base_snapshot.is_conflicted() {
40
        return Ok(0);
41
    }
42
    println!("\nConflicts in the base state (edit the marked files, or the .oxy-conflict-N candidates, then commit):");
43
    for c in &snap.base_snapshot.tree.conflicts {
44
        println!("  tree: {c:?}");
45
    }
46
    for (p, f) in &snap.base_snapshot.files {
47
        if f.is_conflicted() {
48
            let state = match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) {
49
                Some(ChangeStatus::Resolved) => "resolved in working copy",
50
                Some(ChangeStatus::ConflictEdited) => "markers remain",
51
                _ => "unresolved",
52
            };
53
            println!("  {p}  ({state})");
54
        }
55
    }
56
    Ok(2)
57
}
58
59
pub fn commit(message: &str, depends_on: &[String]) -> Result<i32> {
60
    let repo = open()?;
61
    let mut deps = Vec::new();
62
    for d in depends_on {
63
        deps.push(repo.resolve_change(d)?);
64
    }
65
    let h = repo.commit(message, &deps)?;
66
    let c = repo.store().get_change(&h)?;
67
    println!("Committed {} #{}: {}", short_id(&c), h.short(12), message.lines().next().unwrap_or(""));
68
    Ok(0)
69
}
70
71
pub fn log() -> Result<i32> {
72
    let repo = open()?;
73
    repo.snapshot("oxy log")?;
74
    for e in repo.log()? {
75
        let c = &e.change;
76
        let mut flags = Vec::new();
77
        if e.is_working_copy {
78
            flags.push("working copy");
79
        }
80
        if e.is_head {
81
            flags.push("head");
82
        }
83
        let flags = if flags.is_empty() { String::new() } else { format!(" ({})", flags.join(", ")) };
84
        println!("{} #{}{}", short_id(c), e.hash.short(12), flags);
85
        println!("    {}", when(c.hlc.wall_ms));
86
        if !c.deps.is_empty() {
87
            println!("    deps: {}", deps_line(c));
88
        }
89
        let msg = if c.message.is_empty() { "(no description)" } else { &c.message };
90
        for line in msg.lines() {
91
            println!("    {line}");
92
        }
93
        println!();
94
    }
95
    Ok(0)
96
}
97
98
pub fn diff() -> Result<i32> {
99
    let repo = open()?;
100
    let snap = repo.snapshot("oxy diff")?;
101
    print!("{}", render_change(&snap.wc, &snap.base, &snap.base_snapshot));
102
    Ok(0)
103
}
104
105
pub fn show(change: &str) -> Result<i32> {
106
    let repo = open()?;
107
    repo.snapshot("oxy show")?;
108
    let h = repo.resolve_change(change)?;
109
    let c = repo.store().get_change(&h)?;
110
    println!("change {} #{}", short_id(&c), h);
111
    println!("kind:   {:?}", c.kind);
112
    println!("date:   {}", when(c.hlc.wall_ms));
113
    println!("deps:   {}", deps_line(&c));
114
    println!("sig:    {}", signature_line(&repo, &h)?);
115
    println!("\n{}\n", if c.message.is_empty() { "(no description)" } else { &c.message });
116
    print!("{}", repo.describe_change(&h)?);
117
    Ok(0)
118
}
119
120
pub fn checkout(changes: &[String], discard: bool) -> Result<i32> {
121
    let repo = open()?;
122
    let heads = if changes.is_empty() {
123
        repo.bookmark_heads()?
124
    } else {
125
        changes.iter().map(|c| repo.resolve_change(c)).collect::<Result<Vec<_>, _>>()?
126
    };
127
    let spec = if changes.is_empty() { repo.config.bookmark.clone() } else { changes.join(" ") };
128
    let r = repo.checkout(&heads, discard, &format!("oxy checkout {spec}"))?;
129
    println!("Working copy now on {spec}: {} written, {} removed", plural(r.written, "file"), r.removed);
130
    if r.conflicted {
131
        println!("This state has conflicts; see oxy status.");
132
    }
133
    Ok(0)
134
}
135
136
pub fn resolve() -> Result<i32> {
137
    let repo = open()?;
138
    let snap = repo.snapshot("oxy resolve")?;
139
    if !snap.base_snapshot.is_conflicted() {
140
        println!("The working copy's base has no conflicts.");
141
        return Ok(0);
142
    }
143
    let mut remaining = 0;
144
    for (p, f) in &snap.base_snapshot.files {
145
        if !f.is_conflicted() {
146
            continue;
147
        }
148
        match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) {
149
            Some(ChangeStatus::Resolved) => println!("resolved   {p}"),
150
            Some(ChangeStatus::ConflictEdited) => {
151
                remaining += 1;
152
                println!("markers    {p}");
153
            }
154
            _ => {
155
                remaining += 1;
156
                println!("unresolved {p}");
157
            }
158
        }
159
    }
160
    for c in &snap.base_snapshot.tree.conflicts {
161
        remaining += 1;
162
        println!("tree       {c:?}");
163
    }
164
    if remaining == 0 {
165
        println!("All conflicts are resolved; `oxy commit` will record the resolution.");
166
        Ok(0)
167
    } else {
168
        Ok(2)
169
    }
170
}
171
172
pub fn undo() -> Result<i32> {
173
    let repo = open()?;
174
    let op = repo.undo()?;
175
    println!("Undid the last operation; now at: {}", op.command);
176
    Ok(0)
177
}
178
179
pub fn op_log() -> Result<i32> {
180
    let repo = open()?;
181
    for (i, (r, op)) in repo.op_log()?.into_iter().enumerate() {
182
        println!("{}{} {}  {}", if i == 0 { "@ " } else { "  " }, r.short(12), when(op.hlc.wall_ms), op.command);
183
    }
184
    Ok(0)
185
}
d02e545d Andy McCall ↶ 186
ba160f60 Andy McCall ↶ 187
pub fn gc(now: bool) -> Result<i32> {
d02e545d Andy McCall ↶ 188
    let repo = open()?;
ba160f60 Andy McCall ↶ 189
    let r = repo.gc(now)?;
190
    if r.packed.objects > 0 {
d02e545d Andy McCall ↶ 191
        println!(
ba160f60 Andy McCall ↶ 192
            "Packed {}: {} bytes -> {} bytes ({:.0}%).",
193
            plural(r.packed.objects, "loose object"),
194
            r.packed.raw_bytes,
195
            r.packed.packed_bytes,
196
            100.0 * r.packed.packed_bytes as f64 / r.packed.raw_bytes.max(1) as f64
d02e545d Andy McCall ↶ 197
        );
198
    }
ba160f60 Andy McCall ↶ 199
    println!("{} reachable; pruned {} ({} bytes){}; {} present.",
200
        plural(r.reachable, "object"),
201
        plural(r.pruned.objects, "unreachable object"),
202
        r.pruned.bytes,
203
        if r.pruned.deferred > 0 { format!("; {} deferred by the grace period (use --now)", plural(r.pruned.deferred, "object")) } else { String::new() },
204
        plural(repo.store().pack_count(), "pack")
205
    );
d02e545d Andy McCall ↶ 206
    Ok(0)
207
}
614541c3 Andy McCall ↶ 208
209
/// The single head the working copy sits on, for commands that default to it.
210
fn current_head(repo: &Repo) -> Result<oxy_core::hash::ChangeHash> {
211
    let snap = repo.snapshot("oxy")?;
212
    let base = snap.base.maximal(&snap.wc.deps);
213
    match base.as_slice() {
214
        [h] => Ok(*h),
215
        _ => anyhow::bail!("the working copy sits on {} heads; name the change explicitly", base.len()),
216
    }
217
}
218
219
pub fn describe(message: &str, change: Option<&str>) -> Result<i32> {
220
    let repo = open()?;
221
    let h = match change {
222
        Some(c) => repo.resolve_change(c)?,
223
        None => current_head(&repo)?,
224
    };
225
    let new = repo.describe(&h, message)?;
226
    let c = repo.store().get_change(&new)?;
227
    println!("Described {} #{}: {}", short_id(&c), new.short(12), message.lines().next().unwrap_or(""));
228
    Ok(0)
229
}
230
231
pub fn amend(message: Option<&str>) -> Result<i32> {
232
    let repo = open()?;
233
    let new = repo.amend(message)?;
234
    let c = repo.store().get_change(&new)?;
235
    println!("Amended {} #{}: {}", short_id(&c), new.short(12), c.message.lines().next().unwrap_or(""));
236
    Ok(0)
237
}
238
239
pub fn abandon(change: &str) -> Result<i32> {
240
    let repo = open()?;
241
    let h = repo.resolve_change(change)?;
242
    let c = repo.store().get_change(&h)?;
243
    repo.abandon(&h)?;
244
    println!("Abandoned {} #{}; its content remains on disk as uncommitted work if the working copy sat on it.", short_id(&c), h.short(12));
245
    Ok(0)
246
}