stash
This commit is contained in:
parent
2bb2adeaa0
commit
731652d330
1 changed files with 327 additions and 116 deletions
455
src/main.rs
455
src/main.rs
|
|
@ -222,7 +222,7 @@ fn print_usage() {
|
||||||
BOLD, RESET
|
BOLD, RESET
|
||||||
);
|
);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" {}stash pop{} Restore files auto-stashed by clean",
|
" {}stash{} Snapshot .loop/ state (stash log, stash checkout <hash>)",
|
||||||
BOLD, RESET
|
BOLD, RESET
|
||||||
);
|
);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
|
@ -943,7 +943,7 @@ fn print_clean_help() {
|
||||||
eprintln!("Mode snapshots in .loop/.modes/ are preserved by clean.");
|
eprintln!("Mode snapshots in .loop/.modes/ are preserved by clean.");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("Non-empty working files are auto-stashed to .loop/.stash/ before wiping.");
|
eprintln!("Non-empty working files are auto-stashed to .loop/.stash/ before wiping.");
|
||||||
eprintln!("Recover with: {}yoke stash pop{}", BOLD, RESET);
|
eprintln!("Browse with: {}yoke stash log{} Recover with: {}yoke stash pop{}", BOLD, RESET, BOLD, RESET);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clean() -> i32 {
|
fn clean() -> i32 {
|
||||||
|
|
@ -988,145 +988,353 @@ fn clean() -> i32 {
|
||||||
log("Clean complete");
|
log("Clean complete");
|
||||||
if stashed > 0 {
|
if stashed > 0 {
|
||||||
log(&format!(
|
log(&format!(
|
||||||
"stashed {} file(s) — recover with: yoke stash pop",
|
"stashed {} file(s) — browse with: yoke stash log",
|
||||||
stashed
|
stashed
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Files that are candidates for stashing during `yoke clean`.
|
// ── Stash helpers ──────────────────────────────────────────────────────
|
||||||
const STASH_CANDIDATES: &[&str] = &[
|
|
||||||
PLAN_PATH,
|
|
||||||
NOTES_PATH,
|
|
||||||
VERDICT_PATH,
|
|
||||||
GUARD_RESULTS_PATH,
|
|
||||||
SAGA_NOTES_PATH,
|
|
||||||
DECISIONS_PATH,
|
|
||||||
SUB_PLAN_PATH,
|
|
||||||
JUDGE_PATH,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Copy non-empty working files to `.loop/.stash/`, removing any prior stash first.
|
/// Format Unix epoch seconds as `YYYY-MM-DDThh:mm:ss` using Hinnant's algorithm.
|
||||||
/// Returns the count of files stashed.
|
fn format_unix_timestamp(secs: u64) -> String {
|
||||||
|
let z = (secs / 86400) as i64 + 719468;
|
||||||
|
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
||||||
|
let doe = (z - era * 146097) as u64;
|
||||||
|
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||||
|
let y = yoe as i64 + era * 400;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||||
|
let mp = (5 * doy + 2) / 153;
|
||||||
|
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||||
|
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||||
|
let y = if m <= 2 { y + 1 } else { y };
|
||||||
|
|
||||||
|
let time_of_day = secs % 86400;
|
||||||
|
let h = time_of_day / 3600;
|
||||||
|
let min = (time_of_day % 3600) / 60;
|
||||||
|
let s = time_of_day % 60;
|
||||||
|
|
||||||
|
format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", y, m, d, h, min, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SipHash timestamp + file names/contents → lower 28 bits → 7-char hex.
|
||||||
|
fn generate_stash_hash(timestamp: &str, files: &[(String, Vec<u8>)]) -> String {
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
timestamp.hash(&mut hasher);
|
||||||
|
for (name, contents) in files {
|
||||||
|
name.hash(&mut hasher);
|
||||||
|
contents.hash(&mut hasher);
|
||||||
|
}
|
||||||
|
format!("{:07x}", hasher.finish() & 0x0FFF_FFFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect all regular files in `.loop/` excluding dotfile/dotdir entries.
|
||||||
|
/// Returns sorted `(filename, contents)` pairs for deterministic hashing.
|
||||||
|
fn collect_stashable_files() -> Vec<(String, Vec<u8>)> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
if let Ok(entries) = fs::read_dir(".loop") {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if name.starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_file() {
|
||||||
|
if let Ok(contents) = fs::read(&path) {
|
||||||
|
files.push((name, contents));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
files
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StashEntry {
|
||||||
|
hash: String,
|
||||||
|
timestamp: String,
|
||||||
|
mode: String,
|
||||||
|
files: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse `.loop/.stash/index` into a list of stash entries (oldest first).
|
||||||
|
fn parse_stash_index() -> Vec<StashEntry> {
|
||||||
|
let index_path = format!("{}/index", STASH_DIR);
|
||||||
|
let content = match fs::read_to_string(&index_path) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
content
|
||||||
|
.lines()
|
||||||
|
.filter(|l| !l.trim().is_empty())
|
||||||
|
.filter_map(|line| {
|
||||||
|
let parts: Vec<&str> = line.splitn(4, '|').collect();
|
||||||
|
if parts.len() < 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(StashEntry {
|
||||||
|
hash: parts[0].to_string(),
|
||||||
|
timestamp: parts[1].to_string(),
|
||||||
|
mode: parts[2].to_string(),
|
||||||
|
files: parts[3].split(',').map(|s| s.to_string()).collect(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stash core ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Snapshot all stashable files in `.loop/` to a new log entry.
|
||||||
|
/// Returns `Ok(hash)` on success.
|
||||||
|
fn stash_snapshot() -> Result<String, String> {
|
||||||
|
let files = collect_stashable_files();
|
||||||
|
if files.is_empty() {
|
||||||
|
return Err("no files to stash in .loop/".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
let timestamp = format_unix_timestamp(now);
|
||||||
|
let mode = detect_mode().unwrap_or("unknown").to_string();
|
||||||
|
|
||||||
|
let mut hash = generate_stash_hash(×tamp, &files);
|
||||||
|
|
||||||
|
// Collision handling: rehash with counter suffix
|
||||||
|
let stash_base = Path::new(STASH_DIR);
|
||||||
|
for attempt in 0..100u32 {
|
||||||
|
if !stash_base.join(&hash).exists() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if attempt == 99 {
|
||||||
|
return Err("hash collision after 100 attempts".to_string());
|
||||||
|
}
|
||||||
|
hash = generate_stash_hash(&format!("{}{}", timestamp, attempt + 1), &files);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry_dir = stash_base.join(&hash);
|
||||||
|
fs::create_dir_all(&entry_dir)
|
||||||
|
.map_err(|e| format!("failed to create {}: {}", entry_dir.display(), e))?;
|
||||||
|
|
||||||
|
let file_names: Vec<&str> = files.iter().map(|(name, _)| name.as_str()).collect();
|
||||||
|
|
||||||
|
for (name, contents) in &files {
|
||||||
|
let dest = entry_dir.join(name);
|
||||||
|
fs::write(&dest, contents)
|
||||||
|
.map_err(|e| format!("failed to write {}: {}", dest.display(), e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append to index
|
||||||
|
let index_path = format!("{}/index", STASH_DIR);
|
||||||
|
let line = format!(
|
||||||
|
"{}|{}|{}|{}\n",
|
||||||
|
hash,
|
||||||
|
timestamp,
|
||||||
|
mode,
|
||||||
|
file_names.join(",")
|
||||||
|
);
|
||||||
|
let mut f = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&index_path)
|
||||||
|
.map_err(|e| format!("failed to open index: {}", e))?;
|
||||||
|
std::io::Write::write_all(&mut f, line.as_bytes())
|
||||||
|
.map_err(|e| format!("failed to write index: {}", e))?;
|
||||||
|
|
||||||
|
Ok(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stash commands ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Snapshot `.loop/` to a new stash log entry. Returns file count for `clean()`.
|
||||||
fn stash_working_files() -> usize {
|
fn stash_working_files() -> usize {
|
||||||
let stash = Path::new(STASH_DIR);
|
let files = collect_stashable_files();
|
||||||
|
if files.is_empty() {
|
||||||
// Remove prior stash (single-slot semantics)
|
|
||||||
if stash.exists() {
|
|
||||||
if let Err(e) = fs::remove_dir_all(stash) {
|
|
||||||
log_error(&format!("failed to remove prior stash: {}", e));
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
let count = files.len();
|
||||||
|
match stash_snapshot() {
|
||||||
// Collect non-empty candidates
|
Ok(_) => count,
|
||||||
let to_stash: Vec<&str> = STASH_CANDIDATES
|
Err(_) => 0,
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|path| {
|
|
||||||
Path::new(path).exists()
|
|
||||||
&& fs::read_to_string(path)
|
|
||||||
.map(|c| !c.trim().is_empty())
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if to_stash.is_empty() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = fs::create_dir_all(stash) {
|
|
||||||
log_error(&format!("failed to create {}: {}", STASH_DIR, e));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut count = 0;
|
|
||||||
for path in &to_stash {
|
|
||||||
let file_name = Path::new(path).file_name().unwrap();
|
|
||||||
let dest = stash.join(file_name);
|
|
||||||
if let Err(e) = fs::copy(path, &dest) {
|
|
||||||
log_error(&format!("failed to stash {}: {}", path, e));
|
|
||||||
} else {
|
|
||||||
count += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
count
|
/// `yoke stash` — create a new stash entry and print the hash.
|
||||||
}
|
fn stash_create() -> i32 {
|
||||||
|
if !Path::new(".loop").exists() {
|
||||||
/// Restore stashed files back to their canonical paths and remove the stash.
|
log_error(".loop/ directory not found");
|
||||||
fn stash_pop() -> i32 {
|
|
||||||
let stash = Path::new(STASH_DIR);
|
|
||||||
if !stash.exists() {
|
|
||||||
log_error("no stash found — nothing to restore");
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
match stash_snapshot() {
|
||||||
let entries = match fs::read_dir(stash) {
|
Ok(hash) => {
|
||||||
Ok(e) => e,
|
log(&format!("stashed → {}{}{}", BLUE, hash, RESET));
|
||||||
Err(e) => {
|
|
||||||
log_error(&format!("failed to read {}: {}", STASH_DIR, e));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build a map from filename → canonical path
|
|
||||||
let canonical: std::collections::HashMap<&str, &str> = STASH_CANDIDATES
|
|
||||||
.iter()
|
|
||||||
.filter_map(|p| {
|
|
||||||
Path::new(p)
|
|
||||||
.file_name()
|
|
||||||
.and_then(|f| f.to_str())
|
|
||||||
.map(|f| (f, *p))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut restored = 0;
|
|
||||||
for entry in entries {
|
|
||||||
let entry = match entry {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
let name = entry.file_name();
|
|
||||||
let name_str = name.to_string_lossy();
|
|
||||||
if let Some(&dest_path) = canonical.get(name_str.as_ref()) {
|
|
||||||
if let Err(e) = fs::copy(entry.path(), dest_path) {
|
|
||||||
log_error(&format!("failed to restore {}: {}", dest_path, e));
|
|
||||||
} else {
|
|
||||||
log(&format!("restored: {}", dest_path));
|
|
||||||
restored += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log(&format!("skip (unknown): {}", name_str));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = fs::remove_dir_all(stash) {
|
|
||||||
log_error(&format!("failed to remove stash: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
log(&format!("restored {} file(s)", restored));
|
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
Err(msg) => {
|
||||||
|
log_error(&msg);
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `yoke stash log` — list all stash entries (newest first).
|
||||||
|
fn stash_log_cmd() -> i32 {
|
||||||
|
if !Path::new(".loop").exists() {
|
||||||
|
log_error(".loop/ directory not found");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let entries = parse_stash_index();
|
||||||
|
if entries.is_empty() {
|
||||||
|
log("no stash entries");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for entry in entries.iter().rev() {
|
||||||
|
eprintln!(
|
||||||
|
"{}{}{}{} {} mode={}",
|
||||||
|
BOLD, BLUE, entry.hash, RESET, entry.timestamp, entry.mode
|
||||||
|
);
|
||||||
|
for file in &entry.files {
|
||||||
|
eprintln!(" {}", file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `yoke stash checkout <hash>` — auto-stash current state, clear `.loop/` files,
|
||||||
|
/// restore target entry. Supports prefix matching.
|
||||||
|
fn stash_checkout(target_hash: &str) -> i32 {
|
||||||
|
if !Path::new(".loop").exists() {
|
||||||
|
log_error(".loop/ directory not found");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let entries = parse_stash_index();
|
||||||
|
if entries.is_empty() {
|
||||||
|
log_error("no stash entries");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching entries (exact or prefix)
|
||||||
|
let matches: Vec<&StashEntry> = entries
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.hash == target_hash || e.hash.starts_with(target_hash))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match matches.len() {
|
||||||
|
0 => {
|
||||||
|
log_error(&format!("no stash entry matching '{}'", target_hash));
|
||||||
|
1
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
let target = matches[0];
|
||||||
|
let entry_dir = Path::new(STASH_DIR).join(&target.hash);
|
||||||
|
if !entry_dir.exists() {
|
||||||
|
log_error("stash entry directory missing — index is corrupt");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-stash current state (skip if .loop/ has no stashable files)
|
||||||
|
let current_files = collect_stashable_files();
|
||||||
|
if !current_files.is_empty() {
|
||||||
|
match stash_snapshot() {
|
||||||
|
Ok(hash) => {
|
||||||
|
log(&format!(
|
||||||
|
"auto-stashed current state → {}{}{}",
|
||||||
|
BLUE, hash, RESET
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(msg) => {
|
||||||
|
log_error(&format!("failed to auto-stash: {}", msg));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all stashable files from .loop/
|
||||||
|
for (name, _) in &collect_stashable_files() {
|
||||||
|
let path = Path::new(".loop").join(name);
|
||||||
|
let _ = fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore files from the target entry
|
||||||
|
if let Ok(dir_entries) = fs::read_dir(&entry_dir) {
|
||||||
|
for entry in dir_entries.flatten() {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let dest = Path::new(".loop").join(&name);
|
||||||
|
if let Err(e) = fs::copy(entry.path(), &dest) {
|
||||||
|
log_error(&format!(
|
||||||
|
"failed to restore {}: {}",
|
||||||
|
name.to_string_lossy(),
|
||||||
|
e
|
||||||
|
));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(&format!("checked out {}{}{}", BLUE, target.hash, RESET));
|
||||||
|
0
|
||||||
|
}
|
||||||
|
n => {
|
||||||
|
log_error(&format!(
|
||||||
|
"ambiguous hash '{}' — matches {} entries",
|
||||||
|
target_hash, n
|
||||||
|
));
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `yoke stash pop` — checkout the most recent stash entry.
|
||||||
|
fn stash_pop() -> i32 {
|
||||||
|
let entries = parse_stash_index();
|
||||||
|
match entries.last() {
|
||||||
|
Some(entry) => stash_checkout(&entry.hash),
|
||||||
|
None => {
|
||||||
|
log_error("no stash found — nothing to restore");
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn print_stash_help() {
|
fn print_stash_help() {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}{}[yoke stash]{} recover auto-stashed working files",
|
"{}{}[yoke stash]{} snapshot and restore .loop/ state",
|
||||||
ORANGE, BOLD, RESET
|
ORANGE, BOLD, RESET
|
||||||
);
|
);
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}USAGE:{} yoke stash pop",
|
"{}USAGE:{} yoke stash [subcommand]",
|
||||||
BOLD, RESET
|
BOLD, RESET
|
||||||
);
|
);
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("Every 'yoke clean' automatically stashes non-empty working files");
|
|
||||||
eprintln!("into .loop/.stash/ before wiping. Use 'yoke stash pop' to restore");
|
|
||||||
eprintln!("them. Only one stash slot is kept — each clean overwrites the prior stash.");
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("{}SUBCOMMANDS:{}", BOLD, RESET);
|
eprintln!("{}SUBCOMMANDS:{}", BOLD, RESET);
|
||||||
eprintln!(" {}pop{} Restore stashed files and remove the stash", BOLD, RESET);
|
eprintln!(
|
||||||
|
" {}(none){} Snapshot all .loop/ files to a new stash entry",
|
||||||
|
BOLD, RESET
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" {}log{} List all stash entries (hash, timestamp, mode, files)",
|
||||||
|
BOLD, RESET
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" {}checkout <hash>{} Auto-stash current state, then restore target entry",
|
||||||
|
BOLD, RESET
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" {}pop{} Alias for checkout of the most recent entry",
|
||||||
|
BOLD, RESET
|
||||||
|
);
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("{}EXAMPLES:{}", BOLD, RESET);
|
||||||
|
eprintln!(" yoke stash # snapshot current .loop/ files");
|
||||||
|
eprintln!(" yoke stash log # show all stash entries");
|
||||||
|
eprintln!(" yoke stash checkout a1b2c3d # restore a specific snapshot");
|
||||||
|
eprintln!(" yoke stash pop # restore the most recent snapshot");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the list of (path, default_content) pairs for a given mode.
|
/// Return the list of (path, default_content) pairs for a given mode.
|
||||||
|
|
@ -2000,12 +2208,20 @@ fn main() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match args.get(2).map(|a| a.as_str()) {
|
match args.get(2).map(|a| a.as_str()) {
|
||||||
Some("pop") => {
|
None => {
|
||||||
if args.len() > 3 {
|
process::exit(stash_create());
|
||||||
log_error(&format!("unexpected argument '{}'", args[3]));
|
}
|
||||||
print_stash_help();
|
Some("log") => {
|
||||||
|
process::exit(stash_log_cmd());
|
||||||
|
}
|
||||||
|
Some("checkout") => match args.get(3) {
|
||||||
|
Some(hash) => process::exit(stash_checkout(hash)),
|
||||||
|
None => {
|
||||||
|
log_error("missing hash — usage: yoke stash checkout <hash>");
|
||||||
process::exit(2);
|
process::exit(2);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Some("pop") => {
|
||||||
process::exit(stash_pop());
|
process::exit(stash_pop());
|
||||||
}
|
}
|
||||||
Some(other) => {
|
Some(other) => {
|
||||||
|
|
@ -2013,11 +2229,6 @@ fn main() {
|
||||||
print_stash_help();
|
print_stash_help();
|
||||||
process::exit(2);
|
process::exit(2);
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
log_error("missing subcommand — try 'yoke stash pop'");
|
|
||||||
print_stash_help();
|
|
||||||
process::exit(2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"layer" => {
|
"layer" => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue