Coverage for src/pullapprove/cli.py: 0%

79 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-10 15:55 -0500

1import os 

2import sys 

3from pathlib import Path 

4from textwrap import dedent 

5 

6import click 

7from pydantic import ValidationError 

8 

9from . import git 

10from .config import CONFIG_FILENAME, CONFIG_FILENAME_PREFIX, ConfigModel, ConfigModels 

11from .matches import match_diff, match_files 

12 

13 

14@click.group() 

15def cli(): 

16 pass 

17 

18 

19@cli.command() 

20@click.option("--filename", default=CONFIG_FILENAME, help="Configuration filename") 

21def init(filename): 

22 config_path = Path(filename) 

23 if config_path.exists(): 

24 click.secho(f"{CONFIG_FILENAME} already exists!", fg="red") 

25 sys.exit(1) 

26 

27 # Could we use blame to guess? 

28 # go straight to agent? 

29 # gh auth status can give us the user? or ask what's their username? 

30 # keep it simple - agent can do more when I get to it 

31 

32 contents = """ 

33 [[scopes]] 

34 name = "default" 

35 paths = ["**/*"] 

36 request = 1 

37 require = 1 

38 reviewers = ["<YOU>"] 

39 

40 [[scopes]] 

41 name = "pullapprove" 

42 paths = ["**/CODEREVIEW.toml"] 

43 request = 1 

44 require = 1 

45 """ 

46 config_path.write_text(dedent(contents).strip() + "\n") 

47 click.secho(f"Created {filename}") 

48 

49 

50@cli.command() 

51@click.argument("path", type=click.Path(exists=True), default=".") 

52@click.option("--quiet", is_flag=True) 

53def validate(path, quiet): 

54 """ 

55 Locally validate config files 

56 """ 

57 

58 errors = {} 

59 

60 configs = ConfigModels(root={}) 

61 

62 for root, _, files in os.walk(path): 

63 for f in files: 

64 if f.startswith(CONFIG_FILENAME_PREFIX): 

65 config_path = Path(root) / f 

66 

67 if not quiet: 

68 click.echo(config_path, nl=False) 

69 try: 

70 configs.add_config( 

71 ConfigModel.from_filesystem(config_path), config_path 

72 ) 

73 

74 if not quiet: 

75 click.secho(" -> OK", fg="green") 

76 except ValidationError as e: 

77 if not quiet: 

78 click.secho(" -> ERROR", fg="red") 

79 

80 errors[config_path] = e 

81 

82 for path, error in errors.items(): 

83 click.secho(str(path), fg="red") 

84 print(error) 

85 

86 if errors: 

87 sys.exit(1) 

88 

89 return configs 

90 

91 

92@cli.command() 

93@click.argument("path", type=click.Path(exists=True), default=".") 

94@click.option("--changed", is_flag=True) 

95@click.option("--json", "as_json", is_flag=True) 

96@click.option("--by", type=click.Choice(["scope", "path"]), default="path") 

97@click.pass_context 

98def ls(ctx, path, changed, as_json, by): 

99 """ 

100 List files and lines that match scopes 

101 """ 

102 configs = ctx.invoke(validate, path=path, quiet=True) 

103 

104 if changed: 

105 iterator = git.git_ls_changes(path) 

106 else: 

107 iterator = git.git_ls_files(path) 

108 

109 results = match_files(configs, iterator) 

110 if as_json: 

111 click.echo(results.model_dump_json(indent=2)) 

112 else: 

113 results.print(by=by) 

114 

115 

116@cli.command() 

117@click.argument("path", type=click.Path(exists=True), default=".") 

118@click.option("--json", "as_json", is_flag=True) 

119@click.option("--staged", is_flag=True) 

120@click.option("--by", type=click.Choice(["scope", "path"]), default="path") 

121@click.pass_context 

122def diff(ctx, path, as_json, staged, by): 

123 configs = ctx.invoke(validate, path=path, quiet=True) 

124 

125 diff_args = [] 

126 if staged: 

127 diff_args.append("--staged") 

128 

129 diff_stream = git.git_diff_stream(path, *diff_args) 

130 

131 results, _ = match_diff(configs, diff_stream) 

132 if as_json: 

133 click.echo(results.model_dump_json(indent=2)) 

134 else: 

135 results.print(by=by) 

136 

137 

138# @cli.command() 

139# @click.option("--check", is_flag=True) 

140# @click.argument("path", type=click.Path(exists=True), default=".") 

141# def coverage(path, check): 

142# config = load_root_config() 

143# num_matched = 0 

144# num_total = 0 

145 

146# for f in git.git_ls_files(path): 

147# file_path = Path(f) 

148# matched = False 

149 

150# # TODO doesn't include line patterns... 

151 

152# for scope in config.config.scopes: 

153# if scope.matches_path(file_path): 

154# matched = True 

155# break 

156 

157# if matched: 

158# num_matched += 1 

159# num_total += 1 

160 

161# percentage = f"{num_matched / num_total:.1%}" 

162# click.echo(f"{num_matched}/{num_total} files covered ({percentage})") 

163 

164# if check and num_matched != num_total: 

165# sys.exit(1) 

166 

167 

168# list - find open PRs, find status url and send json request (needs PA token)