Skip to content

Commit a7abd05

Browse files
committed
Implementing @bors info command
1 parent a67338b commit a7abd05

9 files changed

+277
-3
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
target/
2+
.env

.sqlx/query-2eaf959d9345ee1d1baf5453b693f0914014029977243d15ef233e8fba49f2d5.json

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/bors/command/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub enum BorsCommand {
4949
TryCancel,
5050
/// Set the priority of a commit.
5151
SetPriority(Priority),
52+
/// Get information about the current PR.
53+
Info,
5254
/// Delegate approval authority to the pull request author.
5355
Delegate,
5456
/// Revoke any previously granted delegation.

src/bors/command/parser.rs

+24-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ impl CommandParser {
5151
parser_ping,
5252
parser_try_cancel,
5353
parser_try,
54+
parser_info,
5455
parse_delegate_author,
5556
parse_undelegate,
5657
];
@@ -132,7 +133,6 @@ fn parse_parts(input: &str) -> Result<Vec<CommandPart>, CommandParseError> {
132133
}
133134

134135
/// Parsers
135-
136136
/// Parses "@bors r+ <p=priority>"
137137
fn parse_self_approve<'a>(command: &'a str, parts: &[CommandPart<'a>]) -> ParseResult<'a> {
138138
if command != "r+" {
@@ -342,6 +342,15 @@ fn parse_priority_arg<'a>(parts: &[CommandPart<'a>]) -> Result<Option<u32>, Comm
342342
Ok(priority)
343343
}
344344

345+
/// Parses "@bors info"
346+
fn parser_info<'a>(command: &'a str, _parts: &[CommandPart<'a>]) -> ParseResult<'a> {
347+
if command == "info" {
348+
Some(Ok(BorsCommand::Info))
349+
} else {
350+
None
351+
}
352+
}
353+
345354
#[cfg(test)]
346355
mod tests {
347356
use crate::bors::command::parser::{CommandParseError, CommandParser};
@@ -835,6 +844,20 @@ line two
835844
"###)
836845
}
837846

847+
#[test]
848+
fn parse_info() {
849+
let cmds = parse_commands("@bors info");
850+
assert_eq!(cmds.len(), 1);
851+
assert!(matches!(cmds[0], Ok(BorsCommand::Info)));
852+
}
853+
854+
#[test]
855+
fn parse_info_unknown_arg() {
856+
let cmds = parse_commands("@bors info a");
857+
assert_eq!(cmds.len(), 1);
858+
assert!(matches!(cmds[0], Ok(BorsCommand::Info)));
859+
}
860+
838861
#[test]
839862
fn parse_try_cancel() {
840863
let cmds = parse_commands("@bors try cancel");

src/bors/handlers/help.rs

+5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub(super) async fn command_help(
2828
BorsCommand::TryCancel,
2929
BorsCommand::Ping,
3030
BorsCommand::Help,
31+
BorsCommand::Info,
3132
]
3233
.into_iter()
3334
.map(|help| format!("- {}", get_command_help(help)))
@@ -73,6 +74,9 @@ fn get_command_help(command: BorsCommand) -> String {
7374
BorsCommand::TryCancel => {
7475
"`try cancel`: Cancel a running try build"
7576
}
77+
BorsCommand::Info => {
78+
"`info`: Get information about the current PR including delegation, priority, merge status, and try build status"
79+
}
7680
};
7781
help.to_string()
7882
}
@@ -96,6 +100,7 @@ mod tests {
96100
- `try cancel`: Cancel a running try build
97101
- `ping`: Check if the bot is alive
98102
- `help`: Print this help message
103+
- `info`: Get information about the current PR including delegation, priority, merge status, and try build status
99104
");
100105
Ok(tester)
101106
})

src/bors/handlers/info.rs

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
use crate::bors::Comment;
2+
use crate::bors::RepositoryState;
3+
use crate::database::PgDbClient;
4+
use crate::github::PullRequest;
5+
use std::sync::Arc;
6+
7+
pub(super) async fn command_info(
8+
repo: Arc<RepositoryState>,
9+
pr: &PullRequest,
10+
db: Arc<PgDbClient>,
11+
) -> anyhow::Result<()> {
12+
// Geting PR info from database
13+
let pr_model = db
14+
.get_or_create_pull_request(repo.client.repository(), pr.number)
15+
.await?;
16+
17+
// Building the info message
18+
let mut info_lines = Vec::new();
19+
20+
// Approval info
21+
if let Some(approved_by) = pr_model.approved_by {
22+
info_lines.push(format!("- **Approved by:** @{}", approved_by));
23+
} else {
24+
info_lines.push("- **Not Approved:**".to_string());
25+
}
26+
27+
// Priority info
28+
if let Some(priority) = pr_model.priority {
29+
info_lines.push(format!("- **Priority:** {}", priority));
30+
} else {
31+
info_lines.push("- **Priority:** Not set".to_string());
32+
}
33+
34+
// Build status
35+
if let Some(try_build) = pr_model.try_build {
36+
info_lines.push(format!("- **Try build branch:** {}", try_build.branch));
37+
38+
if let Ok(urls) = db.get_workflow_urls_for_build(&try_build).await {
39+
info_lines.extend(
40+
urls.into_iter()
41+
.map(|url| format!("- **Workflow URL:** {}", url)),
42+
);
43+
}
44+
}
45+
46+
// Joining all lines
47+
let info = info_lines.join("\n");
48+
49+
// Post the comment
50+
repo.client
51+
.post_comment(pr.number, Comment::new(info))
52+
.await?;
53+
54+
Ok(())
55+
}
56+
57+
#[cfg(test)]
58+
mod tests {
59+
use crate::tests::mocks::{BorsBuilder, User, World};
60+
61+
fn create_world_with_approve_config() -> World {
62+
let world = World::default();
63+
world.default_repo().lock().set_config(
64+
r#"
65+
[labels]
66+
approve = ["+approved"]
67+
"#,
68+
);
69+
world
70+
}
71+
72+
#[sqlx::test]
73+
async fn info_for_unapproved_pr(pool: sqlx::PgPool) {
74+
BorsBuilder::new(pool)
75+
.world(World::default())
76+
.run_test(|mut tester| async {
77+
tester.post_comment("@bors info").await?;
78+
assert_eq!(
79+
tester.get_comment().await?,
80+
"- **Not Approved:**\n- **Priority:** Not set"
81+
);
82+
Ok(tester)
83+
})
84+
.await;
85+
}
86+
87+
#[sqlx::test]
88+
async fn info_for_approved_pr(pool: sqlx::PgPool) {
89+
BorsBuilder::new(pool)
90+
.world(create_world_with_approve_config())
91+
.run_test(|mut tester| async {
92+
// First approve the PR
93+
tester.post_comment("@bors r+").await?;
94+
tester.expect_comments(1).await;
95+
96+
// Then check info
97+
tester.post_comment("@bors info").await?;
98+
assert_eq!(
99+
tester.get_comment().await?,
100+
format!(
101+
"- **Approved by:** @{}\n- **Priority:** Not set",
102+
User::default_user().name
103+
)
104+
);
105+
Ok(tester)
106+
})
107+
.await;
108+
}
109+
110+
#[sqlx::test]
111+
async fn info_for_pr_with_priority(pool: sqlx::PgPool) {
112+
BorsBuilder::new(pool)
113+
.world(create_world_with_approve_config())
114+
.run_test(|mut tester| async {
115+
// Set priority
116+
tester.post_comment("@bors p=5").await?;
117+
tester
118+
.wait_for(|| async {
119+
let pr = tester.get_default_pr().await?;
120+
Ok(pr.priority == Some(5))
121+
})
122+
.await?;
123+
124+
// Check info
125+
tester.post_comment("@bors info").await?;
126+
assert_eq!(
127+
tester.get_comment().await?,
128+
"- **Not Approved:**\n- **Priority:** 5"
129+
);
130+
Ok(tester)
131+
})
132+
.await;
133+
}
134+
135+
#[sqlx::test]
136+
async fn info_for_pr_with_try_build(pool: sqlx::PgPool) {
137+
BorsBuilder::new(pool)
138+
.world(create_world_with_approve_config())
139+
.run_test(|mut tester| async {
140+
// Create a try build
141+
tester.post_comment("@bors try").await?;
142+
tester.expect_comments(1).await;
143+
144+
// Check info
145+
tester.post_comment("@bors info").await?;
146+
assert_eq!(
147+
tester.get_comment().await?,
148+
"- **Not Approved:**\n- **Priority:** Not set\n- **Try build branch:** automation/bors/try"
149+
);
150+
Ok(tester)
151+
})
152+
.await;
153+
}
154+
155+
#[sqlx::test]
156+
async fn info_for_pr_with_everything(pool: sqlx::PgPool) {
157+
BorsBuilder::new(pool)
158+
.world(create_world_with_approve_config())
159+
.run_test(|mut tester| async {
160+
// Approve with priority
161+
tester.post_comment("@bors r+ p=10").await?;
162+
tester.expect_comments(1).await;
163+
164+
// Create a try build
165+
tester.post_comment("@bors try").await?;
166+
tester.expect_comments(1).await;
167+
168+
// Check info
169+
tester.post_comment("@bors info").await?;
170+
assert_eq!(
171+
tester.get_comment().await?,
172+
format!(
173+
"- **Approved by:** @{}\n- **Priority:** 10\n- **Try build branch:** automation/bors/try",
174+
User::default_user().name
175+
)
176+
);
177+
Ok(tester)
178+
})
179+
.await;
180+
}
181+
}

src/bors/handlers/mod.rs

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use tracing::Instrument;
88
use crate::bors::command::{BorsCommand, CommandParseError};
99
use crate::bors::event::{BorsGlobalEvent, BorsRepositoryEvent, PullRequestComment};
1010
use crate::bors::handlers::help::command_help;
11+
use crate::bors::handlers::info::command_info;
1112
use crate::bors::handlers::ping::command_ping;
1213
use crate::bors::handlers::refresh::refresh_repository;
1314
use crate::bors::handlers::review::{
@@ -26,6 +27,7 @@ use crate::{load_repositories, PgDbClient, TeamApiClient};
2627
use crate::tests::util::TestSyncMarker;
2728

2829
mod help;
30+
mod info;
2931
mod labels;
3032
mod ping;
3133
mod refresh;
@@ -278,6 +280,12 @@ async fn handle_comment(
278280
.instrument(span)
279281
.await
280282
}
283+
BorsCommand::Info => {
284+
let span = tracing::info_span!("Info");
285+
command_info(repo, &pull_request, database)
286+
.instrument(span)
287+
.await
288+
}
281289
};
282290
if result.is_err() {
283291
return result.context("Cannot execute Bors command");

src/database/client.rs

+16-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use crate::github::{CommitSha, GithubRepoName};
99
use super::operations::{
1010
approve_pull_request, create_build, create_pull_request, create_workflow,
1111
delegate_pull_request, find_build, find_pr_by_build, get_pull_request, get_running_builds,
12-
get_workflows_for_build, set_pr_priority, unapprove_pull_request, undelegate_pull_request,
13-
update_build_status, update_pr_build_id, update_workflow_status,
12+
get_workflow_urls_for_build, get_workflows_for_build, set_pr_priority, unapprove_pull_request,
13+
undelegate_pull_request, update_build_status, update_pr_build_id, update_workflow_status,
1414
};
1515
use super::RunId;
1616

@@ -186,4 +186,18 @@ impl PgDbClient {
186186
.collect::<Vec<_>>();
187187
Ok(workflows)
188188
}
189+
pub async fn get_pull_request(
190+
&self,
191+
repo: &GithubRepoName,
192+
pr_number: PullRequestNumber,
193+
) -> anyhow::Result<Option<PullRequestModel>> {
194+
get_pull_request(&self.pool, repo, pr_number).await
195+
}
196+
197+
pub async fn get_workflow_urls_for_build(
198+
&self,
199+
build: &BuildModel,
200+
) -> anyhow::Result<Vec<String>> {
201+
get_workflow_urls_for_build(&self.pool, build.id).await
202+
}
189203
}

src/database/operations.rs

+18
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,24 @@ WHERE build.id = $1
369369
Ok(workflows)
370370
}
371371

372+
pub(crate) async fn get_workflow_urls_for_build(
373+
executor: impl PgExecutor<'_>,
374+
build_id: i32,
375+
) -> anyhow::Result<Vec<String>> {
376+
let results = sqlx::query!(
377+
r#"
378+
SELECT url
379+
FROM workflow
380+
WHERE build_id = $1
381+
"#,
382+
build_id
383+
)
384+
.fetch_optional(executor)
385+
.await?;
386+
387+
Ok(results.into_iter().map(|r| r.url).collect())
388+
}
389+
372390
#[cfg(test)]
373391
pub(crate) async fn get_all_workflows(
374392
executor: impl PgExecutor<'_>,

0 commit comments

Comments
 (0)