Skip to content

Commit 3222ccd

Browse files
Extend commit message filter
This adds support to read tree entries to the commit message filter
1 parent 46e7fe8 commit 3222ccd

File tree

9 files changed

+723
-45
lines changed

9 files changed

+723
-45
lines changed

deck_prague.md

Lines changed: 509 additions & 0 deletions
Large diffs are not rendered by default.

docs/src/reference/filters.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ parents.
183183
### Commit message rewriting **`:"template"`** or **`:"template";"regex"`**
184184

185185
Rewrite commit messages using a template string. The template can use regex capture groups
186-
to extract and reformat parts of the original commit message.
186+
to extract and reformat parts of the original commit message, as well as special template variables
187+
for commit metadata.
187188

188189
**Simple message replacement:**
189190
```
@@ -200,6 +201,27 @@ which are then used in the template. The regex `(?s)^(?P<type>fix|feat|docs): (?
200201
commit messages starting with "fix:", "feat:", or "docs:" followed by a message, and the template
201202
reformats them as `[type] message`.
202203

204+
**Using template variables:**
205+
The template supports special variables that provide access to commit metadata:
206+
- `{#}` - The tree object ID (SHA-1 hash) of the commit
207+
- `{@}` - The commit object ID (SHA-1 hash)
208+
- `{/path}` - The content of the file at the specified path in the commit tree
209+
- `{#path}` - The object ID (SHA-1 hash) of the tree entry at the specified path
210+
211+
Regex capture groups take priority over template variables. If a regex capture group has the same name as a template variable, the capture group value will be used.
212+
213+
Example:
214+
```
215+
:"Message: {#} {@}"
216+
```
217+
This replaces commit messages with "Message: " followed by the tree ID and commit ID.
218+
219+
**Combining regex capture groups and template variables:**
220+
```
221+
:"[{type}] {message} (commit: {@})";"(?s)^(?P<type>Original) (?P<message>.+)$"
222+
```
223+
This combines regex capture groups (`{type}` and `{message}`) with template variables (`{@}` for the commit ID).
224+
203225
**Removing text from messages:**
204226
```
205227
:"";"TODO"

josh-core/src/filter/mod.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,7 +1077,7 @@ fn apply_to_commit2(
10771077
for (root, _link_file) in v {
10781078
let embeding = some_or!(
10791079
apply_to_commit2(
1080-
&Op::Chain(message("{commit}"), file(root.join(".josh-link.toml"))),
1080+
&Op::Chain(message("{@}"), file(root.join(".josh-link.toml"))),
10811081
&commit,
10821082
transaction
10831083
)?,
@@ -1400,9 +1400,6 @@ fn apply2<'a>(
14001400
let tree_id = x.tree().id().to_string();
14011401
let commit = x.commit;
14021402
let commit_id = commit.to_string();
1403-
let mut hm = std::collections::HashMap::<String, String>::new();
1404-
hm.insert("tree".to_string(), tree_id);
1405-
hm.insert("commit".to_string(), commit_id);
14061403

14071404
let message = if let Some(ref m) = x.message {
14081405
m.to_string()
@@ -1414,7 +1411,29 @@ fn apply2<'a>(
14141411
}
14151412
};
14161413

1417-
Ok(x.with_message(text::transform_with_template(&r, &m, &message, &hm)?))
1414+
let tree = x.tree().clone();
1415+
Ok(x.with_message(text::transform_with_template(
1416+
&r,
1417+
&m,
1418+
&message,
1419+
|key: &str| -> Option<String> {
1420+
match key {
1421+
"#" => Some(tree_id.clone()),
1422+
"@" => Some(commit_id.clone()),
1423+
key if key.starts_with("/") => {
1424+
Some(tree::get_blob(repo, &tree, std::path::Path::new(&key[1..])))
1425+
}
1426+
1427+
key if key.starts_with("#") => Some(
1428+
tree.get_path(std::path::Path::new(&key[1..]))
1429+
.map(|e| e.id())
1430+
.unwrap_or(git2::Oid::zero())
1431+
.to_string(),
1432+
),
1433+
_ => None,
1434+
}
1435+
},
1436+
)?))
14181437
}
14191438
Op::HistoryConcat(..) => Ok(x),
14201439
Op::Linear => Ok(x),

josh-core/src/filter/text.rs

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,61 @@ use crate::JoshResult;
22
use regex::Regex;
33
use std::cell::RefCell;
44
use std::collections::HashMap;
5+
use std::fmt::Write;
56

6-
pub fn transform_with_template(
7+
pub fn transform_with_template<F>(
78
re: &Regex,
89
template: &str,
910
input: &str,
10-
globals: &HashMap<String, String>,
11-
) -> JoshResult<String> {
11+
globals: F,
12+
) -> JoshResult<String>
13+
where
14+
F: Fn(&str) -> Option<String>,
15+
{
1216
let first_error: RefCell<Option<crate::JoshError>> = RefCell::new(None);
1317

1418
let result = re
1519
.replace_all(input, |caps: &regex::Captures| {
16-
// Build a HashMap with all named captures and globals
17-
// We need to store the string values to keep them alive for the HashMap references
18-
let mut string_storage: HashMap<String, String> = HashMap::new();
19-
2020
// Collect all named capture values
21+
let mut string_storage: HashMap<String, String> = HashMap::new();
2122
for name in re.capture_names().flatten() {
2223
if let Some(m) = caps.name(name) {
2324
string_storage.insert(name.to_string(), m.as_str().to_string());
2425
}
2526
}
2627

27-
// Build the HashMap for strfmt with references to the stored strings
28-
let mut vars: HashMap<String, &dyn strfmt::DisplayStr> = HashMap::new();
28+
// Use strfmt_map which calls our function for each key it needs
29+
match strfmt::strfmt_map(
30+
template,
31+
|mut fmt: strfmt::Formatter| -> Result<(), strfmt::FmtError> {
32+
let key = fmt.key;
2933

30-
// Add all globals first (lower priority)
31-
for (key, value) in globals {
32-
vars.insert(key.clone(), value as &dyn strfmt::DisplayStr);
33-
}
34+
// First check named captures (higher priority)
35+
if let Some(value) = string_storage.get(key) {
36+
write!(fmt, "{}", value).map_err(|_| {
37+
strfmt::FmtError::Invalid(format!(
38+
"failed to write value for key: {}",
39+
key
40+
))
41+
})?;
42+
return Ok(());
43+
}
3444

35-
// Add all named captures (higher priority - will overwrite globals if there's a conflict)
36-
for (key, value) in &string_storage {
37-
vars.insert(key.clone(), value as &dyn strfmt::DisplayStr);
38-
}
45+
// Then call globals function (lower priority)
46+
if let Some(global_value) = globals(key) {
47+
write!(fmt, "{}", global_value).map_err(|_| {
48+
strfmt::FmtError::Invalid(format!(
49+
"failed to write global value for key: {}",
50+
key
51+
))
52+
})?;
53+
return Ok(());
54+
}
3955

40-
// Format the template, propagating errors
41-
match strfmt::strfmt(template, &vars) {
56+
// Key not found - skip it (strfmt will leave the placeholder)
57+
fmt.skip()
58+
},
59+
) {
4260
Ok(s) => s,
4361
Err(e) => {
4462
let mut error = first_error.borrow_mut();

josh-core/src/history.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ pub fn unapply_filter(
609609
&regex::Regex::new("(?m)^Change: [^ ]+")?,
610610
&"",
611611
module_commit.message_raw().unwrap(),
612-
&std::collections::HashMap::new(),
612+
|_key: &str| -> Option<String> { None },
613613
)?;
614614
apply = apply.with_message(new_message);
615615
}

tests/experimental/link-submodules.t

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Test Adapt filter - should expand submodule into actual tree content
6464
[1] :embed=libs
6565
[2] ::libs/.josh-link.toml
6666
[2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs)
67-
[3] :"{commit}"
67+
[3] :"{@}"
6868
[3] :adapt=submodules
6969
[3] :link=embedded
7070
[10] sequence_number
@@ -145,7 +145,7 @@ Test Adapt with multiple submodules
145145
[1] :embed=libs
146146
[2] ::libs/.josh-link.toml
147147
[2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs)
148-
[3] :"{commit}"
148+
[3] :"{@}"
149149
[3] :link=embedded
150150
[4] :adapt=submodules
151151
[11] sequence_number
@@ -165,7 +165,7 @@ Test Adapt with multiple submodules
165165
[2] ::libs/.josh-link.toml
166166
[2] ::modules/another/.josh-link.toml
167167
[2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs)
168-
[4] :"{commit}"
168+
[4] :"{@}"
169169
[4] :adapt=submodules
170170
[4] :link=embedded
171171
[15] sequence_number
@@ -247,7 +247,7 @@ Test Adapt with submodule changes - add commits to submodule and update
247247
[2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs)
248248
[3] ::libs/.josh-link.toml
249249
[4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs)
250-
[5] :"{commit}"
250+
[5] :"{@}"
251251
[5] :adapt=submodules
252252
[5] :link=embedded
253253
[21] sequence_number
@@ -309,7 +309,7 @@ Test Adapt with submodule changes - add commits to submodule and update
309309
[2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs)
310310
[3] ::libs/.josh-link.toml
311311
[4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs)
312-
[5] :"{commit}"
312+
[5] :"{@}"
313313
[5] :adapt=submodules
314314
[5] :link=embedded
315315
[9] :/libs
@@ -335,7 +335,7 @@ Test Adapt with submodule changes - add commits to submodule and update
335335
[2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs)
336336
[3] ::libs/.josh-link.toml
337337
[4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs)
338-
[5] :"{commit}"
338+
[5] :"{@}"
339339
[5] :adapt=submodules
340340
[5] :link=embedded
341341
[7] :prune=trivial-merge
@@ -360,7 +360,7 @@ Test Adapt with submodule changes - add commits to submodule and update
360360
[2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs)
361361
[3] ::libs/.josh-link.toml
362362
[4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs)
363-
[5] :"{commit}"
363+
[5] :"{@}"
364364
[5] :adapt=submodules
365365
[5] :link=embedded
366366
[7] :export
@@ -380,7 +380,7 @@ Test Adapt with submodule changes - add commits to submodule and update
380380
[2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs)
381381
[3] ::libs/.josh-link.toml
382382
[4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs)
383-
[5] :"{commit}"
383+
[5] :"{@}"
384384
[5] :adapt=submodules
385385
[5] :link=embedded
386386
[7] :export
@@ -401,7 +401,7 @@ Test Adapt with submodule changes - add commits to submodule and update
401401
[3] ::libs/.josh-link.toml
402402
[4] :/another
403403
[4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs)
404-
[5] :"{commit}"
404+
[5] :"{@}"
405405
[5] :adapt=submodules
406406
[5] :link=embedded
407407
[7] :/modules
@@ -425,7 +425,7 @@ Test Adapt with submodule changes - add commits to submodule and update
425425
[3] ::libs/.josh-link.toml
426426
[4] :/another
427427
[4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs)
428-
[5] :"{commit}"
428+
[5] :"{@}"
429429
[5] :adapt=submodules
430430
[5] :link=embedded
431431
[7] :/modules
@@ -465,7 +465,7 @@ Test Adapt with submodule changes - add commits to submodule and update
465465
[3] ::libs/.josh-link.toml
466466
[4] :/another
467467
[4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs)
468-
[5] :"{commit}"
468+
[5] :"{@}"
469469
[5] :adapt=submodules
470470
[5] :link=embedded
471471
[7] :/modules
@@ -503,7 +503,7 @@ Test Adapt with submodule changes - add commits to submodule and update
503503
[4] :/another
504504
[4] :prefix=libs
505505
[4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs)
506-
[5] :"{commit}"
506+
[5] :"{@}"
507507
[5] :adapt=submodules
508508
[5] :link=embedded
509509
[7] :/modules

tests/experimental/link.t

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Test Link filter (identical to Adapt)
3333
$ josh-filter -s :adapt=submodules:link master --update refs/josh/filter/master
3434
[1] :embed=libs
3535
[1] :unapply(a1520c70819abcbe295fe431e4b88cf56f5a0c95:/libs)
36-
[2] :"{commit}"
36+
[2] :"{@}"
3737
[2] ::libs/.josh-link.toml
3838
[2] :adapt=submodules
3939
[2] :link=embedded

tests/filter/message.t

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,82 @@ Test that message rewriting works
3434

3535
$ echo contents1 > file1
3636
$ git add file1
37-
$ git commit -m "commit with {tree} and {commit}" 1> /dev/null
37+
$ git commit -m "commit with {#} and {@}" 1> /dev/null
3838
3939
Test that message rewriting with template variables works
40-
$ josh-filter ':"Message: {tree} {commit}"' --update refs/josh/filter/master master
41-
025b01893026c240e56c95e6e8f1659aa417581e
40+
$ josh-filter ':"Message: {#} {@}"' --update refs/josh/filter/master master
41+
1d858b36701f0d673e34f0f601a048b9c9c8d114
4242
$ git log --pretty=%s josh/filter/master
43-
Message: 3d77ff51363c9825cc2a221fc0ba5a883a1a2c72 8e125b48e2286c74bf9be1bbb8d3034a7370eebc
43+
Message: 3d77ff51363c9825cc2a221fc0ba5a883a1a2c72 2c0be119f4925350c097c9e206dfa6353158bba3
4444
$ git cat-file commit josh/filter/master | grep -A 1 "^$"
4545
46-
Message: 3d77ff51363c9825cc2a221fc0ba5a883a1a2c72 8e125b48e2286c74bf9be1bbb8d3034a7370eebc
46+
Message: 3d77ff51363c9825cc2a221fc0ba5a883a1a2c72 2c0be119f4925350c097c9e206dfa6353158bba3
47+
48+
$ cd ${TESTTMP}
49+
$ git init -q testrepo3 1> /dev/null
50+
$ cd testrepo3
51+
52+
$ echo "file content" > file1
53+
$ mkdir -p subdir
54+
$ echo "nested content" > subdir/file2
55+
$ git add file1 subdir/file2
56+
$ git commit -m "initial commit" 1> /dev/null
57+
58+
Test that message rewriting with file content template variable works
59+
$ josh-filter ':"File content: {/file1}"' --update refs/josh/filter/master master
60+
cd7b44dc763fe78dc0b759398e689e54aa131eb5
61+
$ git log --pretty=%s josh/filter/master
62+
File content: file content
63+
$ git cat-file commit josh/filter/master | grep -A 1 "^$"
64+
65+
File content: file content
66+
67+
Test that message rewriting with nested file path works
68+
$ josh-filter ':"Nested: {/subdir/file2}"' --update refs/josh/filter/master master
69+
23f3df907d06d6269adfc749e57b0c2974d66181
70+
$ git log --pretty=%s josh/filter/master
71+
Nested: nested content
72+
$ git cat-file commit josh/filter/master | grep -A 1 "^$"
73+
74+
Nested: nested content
75+
76+
Test that message rewriting with tree entry OID works
77+
$ josh-filter ':"File OID: {#file1}"' --update refs/josh/filter/master master
78+
f90332f7fe886418042703808cca42bf1e33af7c
79+
$ git log --pretty=%s josh/filter/master | head -1
80+
File OID: * (glob)
81+
$ git cat-file commit josh/filter/master | grep -A 1 "^$" | head -1
82+
83+
84+
Test that message rewriting with nested tree entry OID works
85+
$ josh-filter ':"Nested OID: {#subdir/file2}"' --update refs/josh/filter/master master
86+
7c6a0f3f4866f824e3d88a7d3277f85d2c1c62f5
87+
$ git log --pretty=%s josh/filter/master | head -1
88+
Nested OID: * (glob)
89+
$ git cat-file commit josh/filter/master | grep -A 1 "^$" | head -1
90+
91+
92+
Test that non-existent file path returns empty content
93+
$ josh-filter ':"Missing: [{/nonexistent}]"' --update refs/josh/filter/master master
94+
8bf5b583555dd6c4765f3c34515de7e6c79813ac
95+
$ git log --pretty=%s josh/filter/master | head -1
96+
Missing: []
97+
$ git cat-file commit josh/filter/master | grep -A 1 "^$" | head -1
98+
99+
100+
Test that non-existent tree entry returns zero OID
101+
$ josh-filter ':"Missing OID: {#nonexistent}"' --update refs/josh/filter/master master
102+
f63a6621696edc2b9ccec9a2ccd042af6276b081
103+
$ git log --pretty=%s josh/filter/master | head -1
104+
Missing OID: 0000000000000000000000000000000000000000
105+
$ git cat-file commit josh/filter/master | grep -A 1 "^$" | head -1
106+
107+
108+
Test combining multiple template variables
109+
$ josh-filter ':"Tree: {#}, Commit: {@}, File: {/file1}, OID: {#file1}"' --update refs/josh/filter/master master
110+
5be71b6c02eb9a6aa6c1d4cd1fb2b682d732a940
111+
$ git log --pretty=%s josh/filter/master | head -1
112+
Tree: * (glob)
113+
$ git cat-file commit josh/filter/master | grep -A 1 "^$" | head -1
114+
47115

0 commit comments

Comments
 (0)