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
509 changes: 509 additions & 0 deletions deck_prague.md

Large diffs are not rendered by default.

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
4 changes: 2 additions & 2 deletions josh-core/src/cache_notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl CacheBackend for NotesCacheBackend {
return Ok(None);
}

let key = crate::filter::as_tree(&*repo, filter)?;
let key = filter.id();

if let Ok(note) = repo.find_note(Some(&note_path(key, sequence_number)), from) {
let message = note.message().unwrap_or("");
Expand Down Expand Up @@ -88,7 +88,7 @@ impl CacheBackend for NotesCacheBackend {
return Ok(());
}

let key = crate::filter::as_tree(&*repo, filter)?;
let key = filter.id();
let signature = crate::cache::josh_commit_signature()?;

repo.note(
Expand Down
129 changes: 100 additions & 29 deletions josh-core/src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ use op::{LazyRef, Op};

pub use persist::as_tree;
pub use persist::from_tree;
pub(crate) use persist::{to_filter, to_op};

pub use opt::invert;
pub use parse::get_comments;
pub use parse::parse;

static FILTERS: LazyLock<std::sync::Mutex<std::collections::HashMap<Filter, Op>>> =
LazyLock::new(|| Default::default());
static WORKSPACES: LazyLock<std::sync::Mutex<std::collections::HashMap<git2::Oid, Filter>>> =
LazyLock::new(|| Default::default());
static ANCESTORS: LazyLock<
Expand Down Expand Up @@ -236,27 +234,6 @@ pub fn squash(ids: Option<&[(git2::Oid, Filter)]>) -> Filter {
}
}

fn to_filter(op: Op) -> Filter {
let s = format!("{:?}", op);
let f = Filter(
git2::Oid::hash_object(git2::ObjectType::Blob, s.as_bytes()).expect("hash_object filter"),
);
FILTERS.lock().unwrap().insert(f, op);
f
}

fn to_op(filter: Filter) -> Op {
if filter == sequence_number() {
return Op::Nop;
}
FILTERS
.lock()
.unwrap()
.get(&filter)
.expect("unknown filter")
.clone()
}

/// Pretty print the filter on multiple lines with initial indentation level.
/// Nested filters will be indented with additional 4 spaces per nesting level.
pub fn pretty(filter: Filter, indent: usize) -> String {
Expand Down Expand Up @@ -519,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 @@ -846,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);
}

Apply::from_commit(commit)?
.with_tree(apply(transaction, cw, Apply::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 @@ -1077,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 @@ -1400,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 @@ -1414,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 @@ -1641,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
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
Loading