Skip to main content

joy_core/
git_ops.rs

1// Copyright (c) 2026 Joydev GmbH (joydev.com)
2// SPDX-License-Identifier: MIT
3
4//! Automatic git operations triggered by Joy file writes.
5//! All operations are best-effort: failures print a warning but never
6//! abort the Joy command.
7
8use std::path::Path;
9
10use crate::model::config::AutoGit;
11use crate::store;
12use crate::vcs::default_vcs;
13
14/// Read the configured auto-git level.
15pub fn auto_git_level() -> AutoGit {
16    store::load_config().workflow.auto_git
17}
18
19/// Stage the given paths if auto-git >= Add.
20/// Paths are relative to the project root.
21/// Errors are printed as warnings and swallowed.
22///
23/// Paths that match `.gitignore` are silently skipped before `git
24/// add` runs. Without that filter an accidental write to an ignored
25/// path -- or a stale index entry that survived an earlier
26/// ordering bug -- would either pollute the index or print git's
27/// "paths are ignored" warning on every subsequent run.
28pub fn auto_git_add(root: &Path, paths: &[&str]) {
29    let level = auto_git_level();
30    if !level.should_add() || paths.is_empty() {
31        return;
32    }
33    let vcs = default_vcs();
34    let kept: Vec<&str> = paths
35        .iter()
36        .copied()
37        .filter(|p| !vcs.is_ignored(root, p))
38        .collect();
39    if kept.is_empty() {
40        return;
41    }
42    if let Err(e) = vcs.add(root, &kept) {
43        eprintln!("Warning: auto-git add failed: {e}");
44    }
45}
46
47/// After a mutating command completes, commit and optionally push
48/// if auto-git >= Commit.
49///
50/// `summary` is the commit subject line (e.g. "add JOY-005D Auto-add...").
51/// `identity` is the Joy identity string for Co-Authored-By.
52pub fn auto_git_post_command(root: &Path, summary: &str, identity: &str) {
53    let level = auto_git_level();
54    if !level.should_commit() {
55        return;
56    }
57
58    let vcs = default_vcs();
59
60    let message = format!("joy: {summary}\n\nCo-Authored-By: {identity}");
61    if let Err(e) = vcs.commit(root, &message) {
62        let err = e.to_string();
63        // "nothing to commit" is not an error worth warning about
64        if !err.contains("nothing to commit") {
65            eprintln!("Warning: auto-git commit failed: {e}");
66        }
67        return;
68    }
69
70    if level.should_push() {
71        let remote = vcs.default_remote(root).unwrap_or_else(|_| "origin".into());
72        if let Err(e) = vcs.push(root, &remote) {
73            eprintln!("Warning: auto-git push failed: {e}");
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn auto_git_level_returns_default() {
84        // Outside a project root, load_config returns default (Add)
85        let level = auto_git_level();
86        assert_eq!(level, AutoGit::Add);
87    }
88}