Skip to content

Commit cb4d735

Browse files
committed
feat: add optional author override to Ledger::create
Replaces the two-method approach with a single create() taking Option<&git2::Signature<'_>>; None falls back to self.signature() for both author and committer. The CLI create subcommand gains --author-name and --author-email flags; author time defaults to now. feat: accept optional author signature in Ledger::create feat: add --author-name and --author-email to create subcommand closes: #10 Assisted-by: Claude Code (Claude Sonnet 4.6)
1 parent c988c49 commit cb4d735

4 files changed

Lines changed: 94 additions & 21 deletions

File tree

crates/git-ledger/src/cli.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ pub enum Command {
4444
/// Commit message.
4545
#[arg(short, long, default_value = "ledger: create")]
4646
message: String,
47+
48+
/// Author name (requires --author-email).
49+
#[arg(long, requires = "author_email")]
50+
author_name: Option<String>,
51+
52+
/// Author email (requires --author-name).
53+
#[arg(long, requires = "author_name")]
54+
author_email: Option<String>,
4755
},
4856

4957
/// Read a record.

crates/git-ledger/src/lib.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,16 @@ pub enum Mutation<'a> {
4141
/// Core ledger operations.
4242
pub trait Ledger {
4343
/// Create a new record under `ref_prefix`.
44+
///
45+
/// `author` overrides the commit author; when `None`, `self.signature()` is
46+
/// used for both author and committer.
4447
fn create(
4548
&self,
4649
ref_prefix: &str,
4750
strategy: &IdStrategy<'_>,
4851
fields: &[(&str, &[u8])],
4952
message: &str,
53+
author: Option<&git2::Signature<'_>>,
5054
) -> Result<LedgerEntry, Error>;
5155

5256
/// Read an existing record by its full ref name.
@@ -216,14 +220,22 @@ impl Ledger for Repository {
216220
strategy: &IdStrategy<'_>,
217221
fields: &[(&str, &[u8])],
218222
message: &str,
223+
author: Option<&git2::Signature<'_>>,
219224
) -> Result<LedgerEntry, Error> {
220225
let tree_oid = build_fields_tree(self, fields)?;
221226
let tree = self.find_tree(tree_oid)?;
222-
let sig = self.signature()?;
227+
let committer = self.signature()?;
228+
let owned_author;
229+
let author = match author {
230+
Some(a) => a,
231+
None => {
232+
owned_author = self.signature()?;
233+
&owned_author
234+
}
235+
};
223236

224237
if let IdStrategy::CommitOid = strategy {
225-
// Write the commit first (no ref), then name the ref after its OID.
226-
let commit_oid = self.commit(None, &sig, &sig, message, &tree, &[])?;
238+
let commit_oid = self.commit(None, author, &committer, message, &tree, &[])?;
227239
let ref_name = if ref_prefix.ends_with('/') {
228240
format!("{}{}", ref_prefix, commit_oid)
229241
} else {
@@ -258,22 +270,14 @@ impl Ledger for Repository {
258270
format!("{}/{}", ref_prefix, id)
259271
};
260272

261-
// Check the ref doesn't already exist
262273
if self.find_reference(&ref_name).is_ok() {
263274
return Err(Error::from_str(&format!(
264275
"record already exists: {}",
265276
ref_name
266277
)));
267278
}
268279

269-
let commit_oid = self.commit(
270-
Some(&ref_name),
271-
&sig,
272-
&sig,
273-
message,
274-
&tree,
275-
&[], // no parents for first commit
276-
)?;
280+
let commit_oid = self.commit(Some(&ref_name), author, &committer, message, &tree, &[])?;
277281

278282
let fields = read_fields(self, &tree, "")?;
279283
let id = ref_name.rsplit('/').next().unwrap_or(&ref_name).to_string();

crates/git-ledger/src/main.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ fn run(cli: &Cli) -> Result<(), Box<dyn std::error::Error>> {
6565
fields,
6666
files,
6767
message,
68+
author_name,
69+
author_email,
6870
} => {
6971
let mut entries: Vec<(String, Vec<u8>)> = fields
7072
.iter()
@@ -97,7 +99,22 @@ fn run(cli: &Cli) -> Result<(), Box<dyn std::error::Error>> {
9799
IdStrategy::Sequential
98100
};
99101

100-
let entry = repo.create(ref_prefix, &strategy, &parsed, message)?;
102+
let author_sig;
103+
let author = match author_name {
104+
Some(name) => {
105+
let email = author_email.as_deref().unwrap();
106+
let now = std::time::SystemTime::now()
107+
.duration_since(std::time::UNIX_EPOCH)
108+
.unwrap_or_default()
109+
.as_secs() as i64;
110+
let time = git2::Time::new(now, 0);
111+
author_sig = git2::Signature::new(name, email, &time)?;
112+
Some(&author_sig)
113+
}
114+
None => None,
115+
};
116+
117+
let entry = repo.create(ref_prefix, &strategy, &parsed, message, author)?;
101118
println!("{}", entry.ref_);
102119
}
103120

crates/git-ledger/src/tests.rs

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ fn create_sequential() {
2424
&IdStrategy::Sequential,
2525
&[("title", b"hello"), ("status", b"open")],
2626
"create record",
27+
None,
2728
)
2829
.unwrap();
2930

@@ -37,10 +38,22 @@ fn create_sequential_increments() {
3738
let (_dir, repo) = init_repo();
3839

3940
let e1 = repo
40-
.create(PREFIX, &IdStrategy::Sequential, &[("a", b"1")], "first")
41+
.create(
42+
PREFIX,
43+
&IdStrategy::Sequential,
44+
&[("a", b"1")],
45+
"first",
46+
None,
47+
)
4148
.unwrap();
4249
let e2 = repo
43-
.create(PREFIX, &IdStrategy::Sequential, &[("a", b"2")], "second")
50+
.create(
51+
PREFIX,
52+
&IdStrategy::Sequential,
53+
&[("a", b"2")],
54+
"second",
55+
None,
56+
)
4457
.unwrap();
4558

4659
assert_eq!(e1.id, "1");
@@ -57,6 +70,7 @@ fn create_caller_provided() {
5770
&IdStrategy::CallerProvided("my-record"),
5871
&[("title", b"test")],
5972
"create",
73+
None,
6074
)
6175
.unwrap();
6276

@@ -73,6 +87,7 @@ fn create_content_addressed() {
7387
&IdStrategy::ContentAddressed(b"some content"),
7488
&[("data", b"value")],
7589
"create",
90+
None,
7691
)
7792
.unwrap();
7893

@@ -89,13 +104,15 @@ fn create_duplicate_errors() {
89104
&IdStrategy::CallerProvided("dup"),
90105
&[("a", b"1")],
91106
"first",
107+
None,
92108
)
93109
.unwrap();
94110
let result = repo.create(
95111
PREFIX,
96112
&IdStrategy::CallerProvided("dup"),
97113
&[("a", b"2")],
98114
"second",
115+
None,
99116
);
100117
assert!(result.is_err());
101118
}
@@ -110,6 +127,7 @@ fn read_record() {
110127
&IdStrategy::Sequential,
111128
&[("title", b"hello"), ("status", b"open")],
112129
"create",
130+
None,
113131
)
114132
.unwrap();
115133

@@ -138,6 +156,7 @@ fn update_record() {
138156
&IdStrategy::Sequential,
139157
&[("title", b"hello"), ("status", b"open")],
140158
"create",
159+
None,
141160
)
142161
.unwrap();
143162

@@ -164,6 +183,7 @@ fn update_delete_field() {
164183
&IdStrategy::Sequential,
165184
&[("title", b"hello"), ("status", b"open")],
166185
"create",
186+
None,
167187
)
168188
.unwrap();
169189

@@ -189,6 +209,7 @@ fn update_add_field() {
189209
&IdStrategy::Sequential,
190210
&[("title", b"hello")],
191211
"create",
212+
None,
192213
)
193214
.unwrap();
194215

@@ -207,12 +228,30 @@ fn update_add_field() {
207228
fn list_records() {
208229
let (_dir, repo) = init_repo();
209230

210-
repo.create(PREFIX, &IdStrategy::Sequential, &[("a", b"1")], "first")
211-
.unwrap();
212-
repo.create(PREFIX, &IdStrategy::Sequential, &[("a", b"2")], "second")
213-
.unwrap();
214-
repo.create(PREFIX, &IdStrategy::Sequential, &[("a", b"3")], "third")
215-
.unwrap();
231+
repo.create(
232+
PREFIX,
233+
&IdStrategy::Sequential,
234+
&[("a", b"1")],
235+
"first",
236+
None,
237+
)
238+
.unwrap();
239+
repo.create(
240+
PREFIX,
241+
&IdStrategy::Sequential,
242+
&[("a", b"2")],
243+
"second",
244+
None,
245+
)
246+
.unwrap();
247+
repo.create(
248+
PREFIX,
249+
&IdStrategy::Sequential,
250+
&[("a", b"3")],
251+
"third",
252+
None,
253+
)
254+
.unwrap();
216255

217256
let ids = repo.list(PREFIX).unwrap();
218257
assert_eq!(ids, vec!["1", "2", "3"]);
@@ -235,6 +274,7 @@ fn history_tracks_updates() {
235274
&IdStrategy::Sequential,
236275
&[("status", b"open")],
237276
"create",
277+
None,
238278
)
239279
.unwrap();
240280

@@ -265,6 +305,7 @@ fn create_with_nested_fields() {
265305
&IdStrategy::Sequential,
266306
&[("meta/priority", b"high"), ("title", b"hello")],
267307
"create",
308+
None,
268309
)
269310
.unwrap();
270311

@@ -283,6 +324,7 @@ fn delete_nested_field() {
283324
&IdStrategy::Sequential,
284325
&[("meta/priority", b"high"), ("title", b"hello")],
285326
"create",
327+
None,
286328
)
287329
.unwrap();
288330

@@ -310,6 +352,7 @@ fn delete_nested_field_removes_empty_parent() {
310352
&IdStrategy::Sequential,
311353
&[("meta/priority", b"high")],
312354
"create",
355+
None,
313356
)
314357
.unwrap();
315358

@@ -335,6 +378,7 @@ fn create_commit_oid() {
335378
&IdStrategy::CommitOid,
336379
&[("title", b"hello"), ("status", b"open")],
337380
"create record",
381+
None,
338382
)
339383
.unwrap();
340384

0 commit comments

Comments
 (0)