@@ -190,51 +190,44 @@ impl InMemoryBuilder {
190190 push_tree_entries ( & mut entries, [ ( "chain" , params_tree) ] ) ;
191191 }
192192 Op :: Exclude ( b) => {
193- let child = self . build_filter ( * b ) ?;
194- push_tree_entries ( & mut entries, [ ( "exclude" , child ) ] ) ;
193+ let params_tree = self . build_filter_params ( & [ * b ] ) ?;
194+ push_tree_entries ( & mut entries, [ ( "exclude" , params_tree ) ] ) ;
195195 }
196196 Op :: Pin ( b) => {
197- let child = self . build_filter ( * b ) ?;
198- push_tree_entries ( & mut entries, [ ( "pin" , child ) ] ) ;
197+ let params_tree = self . build_filter_params ( & [ * b ] ) ?;
198+ push_tree_entries ( & mut entries, [ ( "pin" , params_tree ) ] ) ;
199199 }
200200 Op :: Subdir ( path) => {
201- let blob = self . write_blob ( path. to_string_lossy ( ) . as_bytes ( ) ) ;
202- push_blob_entries ( & mut entries, [ ( "subdir" , blob ) ] ) ;
201+ let params_tree = self . build_str_params ( & [ path. to_string_lossy ( ) . as_ref ( ) ] ) ;
202+ push_tree_entries ( & mut entries, [ ( "subdir" , params_tree ) ] ) ;
203203 }
204204 Op :: Prefix ( path) => {
205- let blob = self . write_blob ( path. to_string_lossy ( ) . as_bytes ( ) ) ;
206- push_blob_entries ( & mut entries, [ ( "prefix" , blob ) ] ) ;
205+ let params_tree = self . build_str_params ( & [ path. to_string_lossy ( ) . as_ref ( ) ] ) ;
206+ push_tree_entries ( & mut entries, [ ( "prefix" , params_tree ) ] ) ;
207207 }
208208 Op :: File ( dest_path, source_path) => {
209- if source_path == dest_path {
210- // Backward compatibility: use blob format when source and dest are the same
211- let blob = self . write_blob ( dest_path. to_string_lossy ( ) . as_bytes ( ) ) ;
212- push_blob_entries ( & mut entries, [ ( "file" , blob) ] ) ;
213- } else {
214- // New format: use tree format when source and dest differ
215- // Store as (dest_path, source_path) to match enum order
216- let params_tree = self . build_str_params ( & [
217- dest_path. to_string_lossy ( ) . as_ref ( ) ,
218- source_path. to_string_lossy ( ) . as_ref ( ) ,
219- ] ) ;
220- push_tree_entries ( & mut entries, [ ( "file" , params_tree) ] ) ;
221- }
209+ // Store as (dest_path, source_path) to match enum order
210+ let params_tree = self . build_str_params ( & [
211+ dest_path. to_string_lossy ( ) . as_ref ( ) ,
212+ source_path. to_string_lossy ( ) . as_ref ( ) ,
213+ ] ) ;
214+ push_tree_entries ( & mut entries, [ ( "file" , params_tree) ] ) ;
222215 }
223216 Op :: Embed ( path) => {
224- let blob = self . write_blob ( path. to_string_lossy ( ) . as_bytes ( ) ) ;
225- push_blob_entries ( & mut entries, [ ( "embed" , blob ) ] ) ;
217+ let params_tree = self . build_str_params ( & [ path. to_string_lossy ( ) . as_ref ( ) ] ) ;
218+ push_tree_entries ( & mut entries, [ ( "embed" , params_tree ) ] ) ;
226219 }
227220 Op :: Pattern ( pattern) => {
228- let blob = self . write_blob ( pattern. as_bytes ( ) ) ;
229- push_blob_entries ( & mut entries, [ ( "pattern" , blob ) ] ) ;
221+ let params_tree = self . build_str_params ( & [ pattern. as_ref ( ) ] ) ;
222+ push_tree_entries ( & mut entries, [ ( "pattern" , params_tree ) ] ) ;
230223 }
231224 Op :: Workspace ( path) => {
232- let blob = self . write_blob ( path. to_string_lossy ( ) . as_bytes ( ) ) ;
233- push_blob_entries ( & mut entries, [ ( "workspace" , blob ) ] ) ;
225+ let params_tree = self . build_str_params ( & [ path. to_string_lossy ( ) . as_ref ( ) ] ) ;
226+ push_tree_entries ( & mut entries, [ ( "workspace" , params_tree ) ] ) ;
234227 }
235228 Op :: Stored ( path) => {
236- let blob = self . write_blob ( path. to_string_lossy ( ) . as_bytes ( ) ) ;
237- push_blob_entries ( & mut entries, [ ( "stored" , blob ) ] ) ;
229+ let params_tree = self . build_str_params ( & [ path. to_string_lossy ( ) . as_ref ( ) ] ) ;
230+ push_tree_entries ( & mut entries, [ ( "stored" , params_tree ) ] ) ;
238231 }
239232 Op :: Nop => {
240233 let blob = self . write_blob ( b"" ) ;
@@ -253,12 +246,12 @@ impl InMemoryBuilder {
253246 push_blob_entries ( & mut entries, [ ( "paths" , blob) ] ) ;
254247 }
255248 Op :: Link ( mode) => {
256- let blob = self . write_blob ( mode. as_bytes ( ) ) ;
257- push_blob_entries ( & mut entries, [ ( "link" , blob ) ] ) ;
249+ let params_tree = self . build_str_params ( & [ mode. as_ref ( ) ] ) ;
250+ push_tree_entries ( & mut entries, [ ( "link" , params_tree ) ] ) ;
258251 }
259252 Op :: Adapt ( mode) => {
260- let blob = self . write_blob ( mode. as_bytes ( ) ) ;
261- push_blob_entries ( & mut entries, [ ( "adapt" , blob ) ] ) ;
253+ let params_tree = self . build_str_params ( & [ mode. as_ref ( ) ] ) ;
254+ push_tree_entries ( & mut entries, [ ( "adapt" , params_tree ) ] ) ;
262255 }
263256 Op :: Unlink => {
264257 let blob = self . write_blob ( b"" ) ;
@@ -323,8 +316,8 @@ impl InMemoryBuilder {
323316 push_tree_entries ( & mut entries, [ ( "regex_replace" , params_tree) ] ) ;
324317 }
325318 Op :: Hook ( hook) => {
326- let blob = self . write_blob ( hook. as_bytes ( ) ) ;
327- push_blob_entries ( & mut entries, [ ( "hook" , blob ) ] ) ;
319+ let params_tree = self . build_str_params ( & [ hook. as_ref ( ) ] ) ;
320+ push_tree_entries ( & mut entries, [ ( "hook" , params_tree ) ] ) ;
328321 }
329322 }
330323
@@ -392,12 +385,28 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
392385 Ok ( Op :: Export )
393386 }
394387 "link" => {
395- let blob = repo. find_blob ( entry. id ( ) ) ?;
396- Ok ( Op :: Link ( std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ) )
388+ let inner = repo. find_tree ( entry. id ( ) ) ?;
389+ let mode_blob = repo. find_blob (
390+ inner
391+ . get_name ( "0" )
392+ . ok_or_else ( || josh_error ( "link: missing mode" ) ) ?
393+ . id ( ) ,
394+ ) ?;
395+ Ok ( Op :: Link (
396+ std:: str:: from_utf8 ( mode_blob. content ( ) ) ?. to_string ( ) ,
397+ ) )
397398 }
398399 "adapt" => {
399- let blob = repo. find_blob ( entry. id ( ) ) ?;
400- Ok ( Op :: Adapt ( std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ) )
400+ let inner = repo. find_tree ( entry. id ( ) ) ?;
401+ let mode_blob = repo. find_blob (
402+ inner
403+ . get_name ( "0" )
404+ . ok_or_else ( || josh_error ( "adapt: missing mode" ) ) ?
405+ . id ( ) ,
406+ ) ?;
407+ Ok ( Op :: Adapt (
408+ std:: str:: from_utf8 ( mode_blob. content ( ) ) ?. to_string ( ) ,
409+ ) )
401410 }
402411 "unlink" => {
403412 let _ = repo. find_blob ( entry. id ( ) ) ?;
@@ -433,8 +442,14 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
433442 }
434443 }
435444 "hook" => {
436- let blob = repo. find_blob ( entry. id ( ) ) ?;
437- let hook_name = std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ;
445+ let inner = repo. find_tree ( entry. id ( ) ) ?;
446+ let hook_blob = repo. find_blob (
447+ inner
448+ . get_name ( "0" )
449+ . ok_or_else ( || josh_error ( "hook: missing hook name" ) ) ?
450+ . id ( ) ,
451+ ) ?;
452+ let hook_name = std:: str:: from_utf8 ( hook_blob. content ( ) ) ?. to_string ( ) ;
438453 Ok ( Op :: Hook ( hook_name) )
439454 }
440455 "author" => {
@@ -494,66 +509,90 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
494509 Ok ( Op :: Message ( fmt, regex) )
495510 }
496511 "subdir" => {
497- let blob = repo. find_blob ( entry. id ( ) ) ?;
498- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
512+ let inner = repo. find_tree ( entry. id ( ) ) ?;
513+ let path_blob = repo. find_blob (
514+ inner
515+ . get_name ( "0" )
516+ . ok_or_else ( || josh_error ( "subdir: missing path" ) ) ?
517+ . id ( ) ,
518+ ) ?;
519+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
499520 Ok ( Op :: Subdir ( std:: path:: PathBuf :: from ( path) ) )
500521 }
501522 "prefix" => {
502- let blob = repo. find_blob ( entry. id ( ) ) ?;
503- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
523+ let inner = repo. find_tree ( entry. id ( ) ) ?;
524+ let path_blob = repo. find_blob (
525+ inner
526+ . get_name ( "0" )
527+ . ok_or_else ( || josh_error ( "prefix: missing path" ) ) ?
528+ . id ( ) ,
529+ ) ?;
530+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
504531 Ok ( Op :: Prefix ( std:: path:: PathBuf :: from ( path) ) )
505532 }
506533 "file" => {
507- // Try to read as tree (new format with destination path)
508- if let Ok ( inner) = repo. find_tree ( entry. id ( ) ) {
509- let dest_blob = repo. find_blob (
510- inner
511- . get_name ( "0" )
512- . ok_or_else ( || josh_error ( "file: missing destination path" ) ) ?
513- . id ( ) ,
514- ) ?;
515- let dest_path_str = std:: str:: from_utf8 ( dest_blob. content ( ) ) ?. to_string ( ) ;
516- let source_path = inner
534+ let inner = repo. find_tree ( entry. id ( ) ) ?;
535+ let dest_blob = repo. find_blob (
536+ inner
537+ . get_name ( "0" )
538+ . ok_or_else ( || josh_error ( "file: missing destination path" ) ) ?
539+ . id ( ) ,
540+ ) ?;
541+ let source_blob = repo. find_blob (
542+ inner
517543 . get_name ( "1" )
518- . and_then ( |entry| repo. find_blob ( entry. id ( ) ) . ok ( ) )
519- . and_then ( |blob| {
520- std:: str:: from_utf8 ( blob. content ( ) )
521- . ok ( )
522- . map ( |s| s. to_string ( ) )
523- } )
524- . map ( |s| std:: path:: PathBuf :: from ( s) )
525- . unwrap_or_else ( || std:: path:: PathBuf :: from ( & dest_path_str) ) ;
526- Ok ( Op :: File (
527- std:: path:: PathBuf :: from ( dest_path_str) ,
528- source_path,
529- ) )
530- } else {
531- // Fall back to blob format (old format, backward compatibility)
532- let blob = repo. find_blob ( entry. id ( ) ) ?;
533- let path_str = std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ;
534- let path_buf = std:: path:: PathBuf :: from ( & path_str) ;
535- // When reading from blob format, destination is the same as source
536- Ok ( Op :: File ( path_buf. clone ( ) , path_buf) )
537- }
544+ . ok_or_else ( || josh_error ( "file: missing source path" ) ) ?
545+ . id ( ) ,
546+ ) ?;
547+ let dest_path_str = std:: str:: from_utf8 ( dest_blob. content ( ) ) ?. to_string ( ) ;
548+ let source_path_str = std:: str:: from_utf8 ( source_blob. content ( ) ) ?. to_string ( ) ;
549+ Ok ( Op :: File (
550+ std:: path:: PathBuf :: from ( dest_path_str) ,
551+ std:: path:: PathBuf :: from ( source_path_str) ,
552+ ) )
538553 }
539554 "embed" => {
540- let blob = repo. find_blob ( entry. id ( ) ) ?;
541- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
555+ let inner = repo. find_tree ( entry. id ( ) ) ?;
556+ let path_blob = repo. find_blob (
557+ inner
558+ . get_name ( "0" )
559+ . ok_or_else ( || josh_error ( "embed: missing path" ) ) ?
560+ . id ( ) ,
561+ ) ?;
562+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
542563 Ok ( Op :: Embed ( std:: path:: PathBuf :: from ( path) ) )
543564 }
544565 "pattern" => {
545- let blob = repo. find_blob ( entry. id ( ) ) ?;
546- let pattern = std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ;
566+ let inner = repo. find_tree ( entry. id ( ) ) ?;
567+ let pattern_blob = repo. find_blob (
568+ inner
569+ . get_name ( "0" )
570+ . ok_or_else ( || josh_error ( "pattern: missing pattern" ) ) ?
571+ . id ( ) ,
572+ ) ?;
573+ let pattern = std:: str:: from_utf8 ( pattern_blob. content ( ) ) ?. to_string ( ) ;
547574 Ok ( Op :: Pattern ( pattern) )
548575 }
549576 "workspace" => {
550- let blob = repo. find_blob ( entry. id ( ) ) ?;
551- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
577+ let inner = repo. find_tree ( entry. id ( ) ) ?;
578+ let path_blob = repo. find_blob (
579+ inner
580+ . get_name ( "0" )
581+ . ok_or_else ( || josh_error ( "workspace: missing path" ) ) ?
582+ . id ( ) ,
583+ ) ?;
584+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
552585 Ok ( Op :: Workspace ( std:: path:: PathBuf :: from ( path) ) )
553586 }
554587 "stored" => {
555- let blob = repo. find_blob ( entry. id ( ) ) ?;
556- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
588+ let inner = repo. find_tree ( entry. id ( ) ) ?;
589+ let path_blob = repo. find_blob (
590+ inner
591+ . get_name ( "0" )
592+ . ok_or_else ( || josh_error ( "stored: missing path" ) ) ?
593+ . id ( ) ,
594+ ) ?;
595+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
557596 Ok ( Op :: Stored ( std:: path:: PathBuf :: from ( path) ) )
558597 }
559598 "compose" => {
@@ -615,13 +654,33 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
615654 }
616655 "exclude" => {
617656 let exclude_tree = repo. find_tree ( entry. id ( ) ) ?;
618- let filter = from_tree2 ( repo, exclude_tree. id ( ) ) ?;
619- Ok ( Op :: Exclude ( to_filter ( filter) ) )
657+ if exclude_tree. len ( ) == 1 {
658+ let filter_tree = repo. find_tree (
659+ exclude_tree
660+ . get_name ( "0" )
661+ . ok_or_else ( || josh_error ( "exclude: missing 0" ) ) ?
662+ . id ( ) ,
663+ ) ?;
664+ let filter = from_tree2 ( repo, filter_tree. id ( ) ) ?;
665+ Ok ( Op :: Exclude ( to_filter ( filter) ) )
666+ } else {
667+ Err ( josh_error ( "exclude: expected 1 entry" ) )
668+ }
620669 }
621670 "pin" => {
622671 let pin_tree = repo. find_tree ( entry. id ( ) ) ?;
623- let filter = from_tree2 ( repo, pin_tree. id ( ) ) ?;
624- Ok ( Op :: Pin ( to_filter ( filter) ) )
672+ if pin_tree. len ( ) == 1 {
673+ let filter_tree = repo. find_tree (
674+ pin_tree
675+ . get_name ( "0" )
676+ . ok_or_else ( || josh_error ( "pin: missing 0" ) ) ?
677+ . id ( ) ,
678+ ) ?;
679+ let filter = from_tree2 ( repo, filter_tree. id ( ) ) ?;
680+ Ok ( Op :: Pin ( to_filter ( filter) ) )
681+ } else {
682+ Err ( josh_error ( "pin: expected 1 entry" ) )
683+ }
625684 }
626685 "rev" => {
627686 let rev_tree = repo. find_tree ( entry. id ( ) ) ?;
0 commit comments