1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
//! caixa_naming — typed default-target convention for eaten caixas.
//!
//! Per operator convention: every caixa absorbed via `eat-and-ship`
//! defaults to the slug `pleme-io/caixa-<basename>`, where
//! `<basename>` is the source path's directory name (sanitized to a
//! valid GitHub repo name).
//!
//! Examples:
//! /tmp/clones/sharkdp__fd/fd → pleme-io/caixa-fd
//! ~/code/github/BurntSushi/ripgrep → pleme-io/caixa-ripgrep
//! /tmp/eaten-typed-src → pleme-io/caixa-eaten-typed-src
//!
//! The `caixa-` prefix makes eaten repos visually distinct from
//! pleme-io's native repos in the org listing — anyone browsing
//! pleme-io sees `caixa-*` and knows it's an absorbed upstream.
//!
//! Operators override per-invocation via `--target <owner>/<name>`.
use std::path::Path;
/// The canonical org for eaten caixas.
pub const DEFAULT_OWNER: &str = "pleme-io";
/// The canonical prefix that signals "absorbed upstream".
pub const CAIXA_PREFIX: &str = "caixa-";
/// Compute the default target slug for a given source path. Per
/// operator convention: `pleme-io/caixa-<sanitized-basename>`.
pub fn default_target_slug(source: &Path) -> String {
let basename = source.file_name()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
format!("{DEFAULT_OWNER}/{CAIXA_PREFIX}{}", sanitize_repo_name(basename))
}
/// Sanitize an arbitrary string into a valid GitHub repo name.
/// GitHub repo names allow `[A-Za-z0-9._-]`; everything else collapses
/// to `-`. Empty input → "unknown".
pub fn sanitize_repo_name(s: &str) -> String {
if s.is_empty() { return "unknown".to_string(); }
let cleaned: String = s.chars().map(|c| match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '_' | '-' => c,
_ => '-',
}).collect();
// Strip leading dots / dashes (GitHub rejects repo names starting
// with `.`).
let cleaned = cleaned.trim_start_matches(|c: char| c == '.' || c == '-').to_string();
if cleaned.is_empty() { "unknown".to_string() } else { cleaned }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fd_basename_yields_caixa_fd() {
assert_eq!(
default_target_slug(Path::new("/tmp/clones/sharkdp__fd/fd")),
"pleme-io/caixa-fd"
);
}
#[test]
fn ripgrep_basename_yields_caixa_ripgrep() {
assert_eq!(
default_target_slug(Path::new("/some/path/ripgrep")),
"pleme-io/caixa-ripgrep"
);
}
#[test]
fn trailing_slash_handled() {
// PathBuf strips trailing slash on file_name lookup, so basename
// is the actual directory name.
assert_eq!(
default_target_slug(Path::new("/x/y/zoxide/")),
"pleme-io/caixa-zoxide"
);
}
#[test]
fn sanitize_collapses_unsafe_chars() {
assert_eq!(sanitize_repo_name("foo/bar"), "foo-bar");
assert_eq!(sanitize_repo_name("hello world"), "hello-world");
assert_eq!(sanitize_repo_name("foo@1.0"), "foo-1.0");
assert_eq!(sanitize_repo_name("multi__under"), "multi__under");
}
#[test]
fn sanitize_strips_leading_dots_and_dashes() {
assert_eq!(sanitize_repo_name(".github"), "github");
assert_eq!(sanitize_repo_name("-private"), "private");
assert_eq!(sanitize_repo_name("...dots"), "dots");
}
#[test]
fn sanitize_empty_returns_unknown() {
assert_eq!(sanitize_repo_name(""), "unknown");
assert_eq!(sanitize_repo_name(".."), "unknown");
}
}