WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion docs/src/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ parents.
### Commit message rewriting **`:"template"`** or **`:"template";"regex"`**

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

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

**Using template variables:**
The template supports special variables that provide access to commit metadata:
- `{#}` - The tree object ID (SHA-1 hash) of the commit
- `{@}` - The commit object ID (SHA-1 hash)
- `{/path}` - The content of the file at the specified path in the commit tree
- `{#path}` - The object ID (SHA-1 hash) of the tree entry at the specified path

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.

Example:
```
:"Message: {#} {@}"
```
This replaces commit messages with "Message: " followed by the tree ID and commit ID.

**Combining regex capture groups and template variables:**
```
:"[{type}] {message} (commit: {@})";"(?s)^(?P<type>Original) (?P<message>.+)$"
```
This combines regex capture groups (`{type}` and `{message}`) with template variables (`{@}` for the commit ID).

**Removing text from messages:**
```
:"";"TODO"
Expand Down
104 changes: 99 additions & 5 deletions josh-core/src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,14 @@ fn spec2(op: &Op) -> String {
Op::Workspace(path) => {
format!(":workspace={}", parse::quote_if(&path.to_string_lossy()))
}
#[cfg(feature = "incubating")]
Op::Lookup(path) => {
format!(":lookup={}", parse::quote_if(&path.to_string_lossy()))
}
#[cfg(feature = "incubating")]
Op::Lookup2(oid) => {
format!(":lookup2={}", oid.to_string())
}
Op::Stored(path) => {
format!(":+{}", parse::quote_if(&path.to_string_lossy()))
}
Expand Down Expand Up @@ -823,6 +831,71 @@ fn apply_to_commit2(

apply(transaction, nf, Rewrite::from_commit(commit)?)?
}
#[cfg(feature = "incubating")]
Op::Lookup(lookup_path) => {
let lookup_commit = if let Some(lookup_commit) =
apply_to_commit2(&Op::Subdir(lookup_path.clone()), &commit, transaction)?
{
lookup_commit
} else {
return Ok(None);
};

let op = Op::Lookup2(lookup_commit);

if let Some(start) = transaction.get(to_filter(op), commit.id()) {
transaction.insert(filter, commit.id(), start, true);
return Ok(Some(start));
} else {
return Ok(None);
}
}

#[cfg(feature = "incubating")]
Op::Lookup2(lookup_commit_id) => {
let lookup_commit = repo.find_commit(*lookup_commit_id)?;
for parent in lookup_commit.parents() {
let lookup_tree = lookup_commit.tree_id();
let cw = get_filter(
transaction,
&repo.find_tree(lookup_tree)?,
&std::path::PathBuf::new().join(commit.id().to_string()),
);
if cw != filter::empty() {
if let Some(start) =
apply_to_commit2(&Op::Lookup2(parent.id()), &commit, transaction)?
{
transaction.insert(filter, commit.id(), start, true);
return Ok(Some(start));
} else {
return Ok(None);
}
}
break;
}
let lookup_tree = lookup_commit.tree_id();
let cw = get_filter(
transaction,
&repo.find_tree(lookup_tree)?,
&std::path::PathBuf::new().join(commit.id().to_string()),
);

if cw == filter::empty() {
// FIXME empty filter or no entry in table?
for parent in commit.parents() {
if let Some(start) = apply_to_commit2(&op, &parent, transaction)? {
transaction.insert(filter, commit.id(), start, true);
return Ok(Some(start));
} else {
return Ok(None);
}
}
return Ok(None);
}

Rewrite::from_commit(commit)?
.with_tree(apply(transaction, cw, Rewrite::from_commit(commit)?)?.into_tree())
}
Op::Squash(Some(ids)) => {
if let Some(sq) = ids.get(&LazyRef::Resolved(commit.id())) {
let oid = if let Some(oid) =
Expand Down Expand Up @@ -1054,7 +1127,7 @@ fn apply_to_commit2(
for (root, _link_file) in v {
let embeding = some_or!(
apply_to_commit2(
&Op::Chain(message("{commit}"), file(root.join(".josh-link.toml"))),
&Op::Chain(message("{@}"), file(root.join(".josh-link.toml"))),
&commit,
transaction
)?,
Expand Down Expand Up @@ -1377,9 +1450,6 @@ fn apply2<'a>(
let tree_id = x.tree().id().to_string();
let commit = x.commit;
let commit_id = commit.to_string();
let mut hm = std::collections::HashMap::<String, String>::new();
hm.insert("tree".to_string(), tree_id);
hm.insert("commit".to_string(), commit_id);

let message = if let Some(ref m) = x.message {
m.to_string()
Expand All @@ -1391,7 +1461,29 @@ fn apply2<'a>(
}
};

Ok(x.with_message(text::transform_with_template(&r, &m, &message, &hm)?))
let tree = x.tree().clone();
Ok(x.with_message(text::transform_with_template(
&r,
&m,
&message,
|key: &str| -> Option<String> {
match key {
"#" => Some(tree_id.clone()),
"@" => Some(commit_id.clone()),
key if key.starts_with("/") => {
Some(tree::get_blob(repo, &tree, std::path::Path::new(&key[1..])))
}

key if key.starts_with("#") => Some(
tree.get_path(std::path::Path::new(&key[1..]))
.map(|e| e.id())
.unwrap_or(git2::Oid::zero())
.to_string(),
),
_ => None,
}
},
)?))
}
Op::HistoryConcat(..) => Ok(x),
Op::Linear => Ok(x),
Expand Down Expand Up @@ -1618,6 +1710,8 @@ fn apply2<'a>(
}
}
Op::Pin(_) => Ok(x),
#[cfg(feature = "incubating")]
Op::Lookup(_) | Op::Lookup2(_) => Err(josh_error("not applicable to tree")),
}
}

Expand Down
4 changes: 4 additions & 0 deletions josh-core/src/filter/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ pub enum Op {
Prefix(std::path::PathBuf),
Subdir(std::path::PathBuf),
Workspace(std::path::PathBuf),
#[cfg(feature = "incubating")]
Lookup(std::path::PathBuf),
#[cfg(feature = "incubating")]
Lookup2(git2::Oid),
Stored(std::path::PathBuf),

Pattern(String),
Expand Down
2 changes: 2 additions & 0 deletions josh-core/src/filter/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ fn make_op(args: &[&str]) -> JoshResult<Op> {
["author", author, email] => Ok(Op::Author(author.to_string(), email.to_string())),
["committer", author, email] => Ok(Op::Committer(author.to_string(), email.to_string())),
["workspace", arg] => Ok(Op::Workspace(Path::new(arg).to_owned())),
#[cfg(feature = "incubating")]
["lookup", arg] => Ok(Op::Lookup(Path::new(arg).to_owned())),
["prefix"] => Err(josh_error(indoc!(
r#"
Filter ":prefix" requires an argument.
Expand Down
36 changes: 36 additions & 0 deletions josh-core/src/filter/persist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,16 @@ impl InMemoryBuilder {
let params_tree = self.build_str_params(&[hook.as_ref()]);
push_tree_entries(&mut entries, [("hook", params_tree)]);
}
#[cfg(feature = "incubating")]
Op::Lookup(path) => {
let params_tree = self.build_str_params(&[path.to_string_lossy().as_ref()]);
push_tree_entries(&mut entries, [("lookup", params_tree)]);
}
#[cfg(feature = "incubating")]
Op::Lookup2(oid) => {
let params_tree = self.build_str_params(&[oid.to_string().as_ref()]);
push_tree_entries(&mut entries, [("lookup2", params_tree)]);
}
}

let tree = gix_object::Tree { entries };
Expand Down Expand Up @@ -640,6 +650,32 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
let path = std::str::from_utf8(path_blob.content())?;
Ok(Op::Stored(std::path::PathBuf::from(path)))
}
#[cfg(feature = "incubating")]
"lookup" => {
let inner = repo.find_tree(entry.id())?;
let path_blob = repo.find_blob(
inner
.get_name("0")
.ok_or_else(|| josh_error("lookup: missing path"))?
.id(),
)?;
let path = std::str::from_utf8(path_blob.content())?;
Ok(Op::Lookup(std::path::PathBuf::from(path)))
}
#[cfg(feature = "incubating")]
"lookup2" => {
let inner = repo.find_tree(entry.id())?;
let oid_blob = repo.find_blob(
inner
.get_name("0")
.ok_or_else(|| josh_error("lookup2: missing oid"))?
.id(),
)?;
let oid_str = std::str::from_utf8(oid_blob.content())?;
let oid = git2::Oid::from_str(oid_str)
.map_err(|e| josh_error(&format!("lookup2: invalid oid: {}", e)))?;
Ok(Op::Lookup2(oid))
}
"compose" => {
let compose_tree = repo.find_tree(entry.id())?;
let mut filters = Vec::new();
Expand Down
56 changes: 37 additions & 19 deletions josh-core/src/filter/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,61 @@ use crate::JoshResult;
use regex::Regex;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::Write;

pub fn transform_with_template(
pub fn transform_with_template<F>(
re: &Regex,
template: &str,
input: &str,
globals: &HashMap<String, String>,
) -> JoshResult<String> {
globals: F,
) -> JoshResult<String>
where
F: Fn(&str) -> Option<String>,
{
let first_error: RefCell<Option<crate::JoshError>> = RefCell::new(None);

let result = re
.replace_all(input, |caps: &regex::Captures| {
// Build a HashMap with all named captures and globals
// We need to store the string values to keep them alive for the HashMap references
let mut string_storage: HashMap<String, String> = HashMap::new();

// Collect all named capture values
let mut string_storage: HashMap<String, String> = HashMap::new();
for name in re.capture_names().flatten() {
if let Some(m) = caps.name(name) {
string_storage.insert(name.to_string(), m.as_str().to_string());
}
}

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

// Add all globals first (lower priority)
for (key, value) in globals {
vars.insert(key.clone(), value as &dyn strfmt::DisplayStr);
}
// First check named captures (higher priority)
if let Some(value) = string_storage.get(key) {
write!(fmt, "{}", value).map_err(|_| {
strfmt::FmtError::Invalid(format!(
"failed to write value for key: {}",
key
))
})?;
return Ok(());
}

// Add all named captures (higher priority - will overwrite globals if there's a conflict)
for (key, value) in &string_storage {
vars.insert(key.clone(), value as &dyn strfmt::DisplayStr);
}
// Then call globals function (lower priority)
if let Some(global_value) = globals(key) {
write!(fmt, "{}", global_value).map_err(|_| {
strfmt::FmtError::Invalid(format!(
"failed to write global value for key: {}",
key
))
})?;
return Ok(());
}

// Format the template, propagating errors
match strfmt::strfmt(template, &vars) {
// Key not found - skip it (strfmt will leave the placeholder)
fmt.skip()
},
) {
Ok(s) => s,
Err(e) => {
let mut error = first_error.borrow_mut();
Expand Down
2 changes: 1 addition & 1 deletion josh-core/src/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ pub fn unapply_filter(
&regex::Regex::new("(?m)^Change: [^ ]+")?,
&"",
module_commit.message_raw().unwrap(),
&std::collections::HashMap::new(),
|_key: &str| -> Option<String> { None },
)?;
apply = apply.with_message(new_message);
}
Expand Down
Loading