OXYGENHUB
# f9c98deef326 oxygen / src / main.rs
8.2 KiB · Rust · last changed Import the Oxygen specification and the first Rust implementation (root change) 1 month ago source blame history raw
ff0af1ee unknown 1
//! `oxy`: command-line interface for Oxygen.
2
3
use anyhow::{bail, Context, Result};
4
use clap::{Parser, Subcommand};
5
use oxy_core::repo::{ChangeStatus, Repo};
6
use std::path::PathBuf;
7
8
#[derive(Parser)]
9
#[command(name = "oxy", version, about = "Oxygen: breathing fresh air into version control")]
10
struct Cli {
11
    #[command(subcommand)]
12
    cmd: Cmd,
13
}
14
15
#[derive(Subcommand)]
16
enum Cmd {
17
    /// Create a repository in PATH (default: current directory)
18
    Init { path: Option<PathBuf> },
19
    /// Show the working copy: changed paths and conflicts
20
    Status,
21
    /// Record the working copy as a change on the current bookmark
22
    Commit {
23
        #[arg(short, long)]
24
        message: String,
25
        /// Extra explicit dependencies (ChangeId prefix, #hash prefix)
26
        #[arg(long = "depends-on")]
27
        depends_on: Vec<String>,
28
    },
29
    /// List changes reachable from the bookmark and the working copy
30
    Log,
31
    /// Show what the working copy changes against its base
32
    Diff,
33
    /// Show one change (ChangeId prefix, #hash prefix, or @)
34
    Show { change: String },
f9c98dee unknown ↶ 35
    /// Move the working copy to the state formed by CHANGE... (default: the bookmark heads)
36
    Checkout {
37
        changes: Vec<String>,
38
        /// Throw away uncommitted working-copy changes
39
        #[arg(long)]
40
        discard: bool,
41
    },
ff0af1ee unknown 42
    /// Undo the last operation
43
    Undo,
44
    /// List operations
45
    #[command(name = "op")]
46
    Op {
47
        #[command(subcommand)]
48
        cmd: OpCmd,
49
    },
50
}
51
52
#[derive(Subcommand)]
53
enum OpCmd {
54
    Log,
55
}
56
57
fn main() {
58
    match run() {
59
        Ok(code) => std::process::exit(code),
60
        Err(e) => {
61
            eprintln!("error: {e:#}");
62
            std::process::exit(1);
63
        }
64
    }
65
}
66
67
fn open() -> Result<Repo> {
68
    Repo::open(&std::env::current_dir()?).context("opening repository")
69
}
70
71
fn short_id(c: &oxy_core::objects::Change) -> String {
72
    c.change_id.short(8)
73
}
74
75
fn when(ms: u64) -> String {
76
    let secs = (ms / 1000) as i64;
77
    let days = secs.div_euclid(86_400);
78
    let rem = secs.rem_euclid(86_400);
79
    // Civil-from-days (Howard Hinnant), UTC.
80
    let z = days + 719_468;
81
    let era = z.div_euclid(146_097);
82
    let doe = z - era * 146_097;
83
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
84
    let y = yoe + era * 400;
85
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
86
    let mp = (5 * doy + 2) / 153;
87
    let d = doy - (153 * mp + 2) / 5 + 1;
88
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
89
    let y = if m <= 2 { y + 1 } else { y };
90
    format!("{y:04}-{m:02}-{d:02} {:02}:{:02}:{:02}Z", rem / 3600, (rem % 3600) / 60, rem % 60)
91
}
92
93
fn run() -> Result<i32> {
94
    let cli = Cli::parse();
95
    match cli.cmd {
96
        Cmd::Init { path } => {
97
            let path = path.unwrap_or(std::env::current_dir()?);
98
            let repo = Repo::init(&path)?;
99
            println!("Initialized oxy repository in {}", repo.root().display());
100
            println!("Bookmark: {}   Author: {} <{}>", repo.config.bookmark, repo.config.author_name, repo.config.author_email);
101
            Ok(0)
102
        }
103
        Cmd::Status => {
104
            let repo = open()?;
105
            let snap = repo.snapshot("oxy status")?;
106
            let heads = repo.bookmark_heads()?;
107
            println!(
108
                "Working copy: {} on {} ({} head{})",
109
                snap.wc.change_id.short(8),
110
                repo.config.bookmark,
111
                heads.len(),
112
                if heads.len() == 1 { "" } else { "s" }
113
            );
114
            if snap.statuses.is_empty() {
115
                println!("The working copy is clean.");
116
            }
117
            for s in &snap.statuses {
118
                let tag = match s.status {
119
                    ChangeStatus::Added => "A",
120
                    ChangeStatus::Modified => "M",
121
                    ChangeStatus::Removed => "D",
122
                    ChangeStatus::ModeChanged => "X",
123
                    ChangeStatus::ConflictEdited => "C!",
124
                };
125
                println!("{tag:2} {}", s.path);
126
            }
127
            let mut code = 0;
128
            if snap.base_snapshot.is_conflicted() {
129
                println!("\nConflicts in the base state:");
130
                for c in &snap.base_snapshot.tree.conflicts {
131
                    println!("  tree: {c:?}");
132
                }
133
                for (p, f) in &snap.base_snapshot.files {
134
                    if f.is_conflicted() {
135
                        println!("  {p}");
136
                    }
137
                }
138
                code = 2;
139
            }
140
            Ok(code)
141
        }
142
        Cmd::Commit { message, depends_on } => {
143
            let repo = open()?;
144
            let mut deps = Vec::new();
145
            for d in &depends_on {
146
                deps.push(repo.resolve_change(d)?);
147
            }
148
            let h = repo.commit(&message, &deps)?;
149
            let c = repo.store().get_change(&h)?;
150
            println!("Committed {} #{}: {}", short_id(&c), h.short(12), message.lines().next().unwrap_or(""));
151
            Ok(0)
152
        }
153
        Cmd::Log => {
154
            let repo = open()?;
155
            repo.snapshot("oxy log")?;
156
            for e in repo.log()? {
157
                let c = &e.change;
158
                let mut flags = Vec::new();
159
                if e.is_working_copy {
160
                    flags.push("working copy");
161
                }
162
                if e.is_head {
163
                    flags.push("head");
164
                }
165
                let flags = if flags.is_empty() { String::new() } else { format!(" ({})", flags.join(", ")) };
166
                println!("{} #{}{}", short_id(c), e.hash.short(12), flags);
167
                println!("    {}", when(c.hlc.wall_ms));
168
                if !c.deps.is_empty() {
169
                    println!("    deps: {}", c.deps.iter().map(|d| format!("#{}", d.short(12))).collect::<Vec<_>>().join(" "));
170
                }
171
                let msg = if c.message.is_empty() { "(no description)" } else { &c.message };
172
                for line in msg.lines() {
173
                    println!("    {line}");
174
                }
175
                println!();
176
            }
177
            Ok(0)
178
        }
179
        Cmd::Diff => {
180
            let repo = open()?;
181
            let snap = repo.snapshot("oxy diff")?;
182
            print!("{}", oxy_core::repo::render_change(&snap.wc, &snap.base, &snap.base_snapshot));
183
            Ok(0)
184
        }
185
        Cmd::Show { change } => {
186
            let repo = open()?;
187
            repo.snapshot("oxy show")?;
188
            let h = repo.resolve_change(&change)?;
189
            let c = repo.store().get_change(&h)?;
190
            println!("change {} #{}", short_id(&c), h);
191
            println!("kind:   {:?}", c.kind);
192
            println!("date:   {}", when(c.hlc.wall_ms));
193
            println!("deps:   {}", c.deps.iter().map(|d| format!("#{}", d.short(12))).collect::<Vec<_>>().join(" "));
194
            println!("\n{}\n", if c.message.is_empty() { "(no description)" } else { &c.message });
195
            print!("{}", repo.describe_change(&h)?);
196
            Ok(0)
197
        }
f9c98dee unknown ↶ 198
        Cmd::Checkout { changes, discard } => {
199
            let repo = open()?;
200
            let heads = if changes.is_empty() {
201
                repo.bookmark_heads()?
202
            } else {
203
                changes.iter().map(|c| repo.resolve_change(c)).collect::<Result<Vec<_>, _>>()?
204
            };
205
            let spec = if changes.is_empty() { repo.config.bookmark.clone() } else { changes.join(" ") };
206
            let r = repo.checkout(&heads, discard, &format!("oxy checkout {spec}"))?;
207
            println!("Working copy now on {spec}: {} file(s) written, {} removed", r.written, r.removed);
208
            if r.conflicted {
209
                println!("This state has conflicts; see oxy status.");
210
            }
211
            Ok(0)
212
        }
ff0af1ee unknown 213
        Cmd::Undo => {
214
            let repo = open()?;
215
            let op = repo.undo()?;
216
            println!("Undid the last operation; now at: {}", op.command);
217
            Ok(0)
218
        }
219
        Cmd::Op { cmd: OpCmd::Log } => {
220
            let repo = open()?;
221
            for (i, (r, op)) in repo.op_log()?.into_iter().enumerate() {
222
                println!("{}{} {}  {}", if i == 0 { "@ " } else { "  " }, r.short(12), when(op.hlc.wall_ms), op.command);
223
            }
224
            Ok(0)
225
        }
226
    }
227
    .map_err(|e: anyhow::Error| e)
228
    .or_else(|e| {
229
        if e.to_string().is_empty() {
230
            bail!("unknown error")
231
        } else {
232
            Err(e)
233
        }
234
    })
235
}