Skip to main content

newgit_core/
materializer.rs

1use camino::{Utf8Path, Utf8PathBuf};
2use serde::{Deserialize, Serialize};
3
4use crate::branch::BranchInstance;
5use crate::error::{NewgitError, Result};
6use crate::source::GitSource;
7
8/// The materializer contract: a workspace presents a full, real, verifiable
9/// Git repo at the binding record's revision. Nothing outside this boundary
10/// may care how that presentation is produced — this implementation clones;
11/// a future one may project.
12pub trait Materializer {
13    fn materialize(&self, source: &GitSource, branch: &BranchInstance) -> Result<()>;
14    fn remove(&self, branch: &BranchInstance) -> Result<()>;
15}
16
17#[derive(Debug, Clone, Copy, Default)]
18pub struct RealDirMaterializer;
19
20impl Materializer for RealDirMaterializer {
21    fn materialize(&self, source: &GitSource, branch: &BranchInstance) -> Result<()> {
22        if branch.workspace_path.exists() {
23            return Err(NewgitError::WorkspaceExists(branch.workspace_path.clone()));
24        }
25        if let Some(parent) = branch.workspace_path.parent() {
26            create_dir_all(parent)?;
27        }
28        source.clone_to(&branch.source_ref, &branch.workspace_path)?;
29        write_workspace_marker(source.root(), branch)
30    }
31
32    fn remove(&self, branch: &BranchInstance) -> Result<()> {
33        if branch.workspace_path.exists() {
34            std::fs::remove_dir_all(&branch.workspace_path)
35                .map_err(|source| NewgitError::io(branch.workspace_path.clone(), source))?;
36        }
37        Ok(())
38    }
39}
40
41/// Gitignored marker inside a workspace pointing back at the store, so
42/// newgit commands run from inside a workspace find the real metadata and
43/// know which instance they are standing in.
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub struct WorkspaceMarker {
46    pub branch: String,
47    pub store_root: Utf8PathBuf,
48}
49
50pub fn workspace_marker_path(workspace: &Utf8Path) -> Utf8PathBuf {
51    workspace.join(".newgit/local/instance.toml")
52}
53
54/// Keep tracker-owned paths out of the workspace clone's Git by writing them
55/// to `.git/info/exclude`.
56///
57/// `newgit tracker track` appends to the store's `.gitignore`, but that edit
58/// is uncommitted until the user commits it — so a clone would not inherit
59/// the rule, and a lane's content would sit in the workspace as ordinary
60/// untracked files that `git add -A` sweeps into source history. Audience
61/// only keeps content out of Git *by construction* if the construction
62/// reaches every workspace.
63///
64/// `info/exclude` rather than the workspace's `.gitignore`: the latter is
65/// tracked content owned by source, and newgit does not rewrite the user's
66/// committed files. This is Git's own per-clone local-ignore mechanism, and
67/// there is no plumbing command that writes it.
68pub fn exclude_tracker_paths(workspace: &Utf8Path, paths: &[Utf8PathBuf]) -> Result<()> {
69    if paths.is_empty() {
70        return Ok(());
71    }
72    let exclude = workspace.join(".git/info/exclude");
73    if let Some(parent) = exclude.parent() {
74        create_dir_all(parent)?;
75    }
76    let mut contents = std::fs::read_to_string(&exclude).unwrap_or_default();
77    if !contents.is_empty() && !contents.ends_with('\n') {
78        contents.push('\n');
79    }
80    contents.push_str("\n# newgit: tracker-owned paths — these lanes own this content\n");
81    for path in paths {
82        contents.push_str(&format!("/{path}\n"));
83    }
84    std::fs::write(&exclude, contents).map_err(|source| NewgitError::io(exclude, source))
85}
86
87fn write_workspace_marker(store_root: &Utf8Path, branch: &BranchInstance) -> Result<()> {
88    let marker = WorkspaceMarker {
89        branch: branch.name.clone(),
90        store_root: store_root.to_path_buf(),
91    };
92    let path = workspace_marker_path(&branch.workspace_path);
93    if let Some(parent) = path.parent() {
94        create_dir_all(parent)?;
95        // Self-ignoring: the marker must never show up in `git status` or be
96        // committable, even when the project has no committed .newgit rules.
97        let gitignore = parent.join(".gitignore");
98        std::fs::write(&gitignore, "*\n").map_err(|source| NewgitError::io(gitignore, source))?;
99    }
100    let contents = toml::to_string_pretty(&marker).map_err(|source| NewgitError::TomlWrite {
101        label: "workspace marker".to_owned(),
102        source,
103    })?;
104    std::fs::write(&path, contents).map_err(|source| NewgitError::io(path, source))
105}
106
107pub fn create_dir_all(path: &Utf8Path) -> Result<()> {
108    std::fs::create_dir_all(path).map_err(|source| NewgitError::io(path.to_path_buf(), source))
109}