use std::{
fs,
path::{Path, PathBuf},
};
use anyhow::{Context as _, Result};
use repo::{Repository, RepositoryCapability, discover_heddle_root};
use serde::Serialize;
use sley::Repository as SleyRepository;
use super::action_line::print_command;
use crate::cli::{Cli, should_output_json, style};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AnnotationSurface {
Context,
Discuss,
}
impl AnnotationSurface {
fn records(self) -> &'static str {
match self {
Self::Context => "context annotations",
Self::Discuss => "discussions",
}
}
fn empty_collection(self) -> EmptyCollection {
match self {
Self::Context => EmptyCollection::Context { items: [] },
Self::Discuss => EmptyCollection::Discuss { discussions: [] },
}
}
fn notice_marker(self) -> &'static str {
match self {
Self::Context => "context-locality-notice",
Self::Discuss => "discuss-locality-notice",
}
}
fn first_step(self) -> &'static str {
match self {
Self::Context => "heddle context set --path <path> --scope file -m \"...\"",
Self::Discuss => "heddle discuss open <file> <symbol> '<question>'",
}
}
}
pub(crate) enum AnnotationStore {
Present(Box<Repository>),
Absent(AbsentStore),
}
pub(crate) struct AbsentStore {
path: PathBuf,
git_root: Option<PathBuf>,
}
impl AbsentStore {
fn in_git_checkout(&self) -> bool {
self.git_root.is_some()
}
}
fn command_path(cli: &Cli) -> Result<PathBuf> {
match cli.repo.as_ref() {
Some(path) => Ok(path.clone()),
None => std::env::current_dir().context("get current working directory"),
}
}
pub(crate) fn open_annotation_store(cli: &Cli) -> Result<AnnotationStore> {
let path = command_path(cli)?;
if discover_heddle_root(&path).is_some() {
return Ok(AnnotationStore::Present(Box::new(cli.open_repo()?)));
}
Ok(AnnotationStore::Absent(AbsentStore {
git_root: plain_git_root(&path),
path,
}))
}
fn plain_git_root(path: &Path) -> Option<PathBuf> {
SleyRepository::open_from_environment(path)
.ok()
.and_then(|git| git.workdir())
}
#[derive(Serialize)]
pub(crate) struct AbsentStoreOutput {
output_kind: String,
store_present: bool,
store_scope: &'static str,
path: String,
git_checkout: bool,
reason: String,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
collection: Option<EmptyCollection>,
#[serde(skip)]
records: &'static str,
#[serde(skip)]
first_step: &'static str,
}
#[derive(Serialize)]
#[serde(untagged)]
pub(crate) enum EmptyCollection {
Context { items: [(); 0] },
Discuss { discussions: [(); 0] },
}
pub(crate) fn report_absent_store(
cli: &Cli,
surface: AnnotationSurface,
output_kind: &str,
with_items: bool,
absent: &AbsentStore,
) -> Result<()> {
let records = surface.records();
let output = AbsentStoreOutput {
output_kind: output_kind.to_string(),
store_present: false,
store_scope: "native-heddle-only",
path: absent.path.display().to_string(),
git_checkout: absent.in_git_checkout(),
reason: format!(
"no Heddle store at this path; {records} are native-only and are not carried by \
git clone"
),
collection: with_items.then_some(surface.empty_collection()),
records,
first_step: surface.first_step(),
};
render_absent_store(cli, &output)
}
fn render_absent_store(cli: &Cli, output: &AbsentStoreOutput) -> Result<()> {
if should_output_json(cli, None) {
println!("{}", serde_json::to_string(output)?);
return Ok(());
}
let records = output.records;
println!("No Heddle store here — cannot say whether {records} exist.");
if output.git_checkout {
println!(
"This is a Git checkout with no `.heddle` store, and {records} are a native Heddle \
feature — not projected into Git, so `git clone` does not carry them."
);
println!();
println!("{}", style::bold("Next"));
print_command("heddle init");
println!(
" then: {} (records stay local to this working copy)",
output.first_step
);
} else {
println!(
"No Heddle repository was found at {} or in its ancestors.",
output.path
);
println!();
println!("{}", style::bold("Next"));
print_command("heddle init");
}
Ok(())
}
pub(crate) fn emit_locality_notice_once(repo: &Repository, surface: AnnotationSurface) {
if repo.capability() != RepositoryCapability::GitOverlay {
return;
}
let Some(marker) = notice_marker_path(repo, surface) else {
return;
};
if marker.exists() {
return;
}
if let Some(parent) = marker.parent()
&& fs::create_dir_all(parent).is_err()
{
return;
}
if fs::write(&marker, b"shown\n").is_err() {
return;
}
eprintln!(
"Note: {} are local to this working copy — they live in `.heddle` and do not travel \
with `git push` / `git clone`. See `heddle help {}`.",
surface.records(),
match surface {
AnnotationSurface::Context => "context",
AnnotationSurface::Discuss => "discuss",
}
);
}
fn notice_marker_path(repo: &Repository, surface: AnnotationSurface) -> Option<PathBuf> {
let local = repo.root().join(".heddle");
let base = if local.is_dir() {
local
} else {
repo.heddle_dir().to_path_buf()
};
Some(base.join("state").join(surface.notice_marker()))
}