@@ -517,6 +517,202 @@ def patch_title(self) -> str:
517517 return self ._revision_metadata ["fields" ]["title" ]
518518
519519
520+ def create_bug_timeline (comments : list [dict ], history : list [dict ]) -> list [str ]:
521+ """Create a unified timeline from bug history and comments."""
522+ events = []
523+
524+ ignored_fields = {"cc" , "flagtypes.name" }
525+
526+ # Add history events
527+ for event in history :
528+ changes = [
529+ change
530+ for change in event ["changes" ]
531+ if change ["field_name" ] not in ignored_fields
532+ ]
533+ if not changes :
534+ continue
535+
536+ events .append (
537+ {
538+ "time" : event ["when" ],
539+ "type" : "change" ,
540+ "who" : event ["who" ],
541+ "details" : changes ,
542+ }
543+ )
544+
545+ # Add comments
546+ for comment in comments :
547+ events .append (
548+ {
549+ "time" : comment ["time" ],
550+ "type" : "comment" ,
551+ "who" : comment ["author" ],
552+ "id" : comment ["id" ],
553+ "count" : comment ["count" ],
554+ "text" : comment ["text" ],
555+ }
556+ )
557+
558+ # Sort by timestamp
559+ events .sort (key = lambda x : (x ["time" ], x ["type" ] == "change" ))
560+
561+ # Format timeline
562+ timeline = []
563+
564+ last_event = None
565+ for event in events :
566+ date = event ["time" ][:10 ]
567+ time = event ["time" ][11 :19 ]
568+
569+ if last_event and last_event ["time" ] != event ["time" ]:
570+ timeline .append ("---\n " )
571+
572+ last_event = event
573+
574+ if event ["type" ] == "comment" :
575+ timeline .append (
576+ f"**{ date } { time } ** - Comment #{ event ['count' ]} by { event ['who' ]} "
577+ )
578+ timeline .append (f"{ event ['text' ]} \n " )
579+ else :
580+ timeline .append (f"**{ date } { time } ** - Changes by { event ['who' ]} " )
581+ for change in event ["details" ]:
582+ field = change .get ("field_name" , "unknown" )
583+ old = change .get ("removed" , "" )
584+ new = change .get ("added" , "" )
585+ if old or new :
586+ timeline .append (f" - { field } : '{ old } ' → '{ new } '" )
587+ timeline .append ("" )
588+
589+ return timeline
590+
591+
592+ def bug_dict_to_markdown (bug ):
593+ md_lines = []
594+
595+ # Header with bug ID and summary
596+ md_lines .append (
597+ f"# Bug { bug .get ('id' , 'Unknown' )} - { bug .get ('summary' , 'No summary' )} "
598+ )
599+ md_lines .append ("" )
600+
601+ # Basic Information
602+ md_lines .append ("## Basic Information" )
603+ md_lines .append (f"- **Status**: { bug .get ('status' , 'Unknown' )} " )
604+ md_lines .append (f"- **Severity**: { bug .get ('severity' , 'Unknown' )} " )
605+ md_lines .append (f"- **Product**: { bug .get ('product' , 'Unknown' )} " )
606+ md_lines .append (f"- **Component**: { bug .get ('component' , 'Unknown' )} " )
607+ md_lines .append (f"- **Version**: { bug .get ('version' , 'Unknown' )} " )
608+ md_lines .append (f"- **Platform**: { bug .get ('platform' , 'Unknown' )} " )
609+ md_lines .append (f"- **OS**: { bug .get ('op_sys' , 'Unknown' )} " )
610+ md_lines .append (f"- **Created**: { bug .get ('creation_time' , 'Unknown' )} " )
611+ md_lines .append (f"- **Last Updated**: { bug .get ('last_change_time' , 'Unknown' )} " )
612+
613+ if bug .get ("url" ):
614+ md_lines .append (f"- **Related URL**: { bug ['url' ]} " )
615+
616+ if bug .get ("keywords" ):
617+ md_lines .append (f"- **Keywords**: { ', ' .join (bug ['keywords' ])} " )
618+
619+ md_lines .append ("" )
620+
621+ # People Involved
622+ md_lines .append ("## People Involved" )
623+
624+ creator_detail = bug .get ("creator_detail" , {})
625+ if creator_detail :
626+ creator_name = creator_detail .get (
627+ "real_name" ,
628+ creator_detail .get ("nick" , creator_detail .get ("email" , "Unknown" )),
629+ )
630+ md_lines .append (
631+ f"- **Reporter**: { creator_name } ({ creator_detail .get ('email' , 'No email' )} )"
632+ )
633+
634+ assignee_detail = bug .get ("assigned_to_detail" , {})
635+ if assignee_detail :
636+ assignee_name = assignee_detail .get (
637+ "real_name" ,
638+ assignee_detail .get ("nick" , assignee_detail .get ("email" , "Unknown" )),
639+ )
640+ md_lines .append (
641+ f"- **Assignee**: { assignee_name } ({ assignee_detail .get ('email' , 'No email' )} )"
642+ )
643+
644+ # CC List (summarized)
645+ cc_count = len (bug .get ("cc" , []))
646+ if cc_count > 0 :
647+ md_lines .append (f"- **CC Count**: { cc_count } people" )
648+
649+ md_lines .append ("" )
650+
651+ # Dependencies and Relationships
652+ relationships = []
653+ if bug .get ("blocks" ):
654+ relationships .append (f"**Blocks**: { ', ' .join (map (str , bug ['blocks' ]))} " )
655+ if bug .get ("depends_on" ):
656+ relationships .append (
657+ f"**Depends on**: { ', ' .join (map (str , bug ['depends_on' ]))} "
658+ )
659+ if bug .get ("regressed_by" ):
660+ relationships .append (
661+ f"**Regressed by**: { ', ' .join (map (str , bug ['regressed_by' ]))} "
662+ )
663+ if bug .get ("duplicates" ):
664+ relationships .append (
665+ f"**Duplicates**: { ', ' .join (map (str , bug ['duplicates' ]))} "
666+ )
667+ if bug .get ("see_also" ):
668+ relationships .append (f"**See also**: { ', ' .join (bug ['see_also' ])} " )
669+
670+ if relationships :
671+ md_lines .append ("## Bug Relationships" )
672+ for rel in relationships :
673+ md_lines .append (f"- { rel } " )
674+ md_lines .append ("" )
675+
676+ timeline = create_bug_timeline (bug ["comments" ], bug ["history" ])
677+ if timeline :
678+ md_lines .append ("## Bug Timeline" )
679+ md_lines .append ("" )
680+ md_lines .extend (timeline )
681+
682+ return "\n " .join (md_lines )
683+
684+
685+ class Bug :
686+ """Represents a Bugzilla bug from bugzilla.mozilla.org."""
687+
688+ def __init__ (self , data : dict ):
689+ self .metadata = data
690+
691+ @staticmethod
692+ def get (bug_id : int ) -> "Bug" :
693+ from libmozdata .bugzilla import Bugzilla
694+
695+ bugs : list [dict ] = []
696+ Bugzilla (
697+ bug_id ,
698+ include_fields = ["_default" , "comments" , "history" ],
699+ bughandler = lambda bug , data : data .append (bug ),
700+ bugdata = bugs ,
701+ ).get_data ().wait ()
702+
703+ if not bugs :
704+ raise ValueError (f"Bug { bug_id } not found" )
705+
706+ bug_data = bugs [0 ]
707+ assert bug_data ["id" ] == bug_id
708+
709+ return Bug (bug_data )
710+
711+ def to_md (self ) -> str :
712+ """Return a markdown representation of the bug."""
713+ return bug_dict_to_markdown (self .metadata )
714+
715+
520716class ReviewData (ABC ):
521717 NIT_PATTERN = re .compile (r"[^a-zA-Z0-9]nit[\s:,]" , re .IGNORECASE )
522718
0 commit comments