@@ -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"" ) ;
@@ -332,8 +325,8 @@ impl InMemoryBuilder {
332325 push_tree_entries ( & mut entries, [ ( "regex_replace" , params_tree) ] ) ;
333326 }
334327 Op :: Hook ( hook) => {
335- let blob = self . write_blob ( hook. as_bytes ( ) ) ;
336- push_blob_entries ( & mut entries, [ ( "hook" , blob ) ] ) ;
328+ let params_tree = self . build_str_params ( & [ hook. as_ref ( ) ] ) ;
329+ push_tree_entries ( & mut entries, [ ( "hook" , params_tree ) ] ) ;
337330 }
338331 & Op :: Lookup ( _) | & Op :: Lookup2 ( _) => todo ! ( ) ,
339332 }
@@ -402,12 +395,28 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
402395 Ok ( Op :: Export )
403396 }
404397 "link" => {
405- let blob = repo. find_blob ( entry. id ( ) ) ?;
406- Ok ( Op :: Link ( std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ) )
398+ let inner = repo. find_tree ( entry. id ( ) ) ?;
399+ let mode_blob = repo. find_blob (
400+ inner
401+ . get_name ( "0" )
402+ . ok_or_else ( || josh_error ( "link: missing mode" ) ) ?
403+ . id ( ) ,
404+ ) ?;
405+ Ok ( Op :: Link (
406+ std:: str:: from_utf8 ( mode_blob. content ( ) ) ?. to_string ( ) ,
407+ ) )
407408 }
408409 "adapt" => {
409- let blob = repo. find_blob ( entry. id ( ) ) ?;
410- Ok ( Op :: Adapt ( std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ) )
410+ let inner = repo. find_tree ( entry. id ( ) ) ?;
411+ let mode_blob = repo. find_blob (
412+ inner
413+ . get_name ( "0" )
414+ . ok_or_else ( || josh_error ( "adapt: missing mode" ) ) ?
415+ . id ( ) ,
416+ ) ?;
417+ Ok ( Op :: Adapt (
418+ std:: str:: from_utf8 ( mode_blob. content ( ) ) ?. to_string ( ) ,
419+ ) )
411420 }
412421 "unlink" => {
413422 let _ = repo. find_blob ( entry. id ( ) ) ?;
@@ -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,66 +519,90 @@ 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 "embed" => {
550- let blob = repo. find_blob ( entry. id ( ) ) ?;
551- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
565+ let inner = repo. find_tree ( entry. id ( ) ) ?;
566+ let path_blob = repo. find_blob (
567+ inner
568+ . get_name ( "0" )
569+ . ok_or_else ( || josh_error ( "embed: missing path" ) ) ?
570+ . id ( ) ,
571+ ) ?;
572+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
552573 Ok ( Op :: Embed ( std:: path:: PathBuf :: from ( path) ) )
553574 }
554575 "pattern" => {
555- let blob = repo. find_blob ( entry. id ( ) ) ?;
556- let pattern = std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ;
576+ let inner = repo. find_tree ( entry. id ( ) ) ?;
577+ let pattern_blob = repo. find_blob (
578+ inner
579+ . get_name ( "0" )
580+ . ok_or_else ( || josh_error ( "pattern: missing pattern" ) ) ?
581+ . id ( ) ,
582+ ) ?;
583+ let pattern = std:: str:: from_utf8 ( pattern_blob. content ( ) ) ?. to_string ( ) ;
557584 Ok ( Op :: Pattern ( pattern) )
558585 }
559586 "workspace" => {
560- let blob = repo. find_blob ( entry. id ( ) ) ?;
561- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
587+ let inner = repo. find_tree ( entry. id ( ) ) ?;
588+ let path_blob = repo. find_blob (
589+ inner
590+ . get_name ( "0" )
591+ . ok_or_else ( || josh_error ( "workspace: missing path" ) ) ?
592+ . id ( ) ,
593+ ) ?;
594+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
562595 Ok ( Op :: Workspace ( std:: path:: PathBuf :: from ( path) ) )
563596 }
564597 "stored" => {
565- let blob = repo. find_blob ( entry. id ( ) ) ?;
566- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
598+ let inner = repo. find_tree ( entry. id ( ) ) ?;
599+ let path_blob = repo. find_blob (
600+ inner
601+ . get_name ( "0" )
602+ . ok_or_else ( || josh_error ( "stored: missing path" ) ) ?
603+ . id ( ) ,
604+ ) ?;
605+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
567606 Ok ( Op :: Stored ( std:: path:: PathBuf :: from ( path) ) )
568607 }
569608 "compose" => {
@@ -625,13 +664,33 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
625664 }
626665 "exclude" => {
627666 let exclude_tree = repo. find_tree ( entry. id ( ) ) ?;
628- let filter = from_tree2 ( repo, exclude_tree. id ( ) ) ?;
629- Ok ( Op :: Exclude ( to_filter ( filter) ) )
667+ if exclude_tree. len ( ) == 1 {
668+ let filter_tree = repo. find_tree (
669+ exclude_tree
670+ . get_name ( "0" )
671+ . ok_or_else ( || josh_error ( "exclude: missing 0" ) ) ?
672+ . id ( ) ,
673+ ) ?;
674+ let filter = from_tree2 ( repo, filter_tree. id ( ) ) ?;
675+ Ok ( Op :: Exclude ( to_filter ( filter) ) )
676+ } else {
677+ Err ( josh_error ( "exclude: expected 1 entry" ) )
678+ }
630679 }
631680 "pin" => {
632681 let pin_tree = repo. find_tree ( entry. id ( ) ) ?;
633- let filter = from_tree2 ( repo, pin_tree. id ( ) ) ?;
634- Ok ( Op :: Pin ( to_filter ( filter) ) )
682+ if pin_tree. len ( ) == 1 {
683+ let filter_tree = repo. find_tree (
684+ pin_tree
685+ . get_name ( "0" )
686+ . ok_or_else ( || josh_error ( "pin: missing 0" ) ) ?
687+ . id ( ) ,
688+ ) ?;
689+ let filter = from_tree2 ( repo, filter_tree. id ( ) ) ?;
690+ Ok ( Op :: Pin ( to_filter ( filter) ) )
691+ } else {
692+ Err ( josh_error ( "pin: expected 1 entry" ) )
693+ }
635694 }
636695 "rev" => {
637696 let rev_tree = repo. find_tree ( entry. id ( ) ) ?;
0 commit comments