1
1
use std:: {
2
2
collections:: BTreeSet ,
3
+ fmt:: Display ,
3
4
path:: { Path , PathBuf } ,
4
5
process:: Command ,
5
6
} ;
6
7
7
8
use anyhow:: Context ;
8
9
use regex:: Regex ;
10
+ use serde:: { Deserialize , Serialize } ;
9
11
10
- use crate :: { goal, team:: TeamName } ;
12
+ use crate :: {
13
+ goal:: { self , GoalDocument } ,
14
+ team:: TeamName ,
15
+ } ;
16
+
17
+ fn validate_path ( path : & Path ) -> anyhow:: Result < String > {
18
+ if !path. is_dir ( ) {
19
+ return Err ( anyhow:: anyhow!(
20
+ "RFC path should be a directory like src/2024h2"
21
+ ) ) ;
22
+ } ;
23
+
24
+ if path. is_absolute ( ) {
25
+ return Err ( anyhow:: anyhow!( "RFC path should be relative" ) ) ;
26
+ }
27
+
28
+ let timeframe = path
29
+ . components ( )
30
+ . last ( )
31
+ . unwrap ( )
32
+ . as_os_str ( )
33
+ . to_str ( )
34
+ . ok_or_else ( || anyhow:: anyhow!( "invalid path `{}`" , path. display( ) ) ) ?;
35
+
36
+ Ok ( timeframe. to_string ( ) )
37
+ }
11
38
12
39
pub fn generate_comment ( path : & Path ) -> anyhow:: Result < ( ) > {
40
+ let _ = validate_path ( path) ?;
13
41
let goal_documents = goal:: goals_in_dir ( path) ?;
14
- let teams_with_asks: BTreeSet < & TeamName > = goal_documents
15
- . iter ( )
16
- . flat_map ( |g| & g. team_asks )
17
- . flat_map ( |ask| & ask. teams )
18
- . copied ( )
19
- . collect ( ) ;
42
+ let teams_with_asks = teams_with_asks ( & goal_documents) ;
20
43
21
44
for team_name in teams_with_asks {
22
45
let team_data = team_name. data ( ) ;
@@ -38,23 +61,7 @@ pub fn generate_comment(path: &Path) -> anyhow::Result<()> {
38
61
}
39
62
40
63
pub fn generate_rfc ( path : & Path ) -> anyhow:: Result < ( ) > {
41
- if !path. is_dir ( ) {
42
- return Err ( anyhow:: anyhow!(
43
- "RFC path should be a directory like src/2024h2"
44
- ) ) ;
45
- } ;
46
-
47
- if path. is_absolute ( ) {
48
- return Err ( anyhow:: anyhow!( "RFC path should be relative" ) ) ;
49
- }
50
-
51
- let timeframe = path
52
- . components ( )
53
- . last ( )
54
- . unwrap ( )
55
- . as_os_str ( )
56
- . to_str ( )
57
- . ok_or_else ( || anyhow:: anyhow!( "invalid path `{}`" , path. display( ) ) ) ?;
64
+ let timeframe = & validate_path ( path) ?;
58
65
59
66
// run mdbook build
60
67
Command :: new ( "mdbook" ) . arg ( "build" ) . status ( ) ?;
@@ -88,3 +95,166 @@ pub fn generate_rfc(path: &Path) -> anyhow::Result<()> {
88
95
89
96
Ok ( ( ) )
90
97
}
98
+
99
+ pub fn generate_issues ( repository : & str , path : & Path , dry_run : bool ) -> anyhow:: Result < ( ) > {
100
+ let _ = validate_path ( path) ?;
101
+
102
+ let goal_documents = goal:: goals_in_dir ( path) ?;
103
+ let teams_with_asks = teams_with_asks ( & goal_documents) ;
104
+ // let issues: Vec<_> = goal_documents
105
+ // .iter()
106
+ // .map(|goal_document| {
107
+ // let title = format!("Goal: {}", goal_document.title);
108
+ // let owners = goal_document.metadata.owner_usernames();
109
+ // let body = goal_document.description.clone();
110
+ // let teams = goal_document
111
+ // .team_asks
112
+ // .iter()
113
+ // .flat_map(|ask| &ask.teams)
114
+ // .copied()
115
+ // .collect::<BTreeSet<&TeamName>>();
116
+
117
+ // GithubIssue {
118
+ // title,
119
+ // owners,
120
+ // body,
121
+ // teams,
122
+ // }
123
+ // })
124
+ // .collect();
125
+
126
+ let mut actions = initialize_labels ( repository, & teams_with_asks) ?;
127
+
128
+ eprintln ! ( "Actions to be executed:" ) ;
129
+ for action in & actions {
130
+ eprintln ! ( "* {action}" ) ;
131
+ }
132
+
133
+ if !dry_run {
134
+ for action in actions {
135
+ action. execute ( repository) ?;
136
+ }
137
+ }
138
+
139
+ Ok ( ( ) )
140
+ }
141
+
142
+ #[ derive( Debug , PartialEq , Eq , PartialOrd , Ord ) ]
143
+ pub struct GithubIssue {
144
+ pub title : String ,
145
+ pub owners : Vec < String > ,
146
+ pub body : String ,
147
+ pub teams : BTreeSet < & ' static TeamName > ,
148
+ }
149
+
150
+ #[ derive( Debug , PartialEq , Eq , PartialOrd , Ord ) ]
151
+ enum GithubAction {
152
+ CreateLabel { name : String , color : String } ,
153
+ CreateIssue { issue : GithubIssue } ,
154
+ }
155
+
156
+ #[ derive( Debug , Serialize , Deserialize ) ]
157
+ struct GhLabel {
158
+ name : String ,
159
+ color : String ,
160
+ }
161
+
162
+ fn list_labels ( repository : & str ) -> anyhow:: Result < Vec < GhLabel > > {
163
+ let output = Command :: new ( "gh" )
164
+ . arg ( "-R" )
165
+ . arg ( repository)
166
+ . arg ( "label" )
167
+ . arg ( "list" )
168
+ . arg ( "--json" )
169
+ . arg ( "name,color" )
170
+ . output ( ) ?;
171
+
172
+ let labels: Vec < GhLabel > = serde_json:: from_slice ( & output. stdout ) ?;
173
+
174
+ Ok ( labels)
175
+ }
176
+
177
+ /// Initializes the required `T-<team>` labels on the repository.
178
+ /// Warns if the labels are found with wrong color.
179
+ pub fn initialize_labels (
180
+ repository : & str ,
181
+ teams_with_asks : & BTreeSet < & TeamName > ,
182
+ ) -> anyhow:: Result < BTreeSet < GithubAction > > {
183
+ const TEAM_LABEL_COLOR : & str = "bfd4f2" ;
184
+
185
+ let existing_labels = list_labels ( repository) ?;
186
+
187
+ Ok ( teams_with_asks
188
+ . iter ( )
189
+ . flat_map ( |team| {
190
+ let label_name = team. gh_label ( ) ;
191
+
192
+ if let Some ( existing_label) = existing_labels
193
+ . iter ( )
194
+ . find ( |label| label. name == label_name)
195
+ {
196
+ if existing_label. color == TEAM_LABEL_COLOR {
197
+ return None ;
198
+ }
199
+ }
200
+
201
+ Some ( GithubAction :: CreateLabel {
202
+ name : label_name,
203
+ color : TEAM_LABEL_COLOR . to_string ( ) ,
204
+ } )
205
+ } )
206
+ . collect ( ) )
207
+ }
208
+
209
+ fn teams_with_asks ( goal_documents : & [ GoalDocument ] ) -> BTreeSet < & ' static TeamName > {
210
+ goal_documents
211
+ . iter ( )
212
+ . flat_map ( |g| & g. team_asks )
213
+ . flat_map ( |ask| & ask. teams )
214
+ . copied ( )
215
+ . collect ( )
216
+ }
217
+
218
+ impl Display for GithubAction {
219
+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
220
+ match self {
221
+ GithubAction :: CreateLabel { name, color } => {
222
+ write ! ( f, "create label `{}` with color `{}`" , name, color)
223
+ }
224
+ GithubAction :: CreateIssue { issue } => {
225
+ write ! ( f, "create issue `{}`" , issue. title)
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ impl GithubAction {
232
+ pub fn execute ( self , repository : & str ) -> anyhow:: Result < ( ) > {
233
+ match self {
234
+ GithubAction :: CreateLabel { name, color } => {
235
+ let output = Command :: new ( "gh" )
236
+ . arg ( "-R" )
237
+ . arg ( repository)
238
+ . arg ( "label" )
239
+ . arg ( "create" )
240
+ . arg ( & name)
241
+ . arg ( "--color" )
242
+ . arg ( & color)
243
+ . arg ( "--force" )
244
+ . output ( ) ?;
245
+
246
+ if !output. status . success ( ) {
247
+ Err ( anyhow:: anyhow!(
248
+ "failed to create label `{}`: {}" ,
249
+ name,
250
+ String :: from_utf8_lossy( & output. stderr)
251
+ ) )
252
+ } else {
253
+ Ok ( ( ) )
254
+ }
255
+ }
256
+
257
+ GithubAction :: CreateIssue { issue } => todo ! ( ) ,
258
+ }
259
+ }
260
+ }
0 commit comments