Skip to content

Commit 454f1c3

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 1936ec8 commit 454f1c3

File tree

8 files changed

+244
-53
lines changed

8 files changed

+244
-53
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: 67 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,14 @@ fn make_app() -> clap::Command<'static> {
4141
)
4242
.arg(
4343
clap::Arg::new("squash")
44-
.help("Only output one commit, without history")
45-
.long("squash"),
44+
.help("Produce a history that contains only commits pointed to by references matching the given pattern")
45+
.long("squash")
46+
.takes_value(true),
47+
)
48+
.arg(
49+
clap::Arg::new("single")
50+
.help("Produce a history that contains only one single commit")
51+
.long("single"),
4652
)
4753
.arg(
4854
clap::Arg::new("discover")
@@ -152,10 +158,6 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
152158

153159
let mut filterobj = josh::filter::parse(&specstr)?;
154160

155-
if args.is_present("squash") {
156-
filterobj = josh::filter::chain(josh::filter::parse(":SQUASH")?, filterobj);
157-
}
158-
159161
if args.is_present("print-filter") {
160162
let filterobj = if args.is_present("reverse") {
161163
josh::filter::invert(filterobj)?
@@ -176,6 +178,31 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
176178
let transaction = josh::cache::Transaction::new(repo, None);
177179
let repo = transaction.repo();
178180

181+
let input_ref = args.value_of("input").unwrap();
182+
183+
let mut refs = vec![];
184+
let mut ids = vec![];
185+
186+
let reference = repo.resolve_reference_from_short_name(input_ref).unwrap();
187+
let input_ref = reference.name().unwrap().to_string();
188+
refs.push((input_ref.clone(), reference.target().unwrap()));
189+
190+
if args.is_present("single") {
191+
filterobj = josh::filter::chain(josh::filter::squash(None), filterobj);
192+
}
193+
194+
if let Some(pattern) = args.value_of("squash") {
195+
let pattern = pattern.to_string();
196+
for reference in repo.references_glob(&pattern).unwrap() {
197+
let reference = reference?;
198+
if let Some(target) = reference.target() {
199+
ids.push((target, reference.name().unwrap().to_string()));
200+
refs.push((reference.name().unwrap().to_string(), target));
201+
}
202+
}
203+
filterobj = josh::filter::chain(josh::filter::squash(Some(&ids)), filterobj);
204+
};
205+
179206
let odb = repo.odb()?;
180207
let mp = if args.is_present("pack") {
181208
let mempack = odb.add_new_mempack_backend(1000)?;
@@ -202,10 +229,8 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
202229
}
203230
});
204231

205-
let input_ref = args.value_of("input").unwrap();
206-
207232
if args.is_present("discover") {
208-
let r = repo.revparse_single(input_ref)?;
233+
let r = repo.revparse_single(&input_ref)?;
209234
let hs = josh::housekeeping::find_all_workspaces_and_subdirectories(&r.peel_to_tree()?)?;
210235
for i in hs {
211236
if i.contains(":workspace=") {
@@ -224,23 +249,10 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
224249

225250
let update_target = args.value_of("update").unwrap();
226251

227-
let src = input_ref;
228252
let target = update_target;
229253

230254
let reverse = args.is_present("reverse");
231255

232-
let t = if reverse {
233-
"refs/JOSH_TMP".to_owned()
234-
} else {
235-
target.to_string()
236-
};
237-
let src_r = repo
238-
.revparse_ext(src)?
239-
.1
240-
.ok_or(josh::josh_error("reference not found"))?;
241-
242-
let src = src_r.name().unwrap().to_string();
243-
244256
let check_permissions = args.is_present("check-permission");
245257
let mut permissions_filter = josh::filter::empty();
246258
if check_permissions {
@@ -278,28 +290,31 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
278290
permissions_filter = josh::filter::empty();
279291
}
280292

281-
let old_oid = if let Ok(id) = transaction.repo().refname_to_id(&t) {
293+
let old_oid = if let Ok(id) = transaction.repo().refname_to_id(&target) {
282294
id
283295
} else {
284296
git2::Oid::zero()
285297
};
286-
let mut updated_refs = josh::filter_refs(
287-
&transaction,
288-
filterobj,
289-
&[(src.clone(), src_r.target().unwrap())],
290-
permissions_filter,
291-
)?;
292-
updated_refs[0].0 = t;
293-
josh::update_refs(&transaction, &mut updated_refs, "");
294-
if args.value_of("update") != Some("FILTERED_HEAD")
295-
&& updated_refs.len() == 1
296-
&& updated_refs[0].1 == old_oid
297-
{
298-
println!(
299-
"Warning: reference {} wasn't updated",
300-
args.value_of("update").unwrap()
301-
);
298+
299+
let mut updated_refs = josh::filter_refs(&transaction, filterobj, &refs, permissions_filter)?;
300+
for i in 0..updated_refs.len() {
301+
if updated_refs[i].0 == input_ref {
302+
if reverse {
303+
updated_refs[i].0 = "refs/JOSH_TMP".to_string();
304+
} else {
305+
updated_refs[i].0 = target.to_string();
306+
}
307+
} else {
308+
updated_refs[i].0 =
309+
updated_refs[i]
310+
.0
311+
.replacen("refs/heads/", "refs/heads/filtered/", 1);
312+
updated_refs[i].0 = updated_refs[i]
313+
.0
314+
.replacen("refs/tags/", "refs/tags/filtered/", 1);
315+
}
302316
}
317+
josh::update_refs(&transaction, &mut updated_refs, "");
303318

304319
#[cfg(feature = "search")]
305320
if let Some(searchstring) = args.value_of("search") {
@@ -338,7 +353,7 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
338353
if reverse {
339354
let new = repo.revparse_single(target).unwrap().id();
340355
let old = repo.revparse_single("JOSH_TMP").unwrap().id();
341-
let unfiltered_old = repo.revparse_single(input_ref).unwrap().id();
356+
let unfiltered_old = repo.revparse_single(&input_ref).unwrap().id();
342357

343358
match josh::history::unapply_filter(
344359
&transaction,
@@ -351,7 +366,7 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
351366
&mut None,
352367
) {
353368
Ok(rewritten) => {
354-
repo.reference(&src, rewritten, true, "unapply_filter")?;
369+
repo.reference(&input_ref, rewritten, true, "unapply_filter")?;
355370
}
356371
Err(JoshError(msg)) => {
357372
println!("{}", msg);
@@ -360,6 +375,17 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
360375
}
361376
}
362377

378+
if !reverse
379+
&& args.value_of("update") != Some("FILTERED_HEAD")
380+
&& updated_refs.len() == 1
381+
&& updated_refs[0].1 == old_oid
382+
{
383+
println!(
384+
"Warning: reference {} wasn't updated",
385+
args.value_of("update").unwrap()
386+
);
387+
}
388+
363389
if let Some(gql_query) = args.value_of("graphql") {
364390
let context = josh::graphql::context(transaction.try_clone()?, transaction.try_clone()?);
365391
*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: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ pub fn empty() -> Filter {
6161
to_filter(Op::Empty)
6262
}
6363

64+
pub fn squash(ids: Option<&[(git2::Oid, String)]>) -> Filter {
65+
if let Some(ids) = ids {
66+
to_filter(Op::Squash(Some(
67+
ids.iter().map(|(x, y)| (*x, y.clone())).collect(),
68+
)))
69+
} else {
70+
to_filter(Op::Squash(None))
71+
}
72+
}
73+
6474
fn to_filter(op: Op) -> Filter {
6575
let s = format!("{:?}", op);
6676
let f = Filter(
@@ -85,7 +95,7 @@ enum Op {
8595
Empty,
8696
Fold,
8797
Paths,
88-
Squash,
98+
Squash(Option<std::collections::HashMap<git2::Oid, String>>),
8999
Linear,
90100

91101
RegexReplace(regex::Regex, String),
@@ -236,7 +246,18 @@ fn spec2(op: &Op) -> String {
236246
#[cfg(feature = "search")]
237247
Op::Index => ":INDEX".to_string(),
238248
Op::Fold => ":FOLD".to_string(),
239-
Op::Squash => ":SQUASH".to_string(),
249+
Op::Squash(None) => ":SQUASH".to_string(),
250+
Op::Squash(Some(hs)) => {
251+
let mut v = hs
252+
.iter()
253+
.map(|(x, y)| format!("{}:{}", x, y))
254+
.collect::<Vec<String>>();
255+
v.sort();
256+
let s = v.join(",");
257+
let s = git2::Oid::hash_object(git2::ObjectType::Blob, s.as_bytes())
258+
.expect("hash_object filter");
259+
format!(":SQUASH={}", s)
260+
}
240261
Op::Linear => ":linear".to_string(),
241262
Op::Subdir(path) => format!(":/{}", parse::quote(&path.to_string_lossy())),
242263
Op::File(path) => format!("::{}", parse::quote(&path.to_string_lossy())),
@@ -341,8 +362,15 @@ fn apply_to_commit2(
341362
Ok(Some(git2::Oid::zero()))
342363
};
343364
}
344-
Op::Squash => {
345-
return Some(history::rewrite_commit(repo, commit, &[], &commit.tree()?)).transpose()
365+
Op::Squash(None) => {
366+
return Some(history::rewrite_commit(
367+
repo,
368+
commit,
369+
&[],
370+
&commit.tree()?,
371+
None,
372+
))
373+
.transpose()
346374
}
347375
_ => {
348376
if let Some(oid) = transaction.get(filter, commit.id()) {
@@ -354,6 +382,27 @@ fn apply_to_commit2(
354382
rs_tracing::trace_scoped!("apply_to_commit", "spec": spec(filter), "commit": commit.id().to_string());
355383

356384
let filtered_tree = match &to_op(filter) {
385+
Op::Squash(Some(ids)) => {
386+
if let Some(_) = ids.get(&commit.id()) {
387+
commit.tree()?
388+
} else {
389+
for parent in commit.parents() {
390+
return Ok(
391+
if let Some(fparent) = transaction.get(filter, parent.id()) {
392+
Some(history::drop_commit(
393+
commit,
394+
vec![fparent],
395+
transaction,
396+
filter,
397+
)?)
398+
} else {
399+
None
400+
},
401+
);
402+
}
403+
tree::empty(repo)
404+
}
405+
}
357406
Op::Linear => {
358407
let p: Vec<_> = commit.parent_ids().collect();
359408
if p.is_empty() {
@@ -370,6 +419,7 @@ fn apply_to_commit2(
370419
commit.tree()?,
371420
transaction,
372421
filter,
422+
None,
373423
))
374424
.transpose();
375425
}
@@ -452,6 +502,7 @@ fn apply_to_commit2(
452502
filtered_tree,
453503
transaction,
454504
filter,
505+
None,
455506
))
456507
.transpose();
457508
}
@@ -528,12 +579,18 @@ fn apply_to_commit2(
528579

529580
let filtered_parent_ids = some_or!(filtered_parent_ids, { return Ok(None) });
530581

582+
let message = match to_op(filter) {
583+
Op::Squash(Some(ids)) => ids.get(&commit.id()).map(|x| x.clone()),
584+
_ => None,
585+
};
586+
531587
Some(history::create_filtered_commit(
532588
commit,
533589
filtered_parent_ids,
534590
filtered_tree,
535591
transaction,
536592
filter,
593+
message,
537594
))
538595
.transpose()
539596
}
@@ -557,7 +614,8 @@ fn apply2<'a>(
557614
Op::Nop => Ok(tree),
558615
Op::Empty => return Ok(tree::empty(repo)),
559616
Op::Fold => Ok(tree),
560-
Op::Squash => Ok(tree),
617+
Op::Squash(None) => Ok(tree),
618+
Op::Squash(Some(_)) => Err(josh_error("not applicable to tree")),
561619
Op::Linear => Ok(tree),
562620

563621
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)