ironflow_ops_git/lib.rs
1//! Git operations for Ironflow workflows, powered by [`git2`].
2//!
3//! This crate provides a comprehensive set of Git operations as Ironflow
4//! [`Operation`](ironflow_core::operation::Operation) implementations. Each
5//! operation wraps a [`git2`] API call, running it inside
6//! [`spawn_blocking`](tokio::task::spawn_blocking) since `git2` is synchronous.
7//!
8//! # Architecture
9//!
10//! - [`GitRepo`] is the central handle, wrapping a repository path
11//! - Each operation is a standalone struct implementing [`Operation`](ironflow_core::operation::Operation)
12//! - All operations return `kind() == "git"`
13//! - Parameters are set at construction time, not via [`OperationContext`](ironflow_core::operation::OperationContext);
14//! the context only provides the secrets network operations authenticate with
15//!
16//! # Authentication and secrets
17//!
18//! Operations that talk to a remote ([`RepoClone`](repository::RepoClone),
19//! [`FetchRemote`](fetch::FetchRemote), [`PushRemote`](fetch::PushRemote),
20//! [`RemotePrune`](fetch::RemotePrune),
21//! [`RemoteDefaultBranch`](fetch::RemoteDefaultBranch),
22//! [`SubmoduleUpdate`](submodule::SubmoduleUpdate)) authenticate over HTTPS
23//! with the token held in the `git_token` secret, sent with the username
24//! `oauth2`. Both are overridable per operation with `.token_secret(key)` and
25//! `.username(name)`. Without that secret, they fall back to the SSH agent,
26//! the git credential helper, then libgit2's default credential.
27//!
28//! The token never reaches a step record: credentials embedded in a URL are
29//! masked as `***` in every `input()`, output and error message, and a token
30//! resolved from the secret store is scrubbed from error messages.
31//!
32//! ```no_run
33//! use ironflow_ops_git::repository::RepoClone;
34//! use ironflow_core::operation::{OperationContext, NoopSecretResolver};
35//! use std::sync::Arc;
36//!
37//! # async fn example() -> Result<(), ironflow_core::error::OperationError> {
38//! # let ctx = OperationContext::new(Arc::new(NoopSecretResolver));
39//! // Reads `gitlab_token` from the secret store.
40//! RepoClone::new("https://gitlab.com/group/private.git", "/tmp/review.git")
41//! .bare(true)
42//! .token_secret("gitlab_token")
43//! .run(&ctx)
44//! .await?;
45//! # Ok(())
46//! # }
47//! ```
48//!
49//! # Quick start
50//!
51//! ```no_run
52//! use ironflow_ops_git::GitRepo;
53//! use ironflow_ops_git::repository::RepoInit;
54//! use ironflow_ops_git::commit::CommitCreate;
55//! use ironflow_ops_git::index::IndexAdd;
56//! use ironflow_core::operation::{Operation, OperationContext, NoopSecretResolver};
57//! use std::sync::Arc;
58//!
59//! # async fn example() -> Result<(), ironflow_core::error::OperationError> {
60//! let ctx = OperationContext::new(Arc::new(NoopSecretResolver));
61//!
62//! // Initialize a repo
63//! let init = RepoInit::new("/tmp/my-repo", false);
64//! init.execute(&ctx).await?;
65//!
66//! // Stage a file and commit
67//! let add = IndexAdd::new("/tmp/my-repo", "README.md");
68//! add.execute(&ctx).await?;
69//!
70//! let commit = CommitCreate::new("/tmp/my-repo", "Initial commit", "Alice", "alice@example.com");
71//! commit.execute(&ctx).await?;
72//! # Ok(())
73//! # }
74//! ```
75//!
76//! # Tracked operations
77//!
78//! Every operation implements [`Operation`](ironflow_core::operation::Operation),
79//! so it can be passed to `WorkflowContext::operation()` for step lifecycle
80//! tracking (step record, status transitions, duration, output persistence).
81//!
82//! # Modules
83//!
84//! Operations are organized by Git domain:
85//!
86//! | Module | Operations |
87//! |--------|-----------|
88//! | [`repository`] | Init, Open, Clone, Discover, State |
89//! | [`index`] | Add, AddAll, Remove, RemoveAll, UpdateAll, WriteTree |
90//! | [`commit`] | Create, Find, Amend, Signed |
91//! | [`branch`] | Create, Delete, Rename, List, Lookup, IsHead, SetUpstream, Checkout |
92//! | [`tag`] | CreateLightweight, CreateAnnotated, Delete, List, ListMatch |
93//! | [`remote`] | Create, Delete, Rename, SetUrl, List, Lookup |
94//! | [`fetch`] | Fetch, Push, Prune, DefaultBranch |
95//! | [`merge`] | Branch, Analysis, Commits, Base, CleanupState |
96//! | [`rebase`] | Init, Next, Commit, Abort, Finish |
97//! | [`cherrypick`] | Cherrypick, CherrypickCommit, Revert, RevertCommit |
98//! | [`stash`] | Save, Apply, Pop, Drop, List |
99//! | [`diff`] | TreeToTree, TreeToIndex, IndexToWorkdir, Stats, FindSimilar, Apply |
100//! | [`checkout`] | Head, Index, Tree |
101//! | [`blame`] | BlameFile |
102//! | [`log`] | RevwalkNew, RevwalkPushRange, RevwalkSimplifyFirstParent |
103//! | [`refs`] | Create, Delete, Rename, Lookup, NameToId |
104//! | [`reflog`] | Read, Append, Drop |
105//! | [`submodule`] | Add, Init, Update, Lookup, List |
106//! | [`worktree`] | Add (optionally detached at a commit), List, Validate, Prune, Remove |
107//! | [`config`] | Get, Set, Delete, List |
108//! | [`status`] | File, List, ShouldIgnore |
109//! | [`reset`] | Reset (soft, mixed, hard) |
110//! | [`graph`] | AheadBehind, DescendantOf, Describe |
111//! | [`object`] | BlobCreate, TreeLookup, FindObject |
112
113pub mod blame;
114pub mod branch;
115pub mod checkout;
116pub mod cherrypick;
117pub mod commit;
118pub mod config;
119pub mod diff;
120pub mod fetch;
121pub mod graph;
122mod helpers;
123pub mod index;
124pub mod log;
125pub mod merge;
126pub mod object;
127pub mod rebase;
128pub mod reflog;
129pub mod refs;
130pub mod remote;
131mod repo;
132pub mod repository;
133pub mod reset;
134pub mod stash;
135pub mod status;
136pub mod submodule;
137pub mod tag;
138pub mod worktree;
139
140pub use git2;
141pub use repo::GitRepo;
142
143#[cfg(test)]
144pub(crate) mod test_helpers {
145 use std::fs;
146 use std::path::Path;
147 use std::sync::Arc;
148
149 use git2::{Oid, Repository, Signature};
150 use ironflow_core::operation::{NoopSecretResolver, OperationContext};
151
152 pub(crate) fn ctx() -> OperationContext {
153 OperationContext::new(Arc::new(NoopSecretResolver))
154 }
155
156 pub(crate) fn init_repo(path: &Path) -> Oid {
157 let repo = Repository::init(path).unwrap();
158 fs::write(path.join("file.txt"), "content").unwrap();
159 let mut idx = repo.index().unwrap();
160 idx.add_path(Path::new("file.txt")).unwrap();
161 idx.write().unwrap();
162 let tree = repo.find_tree(idx.write_tree().unwrap()).unwrap();
163 let sig = Signature::now("Test", "test@test.com").unwrap();
164 repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
165 .unwrap()
166 }
167
168 pub(crate) fn make_two_commits(path: &Path) -> (String, String) {
169 let repo = Repository::init(path).unwrap();
170 let sig = Signature::now("Test", "test@test.com").unwrap();
171 fs::write(path.join("file.txt"), "v1").unwrap();
172 let mut idx = repo.index().unwrap();
173 idx.add_path(Path::new("file.txt")).unwrap();
174 idx.write().unwrap();
175 let tree = repo.find_tree(idx.write_tree().unwrap()).unwrap();
176 let c1 = repo
177 .commit(Some("HEAD"), &sig, &sig, "first", &tree, &[])
178 .unwrap();
179 let parent = repo.find_commit(c1).unwrap();
180
181 fs::write(path.join("other.txt"), "v2").unwrap();
182 let mut idx = repo.index().unwrap();
183 idx.add_path(Path::new("other.txt")).unwrap();
184 idx.write().unwrap();
185 let tree2 = repo.find_tree(idx.write_tree().unwrap()).unwrap();
186 let c2 = repo
187 .commit(Some("HEAD"), &sig, &sig, "second", &tree2, &[&parent])
188 .unwrap();
189 (c1.to_string(), c2.to_string())
190 }
191}