@@ -190,52 +190,45 @@ 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 #[ cfg( feature = "incubating" ) ]
224217 Op :: Embed ( path) => {
225- let blob = self . write_blob ( path. to_string_lossy ( ) . as_bytes ( ) ) ;
226- push_blob_entries ( & mut entries, [ ( "embed" , blob ) ] ) ;
218+ let params_tree = self . build_str_params ( & [ path. to_string_lossy ( ) . as_ref ( ) ] ) ;
219+ push_tree_entries ( & mut entries, [ ( "embed" , params_tree ) ] ) ;
227220 }
228221 Op :: Pattern ( pattern) => {
229- let blob = self . write_blob ( pattern. as_bytes ( ) ) ;
230- push_blob_entries ( & mut entries, [ ( "pattern" , blob ) ] ) ;
222+ let params_tree = self . build_str_params ( & [ pattern. as_ref ( ) ] ) ;
223+ push_tree_entries ( & mut entries, [ ( "pattern" , params_tree ) ] ) ;
231224 }
232225 Op :: Workspace ( path) => {
233- let blob = self . write_blob ( path. to_string_lossy ( ) . as_bytes ( ) ) ;
234- push_blob_entries ( & mut entries, [ ( "workspace" , blob ) ] ) ;
226+ let params_tree = self . build_str_params ( & [ path. to_string_lossy ( ) . as_ref ( ) ] ) ;
227+ push_tree_entries ( & mut entries, [ ( "workspace" , params_tree ) ] ) ;
235228 }
236229 Op :: Stored ( path) => {
237- let blob = self . write_blob ( path. to_string_lossy ( ) . as_bytes ( ) ) ;
238- push_blob_entries ( & mut entries, [ ( "stored" , blob ) ] ) ;
230+ let params_tree = self . build_str_params ( & [ path. to_string_lossy ( ) . as_ref ( ) ] ) ;
231+ push_tree_entries ( & mut entries, [ ( "stored" , params_tree ) ] ) ;
239232 }
240233 Op :: Nop => {
241234 let blob = self . write_blob ( b"" ) ;
@@ -256,13 +249,13 @@ impl InMemoryBuilder {
256249 }
257250 #[ cfg( feature = "incubating" ) ]
258251 Op :: Link ( mode) => {
259- let blob = self . write_blob ( mode. as_bytes ( ) ) ;
260- push_blob_entries ( & mut entries, [ ( "link" , blob ) ] ) ;
252+ let params_tree = self . build_str_params ( & [ mode. as_ref ( ) ] ) ;
253+ push_tree_entries ( & mut entries, [ ( "link" , params_tree ) ] ) ;
261254 }
262255 #[ cfg( feature = "incubating" ) ]
263256 Op :: Adapt ( mode) => {
264- let blob = self . write_blob ( mode. as_bytes ( ) ) ;
265- push_blob_entries ( & mut entries, [ ( "adapt" , blob ) ] ) ;
257+ let params_tree = self . build_str_params ( & [ mode. as_ref ( ) ] ) ;
258+ push_tree_entries ( & mut entries, [ ( "adapt" , params_tree ) ] ) ;
266259 }
267260 #[ cfg( feature = "incubating" ) ]
268261 Op :: Unlink => {
@@ -329,8 +322,8 @@ impl InMemoryBuilder {
329322 push_tree_entries ( & mut entries, [ ( "regex_replace" , params_tree) ] ) ;
330323 }
331324 Op :: Hook ( hook) => {
332- let blob = self . write_blob ( hook. as_bytes ( ) ) ;
333- push_blob_entries ( & mut entries, [ ( "hook" , blob ) ] ) ;
325+ let params_tree = self . build_str_params ( & [ hook. as_ref ( ) ] ) ;
326+ push_tree_entries ( & mut entries, [ ( "hook" , params_tree ) ] ) ;
334327 }
335328 }
336329
@@ -400,13 +393,29 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
400393 }
401394 #[ cfg( feature = "incubating" ) ]
402395 "link" => {
403- let blob = repo. find_blob ( entry. id ( ) ) ?;
404- Ok ( Op :: Link ( std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ) )
396+ let inner = repo. find_tree ( entry. id ( ) ) ?;
397+ let mode_blob = repo. find_blob (
398+ inner
399+ . get_name ( "0" )
400+ . ok_or_else ( || josh_error ( "link: missing mode" ) ) ?
401+ . id ( ) ,
402+ ) ?;
403+ Ok ( Op :: Link (
404+ std:: str:: from_utf8 ( mode_blob. content ( ) ) ?. to_string ( ) ,
405+ ) )
405406 }
406407 #[ cfg( feature = "incubating" ) ]
407408 "adapt" => {
408- let blob = repo. find_blob ( entry. id ( ) ) ?;
409- Ok ( Op :: Adapt ( std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ) )
409+ let inner = repo. find_tree ( entry. id ( ) ) ?;
410+ let mode_blob = repo. find_blob (
411+ inner
412+ . get_name ( "0" )
413+ . ok_or_else ( || josh_error ( "adapt: missing mode" ) ) ?
414+ . id ( ) ,
415+ ) ?;
416+ Ok ( Op :: Adapt (
417+ std:: str:: from_utf8 ( mode_blob. content ( ) ) ?. to_string ( ) ,
418+ ) )
410419 }
411420 #[ cfg( feature = "incubating" ) ]
412421 "unlink" => {
@@ -443,8 +452,14 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
443452 }
444453 }
445454 "hook" => {
446- let blob = repo. find_blob ( entry. id ( ) ) ?;
447- let hook_name = std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ;
455+ let inner = repo. find_tree ( entry. id ( ) ) ?;
456+ let hook_blob = repo. find_blob (
457+ inner
458+ . get_name ( "0" )
459+ . ok_or_else ( || josh_error ( "hook: missing hook name" ) ) ?
460+ . id ( ) ,
461+ ) ?;
462+ let hook_name = std:: str:: from_utf8 ( hook_blob. content ( ) ) ?. to_string ( ) ;
448463 Ok ( Op :: Hook ( hook_name) )
449464 }
450465 "author" => {
@@ -504,67 +519,91 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
504519 Ok ( Op :: Message ( fmt, regex) )
505520 }
506521 "subdir" => {
507- let blob = repo. find_blob ( entry. id ( ) ) ?;
508- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
522+ let inner = repo. find_tree ( entry. id ( ) ) ?;
523+ let path_blob = repo. find_blob (
524+ inner
525+ . get_name ( "0" )
526+ . ok_or_else ( || josh_error ( "subdir: missing path" ) ) ?
527+ . id ( ) ,
528+ ) ?;
529+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
509530 Ok ( Op :: Subdir ( std:: path:: PathBuf :: from ( path) ) )
510531 }
511532 "prefix" => {
512- let blob = repo. find_blob ( entry. id ( ) ) ?;
513- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
533+ let inner = repo. find_tree ( entry. id ( ) ) ?;
534+ let path_blob = repo. find_blob (
535+ inner
536+ . get_name ( "0" )
537+ . ok_or_else ( || josh_error ( "prefix: missing path" ) ) ?
538+ . id ( ) ,
539+ ) ?;
540+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
514541 Ok ( Op :: Prefix ( std:: path:: PathBuf :: from ( path) ) )
515542 }
516543 "file" => {
517- // Try to read as tree (new format with destination path)
518- if let Ok ( inner) = repo. find_tree ( entry. id ( ) ) {
519- let dest_blob = repo. find_blob (
520- inner
521- . get_name ( "0" )
522- . ok_or_else ( || josh_error ( "file: missing destination path" ) ) ?
523- . id ( ) ,
524- ) ?;
525- let dest_path_str = std:: str:: from_utf8 ( dest_blob. content ( ) ) ?. to_string ( ) ;
526- let source_path = inner
544+ let inner = repo. find_tree ( entry. id ( ) ) ?;
545+ let dest_blob = repo. find_blob (
546+ inner
547+ . get_name ( "0" )
548+ . ok_or_else ( || josh_error ( "file: missing destination path" ) ) ?
549+ . id ( ) ,
550+ ) ?;
551+ let source_blob = repo. find_blob (
552+ inner
527553 . get_name ( "1" )
528- . and_then ( |entry| repo. find_blob ( entry. id ( ) ) . ok ( ) )
529- . and_then ( |blob| {
530- std:: str:: from_utf8 ( blob. content ( ) )
531- . ok ( )
532- . map ( |s| s. to_string ( ) )
533- } )
534- . map ( |s| std:: path:: PathBuf :: from ( s) )
535- . unwrap_or_else ( || std:: path:: PathBuf :: from ( & dest_path_str) ) ;
536- Ok ( Op :: File (
537- std:: path:: PathBuf :: from ( dest_path_str) ,
538- source_path,
539- ) )
540- } else {
541- // Fall back to blob format (old format, backward compatibility)
542- let blob = repo. find_blob ( entry. id ( ) ) ?;
543- let path_str = std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ;
544- let path_buf = std:: path:: PathBuf :: from ( & path_str) ;
545- // When reading from blob format, destination is the same as source
546- Ok ( Op :: File ( path_buf. clone ( ) , path_buf) )
547- }
554+ . ok_or_else ( || josh_error ( "file: missing source path" ) ) ?
555+ . id ( ) ,
556+ ) ?;
557+ let dest_path_str = std:: str:: from_utf8 ( dest_blob. content ( ) ) ?. to_string ( ) ;
558+ let source_path_str = std:: str:: from_utf8 ( source_blob. content ( ) ) ?. to_string ( ) ;
559+ Ok ( Op :: File (
560+ std:: path:: PathBuf :: from ( dest_path_str) ,
561+ std:: path:: PathBuf :: from ( source_path_str) ,
562+ ) )
548563 }
549564 #[ cfg( feature = "incubating" ) ]
550565 "embed" => {
551- let blob = repo. find_blob ( entry. id ( ) ) ?;
552- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
566+ let inner = repo. find_tree ( entry. id ( ) ) ?;
567+ let path_blob = repo. find_blob (
568+ inner
569+ . get_name ( "0" )
570+ . ok_or_else ( || josh_error ( "embed: missing path" ) ) ?
571+ . id ( ) ,
572+ ) ?;
573+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
553574 Ok ( Op :: Embed ( std:: path:: PathBuf :: from ( path) ) )
554575 }
555576 "pattern" => {
556- let blob = repo. find_blob ( entry. id ( ) ) ?;
557- let pattern = std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ;
577+ let inner = repo. find_tree ( entry. id ( ) ) ?;
578+ let pattern_blob = repo. find_blob (
579+ inner
580+ . get_name ( "0" )
581+ . ok_or_else ( || josh_error ( "pattern: missing pattern" ) ) ?
582+ . id ( ) ,
583+ ) ?;
584+ let pattern = std:: str:: from_utf8 ( pattern_blob. content ( ) ) ?. to_string ( ) ;
558585 Ok ( Op :: Pattern ( pattern) )
559586 }
560587 "workspace" => {
561- let blob = repo. find_blob ( entry. id ( ) ) ?;
562- 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 ( "workspace: missing path" ) ) ?
593+ . id ( ) ,
594+ ) ?;
595+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
563596 Ok ( Op :: Workspace ( std:: path:: PathBuf :: from ( path) ) )
564597 }
565598 "stored" => {
566- let blob = repo. find_blob ( entry. id ( ) ) ?;
567- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
599+ let inner = repo. find_tree ( entry. id ( ) ) ?;
600+ let path_blob = repo. find_blob (
601+ inner
602+ . get_name ( "0" )
603+ . ok_or_else ( || josh_error ( "stored: missing path" ) ) ?
604+ . id ( ) ,
605+ ) ?;
606+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
568607 Ok ( Op :: Stored ( std:: path:: PathBuf :: from ( path) ) )
569608 }
570609 "compose" => {
@@ -626,13 +665,33 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
626665 }
627666 "exclude" => {
628667 let exclude_tree = repo. find_tree ( entry. id ( ) ) ?;
629- let filter = from_tree2 ( repo, exclude_tree. id ( ) ) ?;
630- Ok ( Op :: Exclude ( to_filter ( filter) ) )
668+ if exclude_tree. len ( ) == 1 {
669+ let filter_tree = repo. find_tree (
670+ exclude_tree
671+ . get_name ( "0" )
672+ . ok_or_else ( || josh_error ( "exclude: missing 0" ) ) ?
673+ . id ( ) ,
674+ ) ?;
675+ let filter = from_tree2 ( repo, filter_tree. id ( ) ) ?;
676+ Ok ( Op :: Exclude ( to_filter ( filter) ) )
677+ } else {
678+ Err ( josh_error ( "exclude: expected 1 entry" ) )
679+ }
631680 }
632681 "pin" => {
633682 let pin_tree = repo. find_tree ( entry. id ( ) ) ?;
634- let filter = from_tree2 ( repo, pin_tree. id ( ) ) ?;
635- Ok ( Op :: Pin ( to_filter ( filter) ) )
683+ if pin_tree. len ( ) == 1 {
684+ let filter_tree = repo. find_tree (
685+ pin_tree
686+ . get_name ( "0" )
687+ . ok_or_else ( || josh_error ( "pin: missing 0" ) ) ?
688+ . id ( ) ,
689+ ) ?;
690+ let filter = from_tree2 ( repo, filter_tree. id ( ) ) ?;
691+ Ok ( Op :: Pin ( to_filter ( filter) ) )
692+ } else {
693+ Err ( josh_error ( "pin: expected 1 entry" ) )
694+ }
636695 }
637696 "rev" => {
638697 let rev_tree = repo. find_tree ( entry. id ( ) ) ?;
0 commit comments