Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 6 |
| 7 | use std::fs; |
| 8 | use std::path::{Path, PathBuf}; |
| 9 | use std::process::Command; |
| 10 |
| 11 | use vcs_core::tree::EntryKind; |
| 12 |
| 13 | /// The user's home folder. |
| 14 | pub fn user_home() -> Option<PathBuf> { |
| 15 | std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")).filter(|h| !h.is_empty()).map(PathBuf::from) |
| 16 | } |
| 17 |
| 18 | /// The user's name, for an author when nothing else says. |
| 19 | pub fn user_name() -> Option<String> { |
| 20 | std::env::var("USER").or_else(|_| std::env::var("USERNAME")).ok().filter(|u| !u.is_empty()) |
| 21 | } |
| 22 |
| 23 | /// A folder's full path, links resolved. On Windows, without the `\\?\` |
| 24 | /// prefix where the path doesn't need it, so it reads as people write it. |
| 25 | pub fn canonical(path: &Path) -> std::io::Result<PathBuf> { |
| 26 | let full = path.canonicalize()?; |
| 27 | if cfg!(windows) |
| 28 | && let Some(s) = full.to_str() |
| 29 | { |
| 30 | if let Some(rest) = s.strip_prefix(r"\\?\UNC\") { |
| 31 | return Ok(PathBuf::from(format!(r"\\{rest}"))); |
| 32 | } |
| 33 | if let Some(rest) = s.strip_prefix(r"\\?\") |
| 34 | && rest.len() < 260 |
| 35 | { |
| 36 | return Ok(PathBuf::from(rest)); |
| 37 | } |
| 38 | } |
| 39 | Ok(full) |
| 40 | } |
| 41 |
| 42 | /// Whether this system can have a file or folder by this name. Windows |
| 43 | /// can't: reserved device names (`aux.c`, `NUL`), `<>:"|?*`, control |
| 44 | /// characters, or a trailing dot or space. (`:` would also reach a file's |
| 45 | /// hidden streams there.) |
| 46 | pub fn fits(name: &str) -> bool { |
| 47 | if !cfg!(windows) { |
| 48 | return true; |
| 49 | } |
| 50 | if name.ends_with(['.', ' ']) || name.chars().any(|c| (c as u32) < 32 || "<>:\"|?*".contains(c)) { |
| 51 | return false; |
| 52 | } |
| 53 | let stem = name.split('.').next().unwrap_or("").trim_end_matches(' ').to_ascii_uppercase(); |
| 54 | let numbered = |p: &str| stem.len() == 4 && stem.starts_with(p) && matches!(stem.as_bytes()[3], b'0'..=b'9'); |
| 55 | !(matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL" | "CONIN$" | "CONOUT$") || numbered("COM") || numbered("LPT")) |
| 56 | } |
| 57 |
| 58 | /// A path inside a workspace as trees name it: "a/b", whatever the OS's separator. |
| 59 | pub fn tree_path(rel: &Path) -> Option<String> { |
| 60 | let s = rel.to_str()?; |
| 61 | Some(if cfg!(windows) { s.replace('\\', "/") } else { s.to_string() }) |
| 62 | } |
| 63 |
| 64 | /// A file's modification time in nanoseconds, and its file number where |
| 65 | /// the OS gives one cheaply (0 otherwise). |
| 66 | #[cfg(unix)] |
| 67 | pub fn times(meta: &fs::Metadata) -> (i64, u64) { |
| 68 | use std::os::unix::fs::MetadataExt; |
| 69 | (meta.mtime() * 1_000_000_000 + meta.mtime_nsec(), meta.ino()) |
| 70 | } |
| 71 |
| 72 | #[cfg(windows)] |
| 73 | pub fn times(meta: &fs::Metadata) -> (i64, u64) { |
| 74 | use std::os::windows::fs::MetadataExt; |
| 75 | // 100-nanosecond ticks since 1601; as nanoseconds since 1970, like Unix. |
| 76 | const TICKS_TO_1970: i64 = 116_444_736_000_000_000; |
| 77 | (((meta.last_write_time() as i64) - TICKS_TO_1970).saturating_mul(100), 0) |
| 78 | } |
| 79 |
| 80 | /// What a file on disk is. `was` is what vcs knew it as. |
| 81 | #[cfg(unix)] |
| 82 | pub fn kind_of(meta: &fs::Metadata, _was: Option<EntryKind>) -> EntryKind { |
| 83 | use std::os::unix::fs::PermissionsExt; |
| 84 | if meta.file_type().is_symlink() { |
| 85 | EntryKind::Link |
| 86 | } else if meta.permissions().mode() & 0o111 != 0 { |
| 87 | EntryKind::Exec |
| 88 | } else { |
| 89 | EntryKind::File |
| 90 | } |
| 91 | } |
| 92 |
| 93 | #[cfg(windows)] |
| 94 | pub fn kind_of(meta: &fs::Metadata, was: Option<EntryKind>) -> EntryKind { |
| 95 | if meta.file_type().is_symlink() { |
| 96 | return EntryKind::Link; |
| 97 | } |
| 98 | match was { |
| 99 | Some(k @ (EntryKind::Exec | EntryKind::Link)) => k, |
| 100 | _ => EntryKind::File, |
| 101 | } |
| 102 | } |
| 103 |
| 104 | /// A link's target, as trees keep it ("a/b"). |
| 105 | pub fn read_link(path: &Path) -> std::io::Result<Vec<u8>> { |
| 106 | match fs::read_link(path) { |
| 107 | Ok(target) => Ok(target.to_string_lossy().replace(if cfg!(windows) { '\\' } else { '/' }, "/").into_bytes()), |
| 108 | // Written as a file holding its target (Windows without links). |
| 109 | Err(_) if cfg!(windows) => fs::read(path), |
| 110 | Err(e) => Err(e), |
| 111 | } |
| 112 | } |
| 113 |
| 114 | /// Makes a link at `at` to `target` ("a/b"). |
| 115 | pub fn write_link(target: &str, at: &Path) -> std::io::Result<()> { |
| 116 | #[cfg(unix)] |
| 117 | { |
| 118 | std::os::unix::fs::symlink(target, at) |
| 119 | } |
| 120 | #[cfg(windows)] |
| 121 | { |
| 122 | let native = target.replace('/', "\\"); |
| 123 | let to_dir = at.parent().is_some_and(|p| p.join(&native).is_dir()); |
| 124 | let made = if to_dir { std::os::windows::fs::symlink_dir(&native, at) } else { std::os::windows::fs::symlink_file(&native, at) }; |
| 125 | // Links need Developer Mode or admin rights there; one that can't |
| 126 | // be read back is no use either. |
| 127 | if made.is_ok() && fs::symlink_metadata(at).is_ok() { |
| 128 | return Ok(()); |
| 129 | } |
| 130 | let _ = fs::remove_file(at); |
| 131 | fs::write(at, target) |
| 132 | } |
| 133 | } |
| 134 |
| 135 | /// Marks a file executable or not (Windows has no such mark). |
| 136 | pub fn set_exec(path: &Path, exec: bool) -> std::io::Result<()> { |
| 137 | #[cfg(unix)] |
| 138 | { |
| 139 | use std::os::unix::fs::PermissionsExt; |
| 140 | fs::set_permissions(path, fs::Permissions::from_mode(if exec { 0o755 } else { 0o644 })) |
| 141 | } |
| 142 | #[cfg(windows)] |
| 143 | { |
| 144 | let _ = (path, exec); |
| 145 | Ok(()) |
| 146 | } |
| 147 | } |
| 148 |
| 149 | /// Makes a file readable only by its owner. On Windows, files in the |
| 150 | /// user's own folders already are. |
| 151 | pub fn owner_only(path: &Path, exec: bool) -> std::io::Result<()> { |
| 152 | #[cfg(unix)] |
| 153 | { |
| 154 | use std::os::unix::fs::PermissionsExt; |
| 155 | fs::set_permissions(path, fs::Permissions::from_mode(if exec { 0o700 } else { 0o600 })) |
| 156 | } |
| 157 | #[cfg(windows)] |
| 158 | { |
| 159 | let _ = (path, exec); |
| 160 | Ok(()) |
| 161 | } |
| 162 | } |
| 163 |
| 164 | /// This machine's name. |
| 165 | pub fn hostname() -> String { |
| 166 | #[cfg(unix)] |
| 167 | { |
| 168 | let mut buf = [0u8; 256]; |
| 169 | // SAFETY: gethostname writes at most buf.len() bytes into buf. |
| 170 | if unsafe { libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) } == 0 { |
| 171 | let end = buf.iter().position(|b| *b == 0).unwrap_or(buf.len()); |
| 172 | return String::from_utf8_lossy(&buf[..end]).into_owned(); |
| 173 | } |
| 174 | "this machine".into() |
| 175 | } |
| 176 | #[cfg(windows)] |
| 177 | { |
| 178 | std::env::var("COMPUTERNAME").unwrap_or_else(|_| "this machine".into()) |
| 179 | } |
| 180 | } |
| 181 |
| 182 | /// Lets `cmd` run on its own, so closing the terminal doesn't stop it. |
| 183 | pub fn detach(cmd: &mut Command) { |
| 184 | #[cfg(unix)] |
| 185 | { |
| 186 | use std::os::unix::process::CommandExt; |
| 187 | // SAFETY: setsid is async-signal-safe. |
| 188 | unsafe { |
| 189 | cmd.pre_exec(|| { |
| 190 | libc::setsid(); |
| 191 | Ok(()) |
| 192 | }); |
| 193 | } |
| 194 | } |
| 195 | #[cfg(windows)] |
| 196 | { |
| 197 | use std::os::windows::process::CommandExt; |
| 198 | const DETACHED_PROCESS: u32 = 0x0000_0008; |
| 199 | const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200; |
| 200 | const CREATE_NO_WINDOW: u32 = 0x0800_0000; |
| 201 | cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW); |
| 202 | } |
| 203 | } |
| 204 |
| 205 | /// Opens `url` in the default browser. |
| 206 | pub fn open_url(url: &str) { |
| 207 | let mut cmd = if cfg!(target_os = "macos") { |
| 208 | Command::new("open") |
| 209 | } else if cfg!(windows) { |
| 210 | let mut c = Command::new("rundll32"); |
| 211 | c.arg("url.dll,FileProtocolHandler"); |
| 212 | c |
| 213 | } else { |
| 214 | Command::new("xdg-open") |
| 215 | }; |
| 216 | let _ = cmd.arg(url).stdin(std::process::Stdio::null()).stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn(); |
| 217 | } |
| 218 |