Skip to content

Commit cf41ad6

Browse files
Implement squash filter for list of commits
This version of the squash filter will discard all but the listed commits. The `josh-filter` command gets an option to build this list from a pattern of refs. Change: squash-list
1 parent e979b69 commit cf41ad6

File tree

8 files changed

+290
-59
lines changed

8 files changed

+290
-59
lines changed

josh-proxy/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ fn split_changes(
393393
&repo.find_commit(changes[i].1)?,
394394
&vec![&parent],
395395
&new_tree,
396+
None,
396397
)?;
397398
changes[i].1 = new_commit;
398399
new_bases.push(new_commit);

src/bin/josh-filter.rs

Lines changed: 83 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,24 @@ fn make_app() -> clap::Command {
3737
)
3838
.arg(
3939
clap::Arg::new("squash")
40+
.help("Produce a history that contains only commits pointed to by references matching the given pattern")
41+
.long("squash")
42+
)
43+
.arg(
44+
clap::Arg::new("author")
45+
.help("Author to use for commits with rewritten message")
46+
.long("author")
47+
)
48+
.arg(
49+
clap::Arg::new("email")
50+
.help("Author email to use for commits with rewritten message")
51+
.long("email")
52+
)
53+
.arg(
54+
clap::Arg::new("single")
4055
.action(clap::ArgAction::SetTrue)
41-
.help("Only output one commit, without history")
42-
.long("squash"),
56+
.help("Produce a history that contains only one single commit")
57+
.long("single"),
4358
)
4459
.arg(
4560
clap::Arg::new("discover")
@@ -138,10 +153,6 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
138153

139154
let mut filterobj = josh::filter::parse(&specstr)?;
140155

141-
if args.get_flag("squash") {
142-
filterobj = josh::filter::chain(josh::filter::parse(":SQUASH")?, filterobj);
143-
}
144-
145156
if args.get_flag("print-filter") {
146157
let filterobj = if args.get_flag("reverse") {
147158
josh::filter::invert(filterobj)?
@@ -162,6 +173,38 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
162173
let transaction = josh::cache::Transaction::new(repo, None);
163174
let repo = transaction.repo();
164175

176+
let input_ref = args.get_one::<String>("input").unwrap();
177+
178+
let mut refs = vec![];
179+
let mut ids = vec![];
180+
181+
let reference = repo.resolve_reference_from_short_name(input_ref).unwrap();
182+
let input_ref = reference.name().unwrap().to_string();
183+
refs.push((input_ref.clone(), reference.target().unwrap()));
184+
185+
if args.get_flag("single") {
186+
filterobj = josh::filter::chain(josh::filter::squash(None), filterobj);
187+
}
188+
189+
if let Some(pattern) = args.get_one::<String>("squash") {
190+
let pattern = pattern.to_string();
191+
for reference in repo.references_glob(&pattern).unwrap() {
192+
let reference = reference?;
193+
if let Some(target) = reference.target() {
194+
ids.push((target, reference.name().unwrap().to_string()));
195+
refs.push((reference.name().unwrap().to_string(), target));
196+
}
197+
}
198+
filterobj = josh::filter::chain(
199+
josh::filter::squash(Some((
200+
args.get_one::<String>("author").unwrap(),
201+
args.get_one::<String>("email").unwrap(),
202+
&ids,
203+
))),
204+
filterobj,
205+
);
206+
};
207+
165208
let odb = repo.odb()?;
166209
let mp = if args.get_flag("pack") {
167210
let mempack = odb.add_new_mempack_backend(1000)?;
@@ -188,10 +231,8 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
188231
}
189232
});
190233

191-
let input_ref = args.get_one::<String>("input").unwrap();
192-
193234
if args.get_flag("discover") {
194-
let r = repo.revparse_single(input_ref)?;
235+
let r = repo.revparse_single(&input_ref)?;
195236
let hs = josh::housekeeping::find_all_workspaces_and_subdirectories(&r.peel_to_tree()?)?;
196237
for i in hs {
197238
if i.contains(":workspace=") {
@@ -210,23 +251,10 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
210251

211252
let update_target = args.get_one::<String>("update").unwrap();
212253

213-
let src = input_ref;
214254
let target = update_target;
215255

216256
let reverse = args.get_flag("reverse");
217257

218-
let t = if reverse {
219-
"refs/JOSH_TMP".to_owned()
220-
} else {
221-
target.to_string()
222-
};
223-
let src_r = repo
224-
.revparse_ext(src)?
225-
.1
226-
.ok_or(josh::josh_error("reference not found"))?;
227-
228-
let src = src_r.name().unwrap().to_string();
229-
230258
let check_permissions = args.get_flag("check-permission");
231259
let mut permissions_filter = josh::filter::empty();
232260
if check_permissions {
@@ -264,28 +292,31 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
264292
permissions_filter = josh::filter::empty();
265293
}
266294

267-
let old_oid = if let Ok(id) = transaction.repo().refname_to_id(&t) {
295+
let old_oid = if let Ok(id) = transaction.repo().refname_to_id(&target) {
268296
id
269297
} else {
270298
git2::Oid::zero()
271299
};
272-
let mut updated_refs = josh::filter_refs(
273-
&transaction,
274-
filterobj,
275-
&[(src.clone(), src_r.target().unwrap())],
276-
permissions_filter,
277-
)?;
278-
updated_refs[0].0 = t;
279-
josh::update_refs(&transaction, &mut updated_refs, "");
280-
if args.get_one::<String>("update").map(|v| v.as_str()) != Some("FILTERED_HEAD")
281-
&& updated_refs.len() == 1
282-
&& updated_refs[0].1 == old_oid
283-
{
284-
println!(
285-
"Warning: reference {} wasn't updated",
286-
args.get_one::<String>("update").unwrap()
287-
);
300+
301+
let mut updated_refs = josh::filter_refs(&transaction, filterobj, &refs, permissions_filter)?;
302+
for i in 0..updated_refs.len() {
303+
if updated_refs[i].0 == input_ref {
304+
if reverse {
305+
updated_refs[i].0 = "refs/JOSH_TMP".to_string();
306+
} else {
307+
updated_refs[i].0 = target.to_string();
308+
}
309+
} else {
310+
updated_refs[i].0 =
311+
updated_refs[i]
312+
.0
313+
.replacen("refs/heads/", "refs/heads/filtered/", 1);
314+
updated_refs[i].0 = updated_refs[i]
315+
.0
316+
.replacen("refs/tags/", "refs/tags/filtered/", 1);
317+
}
288318
}
319+
josh::update_refs(&transaction, &mut updated_refs, "");
289320

290321
#[cfg(feature = "search")]
291322
if let Some(searchstring) = args.get_one::<String>("search") {
@@ -324,7 +355,7 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
324355
if reverse {
325356
let new = repo.revparse_single(target).unwrap().id();
326357
let old = repo.revparse_single("JOSH_TMP").unwrap().id();
327-
let unfiltered_old = repo.revparse_single(input_ref).unwrap().id();
358+
let unfiltered_old = repo.revparse_single(&input_ref).unwrap().id();
328359

329360
match josh::history::unapply_filter(
330361
&transaction,
@@ -337,7 +368,7 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
337368
&mut None,
338369
) {
339370
Ok(rewritten) => {
340-
repo.reference(&src, rewritten, true, "unapply_filter")?;
371+
repo.reference(&input_ref, rewritten, true, "unapply_filter")?;
341372
}
342373
Err(JoshError(msg)) => {
343374
println!("{}", msg);
@@ -346,6 +377,17 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
346377
}
347378
}
348379

380+
if !reverse
381+
&& args.get_one::<String>("update") != Some(&"FILTERED_HEAD".to_string())
382+
&& updated_refs.len() == 1
383+
&& updated_refs[0].1 == old_oid
384+
{
385+
println!(
386+
"Warning: reference {} wasn't updated",
387+
args.get_one::<String>("update").unwrap()
388+
);
389+
}
390+
349391
if let Some(gql_query) = args.get_one::<String>("graphql") {
350392
let context = josh::graphql::context(transaction.try_clone()?, transaction.try_clone()?);
351393
*context.allow_refs.lock()? = true;

src/cache.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ pub fn print_stats() {
3333
let name = String::from_utf8(name.to_vec()).unwrap();
3434
let t = db.open_tree(&name).unwrap();
3535
if !t.is_empty() {
36-
let name = if name.contains("SUBTRACT") || name.starts_with('_') {
37-
name.clone()
36+
let name = if let Ok(filter) = filter::parse(&name) {
37+
filter::pretty(filter, 4)
3838
} else {
39-
filter::pretty(filter::parse(&name).unwrap(), 4)
39+
name.clone()
4040
};
4141
v.push((t.len(), name));
4242
}

src/filter/mod.rs

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ pub fn empty() -> Filter {
6161
to_filter(Op::Empty)
6262
}
6363

64+
pub fn squash(ids: Option<(&str, &str, &[(git2::Oid, String)])>) -> Filter {
65+
if let Some((author, email, ids)) = ids {
66+
to_filter(Op::Squash(Some(
67+
ids.iter()
68+
.map(|(x, y)| (*x, (y.clone(), author.to_string(), email.to_string())))
69+
.collect(),
70+
)))
71+
} else {
72+
to_filter(Op::Squash(None))
73+
}
74+
}
75+
6476
fn to_filter(op: Op) -> Filter {
6577
let s = format!("{:?}", op);
6678
let f = Filter(
@@ -85,7 +97,7 @@ enum Op {
8597
Empty,
8698
Fold,
8799
Paths,
88-
Squash,
100+
Squash(Option<std::collections::HashMap<git2::Oid, (String, String, String)>>),
89101
Linear,
90102

91103
RegexReplace(regex::Regex, String),
@@ -236,7 +248,18 @@ fn spec2(op: &Op) -> String {
236248
#[cfg(feature = "search")]
237249
Op::Index => ":INDEX".to_string(),
238250
Op::Fold => ":FOLD".to_string(),
239-
Op::Squash => ":SQUASH".to_string(),
251+
Op::Squash(None) => ":SQUASH".to_string(),
252+
Op::Squash(Some(hs)) => {
253+
let mut v = hs
254+
.iter()
255+
.map(|(x, y)| format!("{}:{}:{}:{}", x, y.0, y.1, y.2))
256+
.collect::<Vec<String>>();
257+
v.sort();
258+
let s = v.join(",");
259+
let s = git2::Oid::hash_object(git2::ObjectType::Blob, s.as_bytes())
260+
.expect("hash_object filter");
261+
format!(":SQUASH={}", s)
262+
}
240263
Op::Linear => ":linear".to_string(),
241264
Op::Subdir(path) => format!(":/{}", parse::quote(&path.to_string_lossy())),
242265
Op::File(path) => format!("::{}", parse::quote(&path.to_string_lossy())),
@@ -341,8 +364,15 @@ fn apply_to_commit2(
341364
Ok(Some(git2::Oid::zero()))
342365
};
343366
}
344-
Op::Squash => {
345-
return Some(history::rewrite_commit(repo, commit, &[], &commit.tree()?)).transpose()
367+
Op::Squash(None) => {
368+
return Some(history::rewrite_commit(
369+
repo,
370+
commit,
371+
&[],
372+
&commit.tree()?,
373+
None,
374+
))
375+
.transpose()
346376
}
347377
_ => {
348378
if let Some(oid) = transaction.get(filter, commit.id()) {
@@ -354,6 +384,27 @@ fn apply_to_commit2(
354384
rs_tracing::trace_scoped!("apply_to_commit", "spec": spec(filter), "commit": commit.id().to_string());
355385

356386
let filtered_tree = match &to_op(filter) {
387+
Op::Squash(Some(ids)) => {
388+
if let Some(_) = ids.get(&commit.id()) {
389+
commit.tree()?
390+
} else {
391+
for parent in commit.parents() {
392+
return Ok(
393+
if let Some(fparent) = transaction.get(filter, parent.id()) {
394+
Some(history::drop_commit(
395+
commit,
396+
vec![fparent],
397+
transaction,
398+
filter,
399+
)?)
400+
} else {
401+
None
402+
},
403+
);
404+
}
405+
tree::empty(repo)
406+
}
407+
}
357408
Op::Linear => {
358409
let p: Vec<_> = commit.parent_ids().collect();
359410
if p.is_empty() {
@@ -370,6 +421,7 @@ fn apply_to_commit2(
370421
commit.tree()?,
371422
transaction,
372423
filter,
424+
None,
373425
))
374426
.transpose();
375427
}
@@ -452,6 +504,7 @@ fn apply_to_commit2(
452504
filtered_tree,
453505
transaction,
454506
filter,
507+
None,
455508
))
456509
.transpose();
457510
}
@@ -528,12 +581,18 @@ fn apply_to_commit2(
528581

529582
let filtered_parent_ids = some_or!(filtered_parent_ids, { return Ok(None) });
530583

584+
let message = match to_op(filter) {
585+
Op::Squash(Some(ids)) => ids.get(&commit.id()).map(|x| x.clone()),
586+
_ => None,
587+
};
588+
531589
Some(history::create_filtered_commit(
532590
commit,
533591
filtered_parent_ids,
534592
filtered_tree,
535593
transaction,
536594
filter,
595+
message,
537596
))
538597
.transpose()
539598
}
@@ -557,7 +616,8 @@ fn apply2<'a>(
557616
Op::Nop => Ok(tree),
558617
Op::Empty => return Ok(tree::empty(repo)),
559618
Op::Fold => Ok(tree),
560-
Op::Squash => Ok(tree),
619+
Op::Squash(None) => Ok(tree),
620+
Op::Squash(Some(_)) => Err(josh_error("not applicable to tree")),
561621
Op::Linear => Ok(tree),
562622

563623
Op::RegexReplace(regex, replacement) => {

src/filter/parse.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ fn make_op(args: &[&str]) -> JoshResult<Op> {
3333
Where `path` is path to the directory where workspace.josh file is located
3434
"#
3535
))),
36-
["SQUASH"] => Ok(Op::Squash),
36+
["SQUASH"] => Ok(Op::Squash(None)),
37+
["SQUASH", _ids @ ..] => Err(josh_error("SQUASH with ids can't be parsed")),
3738
["linear"] => Ok(Op::Linear),
3839
["PATHS"] => Ok(Op::Paths),
3940
#[cfg(feature = "search")]

0 commit comments

Comments
 (0)