@@ -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 }
339332
@@ -401,12 +394,28 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
401394 Ok ( Op :: Export )
402395 }
403396 "link" => {
404- let blob = repo. find_blob ( entry. id ( ) ) ?;
405- Ok ( Op :: Link ( std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ) )
397+ let inner = repo. find_tree ( entry. id ( ) ) ?;
398+ let mode_blob = repo. find_blob (
399+ inner
400+ . get_name ( "0" )
401+ . ok_or_else ( || josh_error ( "link: missing mode" ) ) ?
402+ . id ( ) ,
403+ ) ?;
404+ Ok ( Op :: Link (
405+ std:: str:: from_utf8 ( mode_blob. content ( ) ) ?. to_string ( ) ,
406+ ) )
406407 }
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 "unlink" => {
412421 let _ = repo. find_blob ( entry. id ( ) ) ?;
@@ -442,8 +451,14 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
442451 }
443452 }
444453 "hook" => {
445- let blob = repo. find_blob ( entry. id ( ) ) ?;
446- let hook_name = std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ;
454+ let inner = repo. find_tree ( entry. id ( ) ) ?;
455+ let hook_blob = repo. find_blob (
456+ inner
457+ . get_name ( "0" )
458+ . ok_or_else ( || josh_error ( "hook: missing hook name" ) ) ?
459+ . id ( ) ,
460+ ) ?;
461+ let hook_name = std:: str:: from_utf8 ( hook_blob. content ( ) ) ?. to_string ( ) ;
447462 Ok ( Op :: Hook ( hook_name) )
448463 }
449464 "author" => {
@@ -503,66 +518,90 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
503518 Ok ( Op :: Message ( fmt, regex) )
504519 }
505520 "subdir" => {
506- let blob = repo. find_blob ( entry. id ( ) ) ?;
507- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
521+ let inner = repo. find_tree ( entry. id ( ) ) ?;
522+ let path_blob = repo. find_blob (
523+ inner
524+ . get_name ( "0" )
525+ . ok_or_else ( || josh_error ( "subdir: missing path" ) ) ?
526+ . id ( ) ,
527+ ) ?;
528+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
508529 Ok ( Op :: Subdir ( std:: path:: PathBuf :: from ( path) ) )
509530 }
510531 "prefix" => {
511- let blob = repo. find_blob ( entry. id ( ) ) ?;
512- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
532+ let inner = repo. find_tree ( entry. id ( ) ) ?;
533+ let path_blob = repo. find_blob (
534+ inner
535+ . get_name ( "0" )
536+ . ok_or_else ( || josh_error ( "prefix: missing path" ) ) ?
537+ . id ( ) ,
538+ ) ?;
539+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
513540 Ok ( Op :: Prefix ( std:: path:: PathBuf :: from ( path) ) )
514541 }
515542 "file" => {
516- // Try to read as tree (new format with destination path)
517- if let Ok ( inner) = repo. find_tree ( entry. id ( ) ) {
518- let dest_blob = repo. find_blob (
519- inner
520- . get_name ( "0" )
521- . ok_or_else ( || josh_error ( "file: missing destination path" ) ) ?
522- . id ( ) ,
523- ) ?;
524- let dest_path_str = std:: str:: from_utf8 ( dest_blob. content ( ) ) ?. to_string ( ) ;
525- let source_path = inner
543+ let inner = repo. find_tree ( entry. id ( ) ) ?;
544+ let dest_blob = repo. find_blob (
545+ inner
546+ . get_name ( "0" )
547+ . ok_or_else ( || josh_error ( "file: missing destination path" ) ) ?
548+ . id ( ) ,
549+ ) ?;
550+ let source_blob = repo. find_blob (
551+ inner
526552 . get_name ( "1" )
527- . and_then ( |entry| repo. find_blob ( entry. id ( ) ) . ok ( ) )
528- . and_then ( |blob| {
529- std:: str:: from_utf8 ( blob. content ( ) )
530- . ok ( )
531- . map ( |s| s. to_string ( ) )
532- } )
533- . map ( |s| std:: path:: PathBuf :: from ( s) )
534- . unwrap_or_else ( || std:: path:: PathBuf :: from ( & dest_path_str) ) ;
535- Ok ( Op :: File (
536- std:: path:: PathBuf :: from ( dest_path_str) ,
537- source_path,
538- ) )
539- } else {
540- // Fall back to blob format (old format, backward compatibility)
541- let blob = repo. find_blob ( entry. id ( ) ) ?;
542- let path_str = std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ;
543- let path_buf = std:: path:: PathBuf :: from ( & path_str) ;
544- // When reading from blob format, destination is the same as source
545- Ok ( Op :: File ( path_buf. clone ( ) , path_buf) )
546- }
553+ . ok_or_else ( || josh_error ( "file: missing source path" ) ) ?
554+ . id ( ) ,
555+ ) ?;
556+ let dest_path_str = std:: str:: from_utf8 ( dest_blob. content ( ) ) ?. to_string ( ) ;
557+ let source_path_str = std:: str:: from_utf8 ( source_blob. content ( ) ) ?. to_string ( ) ;
558+ Ok ( Op :: File (
559+ std:: path:: PathBuf :: from ( dest_path_str) ,
560+ std:: path:: PathBuf :: from ( source_path_str) ,
561+ ) )
547562 }
548563 "embed" => {
549- let blob = repo. find_blob ( entry. id ( ) ) ?;
550- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
564+ let inner = repo. find_tree ( entry. id ( ) ) ?;
565+ let path_blob = repo. find_blob (
566+ inner
567+ . get_name ( "0" )
568+ . ok_or_else ( || josh_error ( "embed: missing path" ) ) ?
569+ . id ( ) ,
570+ ) ?;
571+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
551572 Ok ( Op :: Embed ( std:: path:: PathBuf :: from ( path) ) )
552573 }
553574 "pattern" => {
554- let blob = repo. find_blob ( entry. id ( ) ) ?;
555- let pattern = std:: str:: from_utf8 ( blob. content ( ) ) ?. to_string ( ) ;
575+ let inner = repo. find_tree ( entry. id ( ) ) ?;
576+ let pattern_blob = repo. find_blob (
577+ inner
578+ . get_name ( "0" )
579+ . ok_or_else ( || josh_error ( "pattern: missing pattern" ) ) ?
580+ . id ( ) ,
581+ ) ?;
582+ let pattern = std:: str:: from_utf8 ( pattern_blob. content ( ) ) ?. to_string ( ) ;
556583 Ok ( Op :: Pattern ( pattern) )
557584 }
558585 "workspace" => {
559- let blob = repo. find_blob ( entry. id ( ) ) ?;
560- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
586+ let inner = repo. find_tree ( entry. id ( ) ) ?;
587+ let path_blob = repo. find_blob (
588+ inner
589+ . get_name ( "0" )
590+ . ok_or_else ( || josh_error ( "workspace: missing path" ) ) ?
591+ . id ( ) ,
592+ ) ?;
593+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
561594 Ok ( Op :: Workspace ( std:: path:: PathBuf :: from ( path) ) )
562595 }
563596 "stored" => {
564- let blob = repo. find_blob ( entry. id ( ) ) ?;
565- let path = std:: str:: from_utf8 ( blob. content ( ) ) ?;
597+ let inner = repo. find_tree ( entry. id ( ) ) ?;
598+ let path_blob = repo. find_blob (
599+ inner
600+ . get_name ( "0" )
601+ . ok_or_else ( || josh_error ( "stored: missing path" ) ) ?
602+ . id ( ) ,
603+ ) ?;
604+ let path = std:: str:: from_utf8 ( path_blob. content ( ) ) ?;
566605 Ok ( Op :: Stored ( std:: path:: PathBuf :: from ( path) ) )
567606 }
568607 "compose" => {
@@ -624,13 +663,33 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
624663 }
625664 "exclude" => {
626665 let exclude_tree = repo. find_tree ( entry. id ( ) ) ?;
627- let filter = from_tree2 ( repo, exclude_tree. id ( ) ) ?;
628- Ok ( Op :: Exclude ( to_filter ( filter) ) )
666+ if exclude_tree. len ( ) == 1 {
667+ let filter_tree = repo. find_tree (
668+ exclude_tree
669+ . get_name ( "0" )
670+ . ok_or_else ( || josh_error ( "exclude: missing 0" ) ) ?
671+ . id ( ) ,
672+ ) ?;
673+ let filter = from_tree2 ( repo, filter_tree. id ( ) ) ?;
674+ Ok ( Op :: Exclude ( to_filter ( filter) ) )
675+ } else {
676+ Err ( josh_error ( "exclude: expected 1 entry" ) )
677+ }
629678 }
630679 "pin" => {
631680 let pin_tree = repo. find_tree ( entry. id ( ) ) ?;
632- let filter = from_tree2 ( repo, pin_tree. id ( ) ) ?;
633- Ok ( Op :: Pin ( to_filter ( filter) ) )
681+ if pin_tree. len ( ) == 1 {
682+ let filter_tree = repo. find_tree (
683+ pin_tree
684+ . get_name ( "0" )
685+ . ok_or_else ( || josh_error ( "pin: missing 0" ) ) ?
686+ . id ( ) ,
687+ ) ?;
688+ let filter = from_tree2 ( repo, filter_tree. id ( ) ) ?;
689+ Ok ( Op :: Pin ( to_filter ( filter) ) )
690+ } else {
691+ Err ( josh_error ( "pin: expected 1 entry" ) )
692+ }
634693 }
635694 "rev" => {
636695 let rev_tree = repo. find_tree ( entry. id ( ) ) ?;
0 commit comments