@@ -505,3 +505,210 @@ int commit_read(const char *hex, Commit *c)
505505 free (data );
506506 return 0 ;
507507}
508+
509+ // -- 디버그/시각화 유틸리티
510+
511+ #define GRAPH_MAX_DEPTH 64
512+ #define GRAPH_BLOB_PREVIEW 48 /* blob 본문 미리보기 최대 바이트 */
513+
514+ // 본문을 출력 가능한 형태로 이스케이프해서 찍는다. 바이너리/제어문자 안전.
515+ static void print_escaped (const uint8_t * data , size_t len , size_t max )
516+ {
517+ size_t n = len < max ? len : max ;
518+
519+ for (size_t i = 0 ; i < n ; i ++ ) {
520+ unsigned char ch = data [i ];
521+
522+ if (ch == '\n' ) {
523+ fputs ("\\n" , stdout );
524+ } else if (ch == '\t' ) {
525+ fputs ("\\t" , stdout );
526+ } else if (ch == '\r' ) {
527+ fputs ("\\r" , stdout );
528+ } else if (ch >= 0x20 && ch < 0x7f ) {
529+ putchar (ch );
530+ } else {
531+ printf ("\\x%02x" , ch );
532+ }
533+ }
534+
535+ if (len > max ) {
536+ fputs ("..." , stdout );
537+ }
538+ }
539+
540+ // 트리 엔트리의 타입 문자열 (ls-tree 표기와 동일).
541+ static const char * entry_type (const TreeEntry * e )
542+ {
543+ return e -> mode == MODE_DIR ? OBJ_TREE : OBJ_BLOB ;
544+ }
545+
546+ void object_debug_print (const char * hex_hash )
547+ {
548+ char type [8 ];
549+ uint8_t * data = NULL ;
550+ size_t len = 0 ;
551+ char path [512 ];
552+
553+ if (!hex_hash ) {
554+ return ;
555+ }
556+
557+ object_path (hex_hash , path , sizeof (path ));
558+
559+ if (object_read (hex_hash , type , & data , & len ) < 0 ) {
560+ printf ("object %s: 읽기 실패 (%s)\n" , hex_hash , path );
561+ return ;
562+ }
563+
564+ printf ("=== object %s ===\n" , hex_hash );
565+ printf (" path : %s\n" , path );
566+ printf (" type : %s\n" , type );
567+ printf (" size : %zu bytes\n" , len );
568+ // 압축 전 raw 형식: "<type> <size>\0<content>"
569+ printf (" stored: \"%s %zu\\0\" + <content %zu bytes> (zlib deflate)\n" , type , len , len );
570+ printf (" ----- content -----\n" );
571+
572+ if (strcmp (type , OBJ_TREE ) == 0 ) {
573+ Tree t ;
574+
575+ if (tree_read (hex_hash , & t ) == 0 ) {
576+ for (int i = 0 ; i < t .count ; i ++ ) {
577+ TreeEntry * e = & t .entries [i ];
578+ char ehex [GIT_HEX_STR_SIZE ];
579+
580+ sha1_to_hex (e -> sha1 , ehex );
581+ printf (" %-6s %s %s\t%s\n" , mode_to_str (e -> mode ), entry_type (e ),
582+ ehex , e -> name );
583+ }
584+ }
585+ } else if (strcmp (type , OBJ_COMMIT ) == 0 ) {
586+ // 커밋 본문은 텍스트라 그대로 출력
587+ fwrite (data , 1 , len , stdout );
588+ if (len == 0 || data [len - 1 ] != '\n' ) {
589+ putchar ('\n' );
590+ }
591+ } else {
592+ // blob 등: 제어문자를 이스케이프해서 출력
593+ printf (" \"" );
594+ print_escaped (data , len , len );
595+ printf ("\"\n" );
596+ }
597+
598+ free (data );
599+ }
600+
601+ // hex를 루트로 오브젝트 그래프를 재귀 출력한다.
602+ // head: 이 노드 줄 앞에 붙는 접두사 (커넥터 포함)
603+ // cont: 이 노드의 자식 줄에 이어 붙는 접두사 (수직선 포함)
604+ // label: 트리 엔트리에서 넘어온 "<mode> <type> <name>" 라벨 (루트면 NULL)
605+ static void graph_rec (const char * hex , const char * label , const char * head , const char * cont ,
606+ int depth )
607+ {
608+ char type [8 ];
609+ uint8_t * data = NULL ;
610+ size_t len = 0 ;
611+ char path [512 ];
612+
613+ object_path (hex , path , sizeof (path ));
614+
615+ if (object_read (hex , type , & data , & len ) < 0 ) {
616+ printf ("%s%.7s <읽기 실패: %s>\n" , head , hex , path );
617+ return ;
618+ }
619+
620+ // 노드 한 줄: [접두사] [라벨 또는 (타입)] [짧은 해시] [저장 경로]
621+ if (label ) {
622+ printf ("%s%s %.7s %s\n" , head , label , hex , path );
623+ } else {
624+ printf ("%s(%s) %.7s %s\n" , head , type , hex , path );
625+ }
626+
627+ if (strcmp (type , OBJ_BLOB ) == 0 ) {
628+ // blob은 본문 미리보기를 자식 줄로 한 줄 더 보여준다.
629+ printf ("%s ↳ \"" , cont );
630+ print_escaped (data , len , GRAPH_BLOB_PREVIEW );
631+ printf ("\" (%zu bytes)\n" , len );
632+ } else if (strcmp (type , OBJ_TREE ) == 0 ) {
633+ Tree t ;
634+
635+ if (depth > 0 && tree_read (hex , & t ) == 0 ) {
636+ for (int i = 0 ; i < t .count ; i ++ ) {
637+ TreeEntry * e = & t .entries [i ];
638+ int last = (i == t .count - 1 );
639+ char child_head [1024 ];
640+ char child_cont [1024 ];
641+ char label_buf [320 ];
642+ char ehex [GIT_HEX_STR_SIZE ];
643+
644+ snprintf (child_head , sizeof (child_head ), "%s%s" , cont ,
645+ last ? "└── " : "├── " );
646+ snprintf (child_cont , sizeof (child_cont ), "%s%s" , cont ,
647+ last ? " " : "│ " );
648+
649+ sha1_to_hex (e -> sha1 , ehex );
650+ snprintf (label_buf , sizeof (label_buf ), "%-6s %s %s" ,
651+ mode_to_str (e -> mode ), entry_type (e ), e -> name );
652+
653+ graph_rec (ehex , label_buf , child_head , child_cont , depth - 1 );
654+ }
655+ }
656+ } else if (strcmp (type , OBJ_COMMIT ) == 0 ) {
657+ Commit c ;
658+
659+ if (commit_read (hex , & c ) == 0 ) {
660+ char msg [80 ];
661+ char * nl ;
662+ int total = 1 + c .parent_count ; // tree 1개 + parent N개
663+ int idx = 0 ;
664+
665+ // 커밋 메타데이터를 정보 줄로 출력
666+ printf ("%s│ author : %s <%s>\n" , cont , c .author .name , c .author .email );
667+
668+ snprintf (msg , sizeof (msg ), "%s" , c .message );
669+ nl = strchr (msg , '\n' );
670+ if (nl ) {
671+ * nl = '\0' ;
672+ }
673+ printf ("%s│ message: %s\n" , cont , msg );
674+ printf ("%s│\n" , cont );
675+
676+ // tree 자식 (재귀)
677+ {
678+ int last = (idx == total - 1 );
679+ char child_head [1024 ];
680+ char child_cont [1024 ];
681+
682+ snprintf (child_head , sizeof (child_head ), "%s%s" , cont ,
683+ last ? "└── " : "├── " );
684+ snprintf (child_cont , sizeof (child_cont ), "%s%s" , cont ,
685+ last ? " " : "│ " );
686+
687+ graph_rec (c .tree_hex , "tree" , child_head , child_cont , depth - 1 );
688+ idx ++ ;
689+ }
690+
691+ // parent 들은 히스토리 폭주를 막기 위해 재귀하지 않고 참조 줄만 출력
692+ for (int i = 0 ; i < c .parent_count ; i ++ ) {
693+ int last = (idx == total - 1 );
694+ char ppath [512 ];
695+
696+ object_path (c .parent_hex [i ], ppath , sizeof (ppath ));
697+ printf ("%s%sparent %.7s %s\n" , cont , last ? "└── " : "├── " ,
698+ c .parent_hex [i ], ppath );
699+ idx ++ ;
700+ }
701+ }
702+ }
703+
704+ free (data );
705+ }
706+
707+ void object_print_graph (const char * hex_hash )
708+ {
709+ if (!hex_hash ) {
710+ return ;
711+ }
712+
713+ graph_rec (hex_hash , NULL , "" , "" , GRAPH_MAX_DEPTH );
714+ }
0 commit comments