Skip to main content

git_atomic/core/
effect.rs

1use crate::cli::output::Printer;
2use crate::core::{Error, GitError};
3use std::path::PathBuf;
4
5/// A planned reference edit for atomic batch updates.
6#[derive(Debug)]
7pub struct PlannedRefEdit {
8    pub ref_name: String,
9    pub new_id: gix::ObjectId,
10    pub previous: Option<gix::ObjectId>,
11    pub component: String,
12    pub created: bool,
13}
14
15/// A side effect that a command wants to perform.
16#[derive(Debug)]
17pub enum Effect {
18    /// Atomic batch ref update (preserves all-or-nothing semantics).
19    RefTransaction {
20        repo_path: PathBuf,
21        edits: Vec<PlannedRefEdit>,
22    },
23    /// Push branches to a remote via `git push`.
24    Push {
25        remote: String,
26        branches: Vec<String>,
27    },
28    /// Write a file to disk.
29    WriteFile {
30        path: PathBuf,
31        content: String,
32        /// Structured representation for JSON output. When present, JSON
33        /// dry-run uses this instead of the raw content string.
34        structured: Option<serde_json::Value>,
35    },
36}
37
38/// Execute or preview a list of effects.
39pub fn execute(
40    repo: Option<&gix::Repository>,
41    effects: &[Effect],
42    dry_run: bool,
43    printer: &Printer,
44) -> Result<(), Error> {
45    for effect in effects {
46        if dry_run {
47            printer.print_effect_preview(effect);
48        } else {
49            run_effect(repo, effect)?;
50        }
51    }
52    Ok(())
53}
54
55fn run_effect(repo: Option<&gix::Repository>, effect: &Effect) -> Result<(), Error> {
56    match effect {
57        Effect::RefTransaction { edits, .. } => {
58            let repo =
59                repo.ok_or_else(|| Error::General("RefTransaction requires a repository".into()))?;
60
61            let mut gix_edits: Vec<gix::refs::transaction::RefEdit> = Vec::new();
62            for e in edits {
63                let target = gix::refs::Target::Object(e.new_id);
64                let expected = match e.previous {
65                    Some(id) => gix::refs::transaction::PreviousValue::MustExistAndMatch(
66                        gix::refs::Target::Object(id),
67                    ),
68                    None => gix::refs::transaction::PreviousValue::MustNotExist,
69                };
70
71                gix_edits.push(gix::refs::transaction::RefEdit {
72                    change: gix::refs::transaction::Change::Update {
73                        log: gix::refs::transaction::LogChange {
74                            mode: gix::refs::transaction::RefLog::AndReference,
75                            force_create_reflog: false,
76                            message: "git-atomic: commit".into(),
77                        },
78                        expected,
79                        new: target,
80                    },
81                    name: gix::refs::FullName::try_from(e.ref_name.clone()).map_err(|err| {
82                        GitError::RefUpdate {
83                            branch: e.ref_name.clone(),
84                            reason: format!("invalid ref name: {err}"),
85                        }
86                    })?,
87                    deref: false,
88                });
89            }
90
91            if !gix_edits.is_empty() {
92                repo.edit_references(gix_edits)
93                    .map_err(|e| GitError::RefUpdate {
94                        branch: "batch update".into(),
95                        reason: e.to_string(),
96                    })?;
97            }
98        }
99        Effect::Push { remote, branches } => {
100            let mut cmd = std::process::Command::new("git");
101            cmd.arg("push").arg(remote);
102            for b in branches {
103                cmd.arg(b);
104            }
105            let status = cmd
106                .status()
107                .map_err(|e| Error::General(format!("failed to run git push: {e}")))?;
108            if !status.success() {
109                return Err(Error::General(format!(
110                    "git push exited with status {}",
111                    status
112                )));
113            }
114        }
115        Effect::WriteFile { path, content, .. } => {
116            if let Some(parent) = path.parent() {
117                std::fs::create_dir_all(parent).map_err(|e| {
118                    Error::General(format!(
119                        "failed to create directory {}: {e}",
120                        parent.display()
121                    ))
122                })?;
123            }
124            std::fs::write(path, content)
125                .map_err(|e| Error::General(format!("failed to write {}: {e}", path.display())))?;
126        }
127    }
128    Ok(())
129}