Coverage for src / rhiza / commands / summarise / __init__.py: 100%

37 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-07-16 12:51 +0000

1"""Command for generating PR descriptions from staged changes. 

2 

3This package analyses staged git changes and generates structured PR 

4descriptions for rhiza sync operations. Responsibilities are split across two 

5private modules: 

6 

7* :mod:`._gather` — collects staged changes and reads template metadata. 

8* :mod:`._render` — formats the gathered data into markdown, plain text, JSON, 

9 or a custom Jinja2 template. 

10 

11The orchestration (:func:`generate_pr_description` and :func:`summarise`) lives 

12here and wires the two together. 

13""" 

14 

15from datetime import datetime 

16from pathlib import Path 

17 

18from loguru import logger 

19 

20from ._gather import ( 

21 _categorize_single_file, 

22 _TemplateInfo, 

23 categorize_files, 

24 get_last_sync_date, 

25 get_staged_changes, 

26 get_template_info, 

27 run_git_command, 

28) 

29from ._render import ( 

30 SummariseOptions, 

31 _generate_jinja2_output, 

32 _generate_json_output, 

33 _generate_plain_output, 

34 _markdown_body, 

35) 

36 

37__all__ = [ 

38 "SummariseOptions", 

39 "_TemplateInfo", 

40 "_categorize_single_file", 

41 "categorize_files", 

42 "generate_pr_description", 

43 "get_last_sync_date", 

44 "get_staged_changes", 

45 "get_template_info", 

46 "run_git_command", 

47 "summarise", 

48] 

49 

50 

51def generate_pr_description(repo_path: Path, options: SummariseOptions | None = None) -> str: 

52 """Generate PR description based on staged changes. 

53 

54 Args: 

55 repo_path: Path to the repository 

56 options: Output customisation options. Defaults to :class:`SummariseOptions` 

57 with all fields at their defaults (markdown format, with header / footer / 

58 categories, no custom title, staged-index diff). 

59 

60 Returns: 

61 Formatted PR description 

62 """ 

63 opts = options or SummariseOptions() 

64 

65 changes = get_staged_changes(repo_path, compare_ref=opts.compare_ref) 

66 template_repo, template_branch = get_template_info(repo_path) 

67 last_sync = get_last_sync_date(repo_path, template_repo=template_repo) 

68 

69 all_changed_files = changes["added"] + changes["modified"] + changes["deleted"] 

70 categories = categorize_files(all_changed_files) if all_changed_files else {} 

71 

72 tmpl = _TemplateInfo(repo=template_repo, branch=template_branch, last_sync=last_sync) 

73 

74 # Custom Jinja2 template takes full precedence over all other options 

75 if opts.jinja2_template: 

76 context = { 

77 "template_repo": tmpl.repo, 

78 "template_branch": tmpl.branch, 

79 "last_sync": tmpl.last_sync, 

80 "sync_date": datetime.now().astimezone().isoformat(), 

81 "changes": changes, 

82 "categories": categories, 

83 "title": opts.title, 

84 } 

85 return _generate_jinja2_output(opts.jinja2_template, context) 

86 

87 if opts.output_format == "json": 

88 return _generate_json_output(changes, categories, tmpl) 

89 

90 if opts.output_format == "plain": 

91 return _generate_plain_output(changes, categories, tmpl, opts) 

92 

93 return _markdown_body(changes, categories, tmpl, opts) 

94 

95 

96def summarise( 

97 target: Path, 

98 output: Path | None = None, 

99 *, 

100 options: SummariseOptions | None = None, 

101) -> None: 

102 """Generate a summary of staged changes for rhiza sync operations. 

103 

104 This command analyzes staged git changes and generates a structured 

105 PR description with: 

106 - Summary statistics (files added/modified/deleted) 

107 - Changes categorized by type (workflows, configs, docs, tests, etc.) 

108 - Template repository information 

109 - Last sync date 

110 

111 Args: 

112 target: Path to the target repository. 

113 output: Optional output file path. If not provided, prints to stdout. 

114 options: Output customisation options. Defaults to :class:`SummariseOptions` 

115 with all fields at their defaults. 

116 """ 

117 target = target.resolve() 

118 logger.info(f"Target repository: {target}") 

119 

120 # Check if target is a git repository 

121 if not (target / ".git").is_dir(): 

122 err_msg = f"Target directory is not a git repository: {target}" 

123 logger.error(err_msg) 

124 logger.error("Initialize a git repository with 'git init' first") 

125 raise RuntimeError(err_msg) 

126 

127 description = generate_pr_description(target, options) 

128 

129 if output: 

130 output_path = output.resolve() 

131 output_path.write_text(description, encoding="utf-8") 

132 logger.success(f"PR description written to {output_path}") 

133 else: 

134 print(description) 

135 

136 logger.success("Summary generated successfully")