Skip to content

Commit 4a07239

Browse files
authored
resort to paginated requests for changed files (#37)
resolves #32
1 parent 740bdf2 commit 4a07239

File tree

11 files changed

+347
-16
lines changed

11 files changed

+347
-16
lines changed

.github/workflows/run-dev-tests.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -80,25 +80,25 @@ jobs:
8080
run: sudo apt-get update
8181

8282
- name: Install clang v7
83-
if: matrix.os == 'Linux'
83+
if: runner.os == 'Linux'
8484
uses: cpp-linter/cpp_linter_rs/install-clang-action@main
8585
with:
8686
version: '7'
8787

8888
- name: Collect Coverage for clang v7
89-
if: matrix.os == 'Linux'
89+
if: runner.os == 'Linux'
9090
env:
9191
CLANG_VERSION: '7'
9292
run: just test
9393

9494
- name: Install clang v8
95-
if: matrix.os == 'Linux'
95+
if: runner.os == 'Linux'
9696
uses: cpp-linter/cpp_linter_rs/install-clang-action@main
9797
with:
9898
version: '8'
9999

100100
- name: Collect Coverage for clang v8
101-
if: matrix.os == 'Linux'
101+
if: runner.os == 'Linux'
102102
env:
103103
CLANG_VERSION: '8'
104104
run: just test

cpp-linter-lib/src/rest_api/github_api.rs

+95-9
Original file line numberDiff line numberDiff line change
@@ -192,20 +192,40 @@ impl RestApiClient for GithubApiClient {
192192
.unwrap();
193193
let mut diff_header = HeaderMap::new();
194194
diff_header.insert("Accept", "application/vnd.github.diff".parse().unwrap());
195-
let request =
196-
Self::make_api_request(&self.client, url, Method::GET, None, Some(diff_header));
195+
let request = Self::make_api_request(
196+
&self.client,
197+
url.as_str(),
198+
Method::GET,
199+
None,
200+
Some(diff_header),
201+
);
197202
let response = Self::send_api_request(
198203
self.client.clone(),
199204
request,
200-
true,
205+
false,
201206
self.rate_limit_headers.to_owned(),
202207
0,
203208
)
204-
.await
205-
.unwrap()
206-
.text;
207-
208-
parse_diff_from_buf(response.as_bytes(), file_filter)
209+
.await;
210+
match response {
211+
Some(response) => {
212+
if response.status.is_success() {
213+
return parse_diff_from_buf(response.text.as_bytes(), file_filter);
214+
} else {
215+
let endpoint = if is_pr {
216+
Url::parse(format!("{}/files", url.as_str()).as_str())
217+
.expect("failed to parse URL endpoint")
218+
} else {
219+
url
220+
};
221+
self.get_changed_files_paginated(endpoint, file_filter)
222+
.await
223+
}
224+
}
225+
None => {
226+
panic!("Failed to connect with GitHub server to get list of changed files.")
227+
}
228+
}
209229
} else {
210230
// get diff from libgit2 API
211231
let repo = open_repo(".")
@@ -215,6 +235,54 @@ impl RestApiClient for GithubApiClient {
215235
}
216236
}
217237

238+
async fn get_changed_files_paginated(
239+
&self,
240+
url: Url,
241+
file_filter: &FileFilter,
242+
) -> Vec<FileObj> {
243+
let mut url = Some(Url::parse_with_params(url.as_str(), &[("page", "1")]).unwrap());
244+
let mut files = vec![];
245+
while let Some(ref endpoint) = url {
246+
let request =
247+
Self::make_api_request(&self.client, endpoint.as_str(), Method::GET, None, None);
248+
let response = Self::send_api_request(
249+
self.client.clone(),
250+
request,
251+
true,
252+
self.rate_limit_headers.clone(),
253+
0,
254+
)
255+
.await;
256+
if let Some(response) = response {
257+
url = Self::try_next_page(&response.headers);
258+
let files_list = if self.event_name != "pull_request" {
259+
let json_value: PushEventFiles = serde_json::from_str(&response.text)
260+
.expect("Failed to deserialize list of changed files from json response");
261+
json_value.files
262+
} else {
263+
serde_json::from_str::<Vec<GithubChangedFile>>(&response.text).expect(
264+
"Failed to deserialize list of file changes from Pull Request event.",
265+
)
266+
};
267+
for file in files_list {
268+
if let Some(patch) = file.patch {
269+
let diff = format!(
270+
"diff --git a/{old} b/{new}\n--- a/{old}\n+++ b/{new}\n{patch}",
271+
old = file.previous_filename.unwrap_or(file.filename.clone()),
272+
new = file.filename,
273+
);
274+
if let Some(file_obj) =
275+
parse_diff_from_buf(diff.as_bytes(), file_filter).first()
276+
{
277+
files.push(file_obj.to_owned());
278+
}
279+
}
280+
}
281+
}
282+
}
283+
files
284+
}
285+
218286
async fn post_feedback(
219287
&self,
220288
files: &[Arc<Mutex<FileObj>>],
@@ -671,6 +739,24 @@ impl From<Suggestion> for ReviewDiffComment {
671739
}
672740
}
673741

742+
/// A structure for deserializing a single changed file in a CI event.
743+
#[derive(Debug, Deserialize, PartialEq, Clone)]
744+
struct GithubChangedFile {
745+
/// The file's name (including relative path to repo root)
746+
pub filename: String,
747+
/// If renamed, this will be the file's old name as a [`Some`], otherwise [`None`].
748+
pub previous_filename: Option<String>,
749+
/// The individual patch that describes the file's changes.
750+
pub patch: Option<String>,
751+
}
752+
753+
/// A structure for deserializing a Push event's changed files.
754+
#[derive(Debug, Deserialize, PartialEq, Clone)]
755+
struct PushEventFiles {
756+
/// The list of changed files.
757+
pub files: Vec<GithubChangedFile>,
758+
}
759+
674760
/// A structure for deserializing a comment from a response's json.
675761
#[derive(Debug, Deserialize, PartialEq, Clone)]
676762
struct PullRequestInfo {
@@ -840,7 +926,7 @@ mod test {
840926
}
841927
let request =
842928
GithubApiClient::make_api_request(&client.client, url, Method::GET, None, None);
843-
let _response = GithubApiClient::send_api_request(
929+
GithubApiClient::send_api_request(
844930
client.client.clone(),
845931
request,
846932
true,

cpp-linter-lib/src/rest_api/mod.rs

+10
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,16 @@ pub trait RestApiClient {
233233
file_filter: &FileFilter,
234234
) -> impl Future<Output = Vec<FileObj>>;
235235

236+
/// A way to get the list of changed files using REST API calls that employ a paginated response.
237+
///
238+
/// This is a helper to [`RestApiClient::get_list_of_changed_files()`] but takes a formulated URL
239+
/// endpoint based on the context of the triggering CI event.
240+
fn get_changed_files_paginated(
241+
&self,
242+
url: Url,
243+
file_filter: &FileFilter,
244+
) -> impl Future<Output = Vec<FileObj>>;
245+
236246
/// Makes a comment in MarkDown syntax based on the concerns in `format_advice` and
237247
/// `tidy_advice` about the given set of `files`.
238248
///

cpp-linter-lib/tests/comments.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ async fn setup(lib_root: &Path, test_params: &TestParams) {
230230
}
231231

232232
async fn test_comment(test_params: &TestParams) {
233-
let tmp_dir = create_test_space();
233+
let tmp_dir = create_test_space(true);
234234
let lib_root = env::current_dir().unwrap();
235235
env::set_current_dir(tmp_dir.path()).unwrap();
236236
setup(&lib_root, test_params).await;

cpp-linter-lib/tests/common/mod.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use tempfile::TempDir;
2020
///
2121
/// The returned directory object will automatically delete the
2222
/// temporary folder when it is dropped out of scope.
23-
pub fn create_test_space() -> TempDir {
23+
pub fn create_test_space(setup_meson: bool) -> TempDir {
2424
let tmp = TempDir::new().unwrap();
2525
fs::create_dir(tmp.path().join("src")).unwrap();
2626
let src = fs::read_dir("tests/demo").unwrap();
@@ -32,6 +32,10 @@ pub fn create_test_space() -> TempDir {
3232
}
3333
}
3434

35+
if !setup_meson {
36+
return tmp;
37+
}
38+
3539
// generate compilation database with meson (& ninja)
3640
let test_dir = tmp.path().join("src");
3741
let mut cmd = Command::new("meson");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
mod common;
2+
use chrono::Utc;
3+
use common::{create_test_space, mock_server};
4+
use mockito::Matcher;
5+
use tempfile::{NamedTempFile, TempDir};
6+
7+
use cpp_linter_lib::{
8+
common_fs::FileFilter,
9+
rest_api::{github_api::GithubApiClient, RestApiClient},
10+
};
11+
use std::{env, io::Write, path::Path};
12+
13+
#[derive(PartialEq)]
14+
enum EventType {
15+
Push,
16+
PullRequest(u64),
17+
}
18+
19+
const REPO: &str = "cpp-linter/test-cpp-linter-action";
20+
const SHA: &str = "DEADBEEF";
21+
const TOKEN: &str = "123456";
22+
const RESET_RATE_LIMIT_HEADER: &str = "x-ratelimit-reset";
23+
const REMAINING_RATE_LIMIT_HEADER: &str = "x-ratelimit-remaining";
24+
25+
async fn get_paginated_changes(lib_root: &Path, event_type: EventType) {
26+
env::set_var("GITHUB_REPOSITORY", REPO);
27+
env::set_var("GITHUB_SHA", SHA);
28+
env::set_var("GITHUB_TOKEN", TOKEN);
29+
env::set_var("CI", "true");
30+
env::set_var(
31+
"GITHUB_EVENT_NAME",
32+
if event_type == EventType::Push {
33+
"push"
34+
} else {
35+
"pull_request"
36+
},
37+
);
38+
let tmp = TempDir::new().expect("Failed to create a temp dir for test");
39+
let mut event_payload = NamedTempFile::new_in(tmp.path())
40+
.expect("Failed to spawn a tmp file for test event payload");
41+
env::set_var("GITHUB_EVENT_PATH", event_payload.path());
42+
if let EventType::PullRequest(pr_number) = event_type {
43+
event_payload
44+
.write_all(
45+
serde_json::json!({"number": pr_number})
46+
.to_string()
47+
.as_bytes(),
48+
)
49+
.expect("Failed to write data to test event payload file")
50+
}
51+
52+
let reset_timestamp = (Utc::now().timestamp() + 60).to_string();
53+
let asset_path = format!("{}/tests/paginated_changes", lib_root.to_str().unwrap());
54+
55+
let mut server = mock_server().await;
56+
env::set_var("GITHUB_API_URL", server.url());
57+
env::set_current_dir(tmp.path()).unwrap();
58+
let gh_client = GithubApiClient::new();
59+
60+
let mut mocks = vec![];
61+
let diff_end_point = format!(
62+
"/repos/{REPO}/{}",
63+
if let EventType::PullRequest(pr) = event_type {
64+
format!("pulls/{pr}")
65+
} else {
66+
format!("commits/{SHA}")
67+
}
68+
);
69+
mocks.push(
70+
server
71+
.mock("GET", diff_end_point.as_str())
72+
.match_header("Accept", "application/vnd.github.diff")
73+
.match_header("Authorization", TOKEN)
74+
.with_header(REMAINING_RATE_LIMIT_HEADER, "50")
75+
.with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str())
76+
.with_status(403)
77+
.create(),
78+
);
79+
let pg_end_point = if event_type == EventType::Push {
80+
diff_end_point.clone()
81+
} else {
82+
format!("{diff_end_point}/files")
83+
};
84+
for pg in 1..=2 {
85+
let link = if pg == 1 {
86+
format!("<{}{pg_end_point}?page=2>; rel=\"next\"", server.url())
87+
} else {
88+
"".to_string()
89+
};
90+
mocks.push(
91+
server
92+
.mock("GET", pg_end_point.as_str())
93+
.match_header("Accept", "application/vnd.github.raw+json")
94+
.match_header("Authorization", TOKEN)
95+
.match_query(Matcher::UrlEncoded("page".to_string(), pg.to_string()))
96+
.with_header(REMAINING_RATE_LIMIT_HEADER, "50")
97+
.with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str())
98+
.with_body_from_file(format!(
99+
"{asset_path}/{}_files_pg{pg}.json",
100+
if event_type == EventType::Push {
101+
"push"
102+
} else {
103+
"pull_request"
104+
}
105+
))
106+
.with_header("link", link.as_str())
107+
.create(),
108+
);
109+
}
110+
111+
let file_filter = FileFilter::new(&[], vec!["cpp".to_string(), "hpp".to_string()]);
112+
let files = gh_client.get_list_of_changed_files(&file_filter).await;
113+
assert_eq!(files.len(), 2);
114+
for file in files {
115+
assert!(["src/demo.cpp", "src/demo.hpp"].contains(
116+
&file
117+
.name
118+
.as_path()
119+
.to_str()
120+
.expect("Failed to get file name from path")
121+
));
122+
}
123+
for mock in mocks {
124+
mock.assert();
125+
}
126+
}
127+
128+
async fn test_get_changes(event_type: EventType) {
129+
let tmp_dir = create_test_space(false);
130+
let lib_root = env::current_dir().unwrap();
131+
env::set_current_dir(tmp_dir.path()).unwrap();
132+
get_paginated_changes(&lib_root, event_type).await;
133+
env::set_current_dir(lib_root.as_path()).unwrap();
134+
drop(tmp_dir);
135+
}
136+
137+
#[tokio::test]
138+
async fn get_push_files_paginated() {
139+
test_get_changes(EventType::Push).await
140+
}
141+
142+
#[tokio::test]
143+
async fn get_pr_files_paginated() {
144+
test_get_changes(EventType::PullRequest(42)).await
145+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[
2+
{
3+
"sha": "52501fa1dc96d6bc6f8a155816df041b1de975d9",
4+
"filename": ".github/workflows/cpp-lint-package.yml",
5+
"status": "modified",
6+
"additions": 9,
7+
"deletions": 5,
8+
"changes": 14,
9+
"blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/635a9c57bdcca07b99ddef52c2640337c50280b1/.github%2Fworkflows%2Fcpp-lint-package.yml",
10+
"raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/635a9c57bdcca07b99ddef52c2640337c50280b1/.github%2Fworkflows%2Fcpp-lint-package.yml",
11+
"contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/.github%2Fworkflows%2Fcpp-lint-package.yml?ref=635a9c57bdcca07b99ddef52c2640337c50280b1",
12+
"patch": "@@ -7,16 +7,17 @@ on:\n description: 'which branch to test'\n default: 'main'\n required: true\n+ pull_request:\n \n jobs:\n cpp-linter:\n runs-on: windows-latest\n \n strategy:\n matrix:\n- clang-version: ['7', '8', '9','10', '11', '12', '13', '14', '15', '16', '17']\n+ clang-version: ['10', '11', '12', '13', '14', '15', '16', '17']\n repo: ['cpp-linter/cpp-linter']\n- branch: ['${{ inputs.branch }}']\n+ branch: ['pr-review-suggestions']\n fail-fast: false\n \n steps:\n@@ -62,10 +63,13 @@ jobs:\n -i=build \n -p=build \n -V=${{ runner.temp }}/llvm \n- -f=false \n --extra-arg=\"-std=c++14 -Wall\" \n- --thread-comments=${{ matrix.clang-version == '12' }} \n- -a=${{ matrix.clang-version == '12' }}\n+ --file-annotations=false\n+ --lines-changed-only=false\n+ --extension=h,c\n+ --thread-comments=${{ matrix.clang-version == '16' }} \n+ --tidy-review=${{ matrix.clang-version == '16' }}\n+ --format-review=${{ matrix.clang-version == '16' }}\n \n - name: Fail fast?!\n if: steps.linter.outputs.checks-failed > 0"
13+
},
14+
{
15+
"sha": "1bf553e06e4b7c6c9a9be5da4845acbdeb04f6a5",
16+
"filename": "src/demo.cpp",
17+
"previous_filename": "src/demo.c",
18+
"status": "modified",
19+
"additions": 11,
20+
"deletions": 10,
21+
"changes": 21,
22+
"blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/635a9c57bdcca07b99ddef52c2640337c50280b1/src%2Fdemo.cpp",
23+
"raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/635a9c57bdcca07b99ddef52c2640337c50280b1/src%2Fdemo.cpp",
24+
"contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/src%2Fdemo.cpp?ref=635a9c57bdcca07b99ddef52c2640337c50280b1",
25+
"patch": "@@ -1,17 +1,18 @@\n /** This is a very ugly test code (doomed to fail linting) */\n #include \"demo.hpp\"\n-#include <cstdio>\n-#include <cstddef>\n+#include <stdio.h>\n \n-// using size_t from cstddef\n-size_t dummyFunc(size_t i) { return i; }\n \n-int main()\n-{\n- for (;;)\n- break;\n+\n+\n+int main(){\n+\n+ for (;;) break;\n+\n \n printf(\"Hello world!\\n\");\n \n- return 0;\n-}\n+\n+\n+\n+ return 0;}"
26+
}
27+
]

0 commit comments

Comments
 (0)