use eyre::{Result, bail};
use serde::{Deserialize, Serialize};
use std::path::Path;
pub(crate) const DEFAULT_STRIDE: u16 = 1;
pub(crate) const SLOTS: u16 = 512;
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum PortRequest {
Fixed(u16),
Auto { base: Option<u16>, stride: u16 },
Passthrough(toml::Value),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct PortClaim {
pub port: u16,
pub base: u16,
pub stride: u16,
}
impl PortClaim {
pub(crate) fn fixed(port: u16) -> Self {
Self {
port,
base: port,
stride: 0,
}
}
pub(crate) fn is_auto(&self) -> bool {
self.stride > 0
}
}
pub(crate) fn parse(name: &str, value: toml::Value) -> Result<PortRequest> {
match value {
toml::Value::Integer(port) => u16::try_from(port)
.ok()
.filter(|p| *p > 0)
.map(PortRequest::Fixed)
.ok_or_else(|| eyre::eyre!("[daemons.{name}].port must be an integer from 1 to 65535")),
toml::Value::String(text) if text == "auto" => Ok(PortRequest::Auto {
base: None,
stride: DEFAULT_STRIDE,
}),
toml::Value::String(text) => bail!(
"[daemons.{name}].port string must be \"auto\"; got {text:?}. Use an integer for a fixed port."
),
toml::Value::Table(table) if table.contains_key("auto") => {
if table.get("auto").and_then(toml::Value::as_bool) != Some(true) {
bail!("[daemons.{name}].port.auto must be true; omit it for a fixed port");
}
let base = port_field(name, &table, "base")?;
let stride = port_field(name, &table, "stride")?.unwrap_or(DEFAULT_STRIDE);
for key in table.keys() {
if !matches!(key.as_str(), "auto" | "base" | "stride") {
bail!("unknown [daemons.{name}].port key {key:?}");
}
}
Ok(PortRequest::Auto { base, stride })
}
other => Ok(PortRequest::Passthrough(other)),
}
}
fn port_field(name: &str, table: &toml::Table, key: &str) -> Result<Option<u16>> {
table
.get(key)
.map(|value| {
value
.as_integer()
.and_then(|n| u16::try_from(n).ok())
.filter(|n| *n > 0)
.ok_or_else(|| {
eyre::eyre!("[daemons.{name}].port.{key} must be an integer from 1 to 65535")
})
})
.transpose()
}
pub(crate) fn is_primary(root: &Path) -> bool {
!crate::git::in_linked_worktree(root)
}
pub(crate) fn slot(root: &Path) -> u16 {
let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
if is_primary(&root) {
return 0;
}
let hash = u64::from_str_radix(&crate::hash::hash_to_str(&root), 16).unwrap_or_default();
1 + u16::try_from(hash % u64::from(SLOTS - 1)).unwrap_or_default()
}
pub(crate) fn resolve(
name: &str,
root: &Path,
base: Option<u16>,
stride: u16,
preset_default: Option<u16>,
persisted: Option<PortClaim>,
) -> Result<PortClaim> {
let base = base.or(preset_default).ok_or_else(|| {
eyre::eyre!(
"[daemons.{name}].port = \"auto\" needs a base port; \
set port = {{ auto = true, base = <port> }} or use a preset"
)
})?;
if let Some(claim) = persisted
&& claim.base == base
&& claim.stride == stride
{
return Ok(claim);
}
let offset = slot(root).checked_mul(stride).ok_or_else(|| {
eyre::eyre!("[daemons.{name}].port stride {stride} overflows the port range")
})?;
let port = base.checked_add(offset).filter(|p| *p > 0).ok_or_else(|| {
eyre::eyre!(
"[daemons.{name}].port base {base} with stride {stride} exceeds 65535 for this worktree; \
choose a lower base or stride"
)
})?;
Ok(PortClaim { port, base, stride })
}
#[cfg(test)]
mod tests {
use super::*;
fn worktree(dir: &Path, name: &str) -> std::path::PathBuf {
let private = dir.join(".git").join("worktrees").join(name);
std::fs::create_dir_all(&private).unwrap();
std::fs::write(dir.join(".git").join("HEAD"), "ref: refs/heads/main\n").unwrap();
std::fs::write(private.join("commondir"), "../..\n").unwrap();
let root = dir.join(name);
std::fs::create_dir_all(&root).unwrap();
std::fs::write(
root.join(".git"),
format!("gitdir: {}\n", private.display()),
)
.unwrap();
root
}
#[test]
fn primary_checkouts_keep_the_base_port() {
let tmp = tempfile::tempdir().unwrap();
let primary = tmp.path().join("project");
std::fs::create_dir_all(primary.join(".git")).unwrap();
assert!(is_primary(&primary));
assert_eq!(slot(&primary), 0);
let claim = resolve("db", &primary, None, 1, Some(5432), None).unwrap();
assert_eq!(claim.port, 5432);
let plain = tmp.path().join("plain");
std::fs::create_dir_all(&plain).unwrap();
assert_eq!(slot(&plain), 0);
}
#[test]
fn linked_worktrees_are_stable_and_distinct() {
let tmp = tempfile::tempdir().unwrap();
let one = worktree(tmp.path(), "feature-a");
let two = worktree(tmp.path(), "feature-b");
assert!(!is_primary(&one));
assert_eq!(
slot(&one),
slot(&one),
"same root resolves to the same slot"
);
assert_ne!(slot(&one), slot(&two));
assert!((1..SLOTS).contains(&slot(&one)));
let a = resolve("db", &one, None, 1, Some(5432), None).unwrap();
let b = resolve("db", &two, None, 1, Some(5432), None).unwrap();
assert_ne!(a.port, b.port);
assert!(a.port > 5432 && a.port <= 5432 + SLOTS);
assert_eq!(a, resolve("db", &one, None, 1, Some(5432), None).unwrap());
}
#[test]
fn persisted_allocations_survive_a_hash_change() {
let tmp = tempfile::tempdir().unwrap();
let root = worktree(tmp.path(), "feature");
let stored = PortClaim {
port: 5999,
base: 5432,
stride: 1,
};
assert_eq!(
resolve("db", &root, None, 1, Some(5432), Some(stored)).unwrap(),
stored
);
let rederived = resolve("db", &root, Some(6000), 1, Some(5432), Some(stored)).unwrap();
assert_eq!(rederived.base, 6000);
assert_ne!(rederived.port, stored.port);
}
#[test]
fn auto_requires_a_base_and_fits_the_port_range() {
let tmp = tempfile::tempdir().unwrap();
let root = worktree(tmp.path(), "feature");
assert!(
resolve("api", &root, None, 1, None, None)
.unwrap_err()
.to_string()
.contains("needs a base port")
);
assert!(resolve("api", &root, Some(65535), 1, None, None).is_err());
assert!(resolve("api", &root, Some(3000), u16::MAX, None, None).is_err());
let primary = tmp.path().join("primary");
std::fs::create_dir_all(&primary).unwrap();
assert_eq!(
resolve("api", &primary, Some(3000), u16::MAX, None, None)
.unwrap()
.port,
3000
);
}
#[test]
fn nested_project_roots_inherit_their_checkout() {
let tmp = tempfile::tempdir().unwrap();
let wt = worktree(tmp.path(), "feature");
let nested = wt.join("packages").join("api");
std::fs::create_dir_all(&nested).unwrap();
assert!(!is_primary(&nested), "nested root is still in the worktree");
assert_ne!(slot(&nested), 0);
let sibling = wt.join("packages").join("web");
std::fs::create_dir_all(&sibling).unwrap();
assert_ne!(slot(&nested), slot(&sibling));
let other_wt = worktree(tmp.path(), "second");
let other_nested = other_wt.join("packages").join("api");
std::fs::create_dir_all(&other_nested).unwrap();
assert_ne!(slot(&nested), slot(&other_nested));
let primary = tmp.path().join("primary");
std::fs::create_dir_all(primary.join(".git")).unwrap();
let primary_nested = primary.join("packages").join("api");
std::fs::create_dir_all(&primary_nested).unwrap();
assert_eq!(slot(&primary_nested), 0);
}
#[test]
fn every_worktree_of_a_bare_repository_is_offset() {
let tmp = tempfile::tempdir().unwrap();
let bare = tmp.path().join("repo.git");
std::fs::create_dir_all(&bare).unwrap();
std::fs::write(bare.join("HEAD"), "ref: refs/heads/main\n").unwrap();
let mut slots = Vec::new();
for name in ["main", "feature"] {
let private = bare.join("worktrees").join(name);
std::fs::create_dir_all(&private).unwrap();
std::fs::write(private.join("commondir"), "../..\n").unwrap();
let root = tmp.path().join(name);
std::fs::create_dir_all(&root).unwrap();
std::fs::write(
root.join(".git"),
format!("gitdir: {}\n", private.display()),
)
.unwrap();
assert!(!is_primary(&root), "{name} is a worktree");
slots.push(slot(&root));
}
assert!(slots.iter().all(|s| *s != 0), "none keeps the base port");
assert_ne!(slots[0], slots[1]);
}
#[test]
fn only_a_worktrees_gitdir_moves_off_the_base_port() {
let tmp = tempfile::tempdir().unwrap();
for target in [
"/repo/.git/modules/sub",
"/elsewhere/detached-git-dir",
"not a gitdir line",
"/repo/worktrees/nested/.git/modules/sub",
"/worktrees",
"/repo/.git/worktrees/pruned",
] {
let root = tmp.path().join(crate::hash::hash_to_str(&target));
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join(".git"), format!("gitdir: {target}\n")).unwrap();
assert!(is_primary(&root), "{target} must keep the base port");
assert_eq!(slot(&root), 0);
}
}
#[test]
fn port_declarations_are_parsed_and_validated() {
assert_eq!(
parse("api", toml::Value::Integer(3000)).unwrap(),
PortRequest::Fixed(3000)
);
assert_eq!(
parse("api", toml::Value::String("auto".into())).unwrap(),
PortRequest::Auto {
base: None,
stride: DEFAULT_STRIDE
}
);
assert_eq!(
parse(
"api",
toml::Value::Table(toml::toml! { auto = true base = 3000 stride = 4 })
)
.unwrap(),
PortRequest::Auto {
base: Some(3000),
stride: 4
}
);
assert!(matches!(
parse(
"api",
toml::Value::Table(toml::toml! { expect = [3000] bump = false })
)
.unwrap(),
PortRequest::Passthrough(_)
));
for invalid in [
toml::Value::Integer(0),
toml::Value::Integer(70000),
toml::Value::String("bump".into()),
toml::Value::Table(toml::toml! { auto = false }),
toml::Value::Table(toml::toml! { auto = true base = 0 }),
toml::Value::Table(toml::toml! { auto = true offset = 3 }),
] {
assert!(parse("api", invalid).is_err());
}
}
}