diff --git a/crates/travelagent-forge-github/tests/fixture_tests.rs b/crates/travelagent-forge-github/tests/fixture_tests.rs new file mode 100644 index 0000000..fa70703 --- /dev/null +++ b/crates/travelagent-forge-github/tests/fixture_tests.rs @@ -0,0 +1,229 @@ +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +use travelagent_core::forge::*; +use travelagent_core::model::FileStatus; +use travelagent_forge_github::GitHubForge; + +fn load_fixture(name: &str) -> String { + std::fs::read_to_string(format!("tests/fixtures/{name}")).unwrap() +} + +fn ripgrep_pr_id() -> PrId { + PrId { + owner: "BurntSushi".into(), + repo: "ripgrep".into(), + number: 2900, + } +} + +fn ruff_pr_id() -> PrId { + PrId { + owner: "astral-sh".into(), + repo: "ruff".into(), + number: 16000, + } +} + +async fn setup() -> (MockServer, GitHubForge) { + let server = MockServer::start().await; + let forge = GitHubForge::with_token(&server.uri(), "test-token".into()).unwrap(); + (server, forge) +} + +#[tokio::test] +async fn ripgrep_pr_metadata_parsed_correctly() { + let (server, forge) = setup().await; + let body: serde_json::Value = + serde_json::from_str(&load_fixture("ripgrep_2900_pr.json")).unwrap(); + + Mock::given(method("GET")) + .and(path("/repos/BurntSushi/ripgrep/pulls/2900")) + .respond_with(ResponseTemplate::new(200).set_body_json(&body)) + .mount(&server) + .await; + + let pr = forge.get_pr(&ripgrep_pr_id()).await.unwrap(); + assert_eq!(pr.title, "globset: add matches_all method"); + assert_eq!(pr.author, "tmccombs"); + assert_eq!(pr.state, PrState::Closed); + assert_eq!(pr.base_branch, "master"); + assert_eq!(pr.head_branch, "matches-all"); + assert!(!pr.is_draft); +} + +#[tokio::test] +async fn ripgrep_commits_parsed_correctly() { + let (server, forge) = setup().await; + let body: serde_json::Value = + serde_json::from_str(&load_fixture("ripgrep_2900_commits.json")).unwrap(); + + Mock::given(method("GET")) + .and(path("/repos/BurntSushi/ripgrep/pulls/2900/commits")) + .respond_with(ResponseTemplate::new(200).set_body_json(&body)) + .mount(&server) + .await; + + let commits = forge.get_pr_commits(&ripgrep_pr_id()).await.unwrap(); + assert_eq!(commits.len(), 2); + assert_eq!(commits[0].id, "3c17c22ef64e78064d8c621b118d7cdb3652fa76"); + assert_eq!(commits[0].short_id, "3c17c22"); + assert_eq!(commits[0].summary, "globset: add matches_all method"); + assert_eq!(commits[0].author, "tmccombs"); + assert_eq!(commits[1].summary, "fixup! globset: add matches_all method"); +} + +#[tokio::test] +async fn ripgrep_files_with_patches_parsed_correctly() { + let (server, forge) = setup().await; + let body: serde_json::Value = + serde_json::from_str(&load_fixture("ripgrep_2900_files.json")).unwrap(); + + Mock::given(method("GET")) + .and(path("/repos/BurntSushi/ripgrep/pulls/2900/files")) + .respond_with(ResponseTemplate::new(200).set_body_json(&body)) + .mount(&server) + .await; + + let files = forge.get_pr_files(&ripgrep_pr_id()).await.unwrap(); + assert_eq!(files.len(), 1); + assert_eq!(files[0].status, FileStatus::Modified); + assert_eq!( + files[0].new_path, + Some(std::path::PathBuf::from("crates/globset/src/lib.rs")) + ); + // The patch has 4 hunks (4 @@ sections in the fixture) + assert!( + !files[0].hunks.is_empty(), + "Expected hunks to be parsed from the patch" + ); + // Verify line numbers from the first hunk (@@ -351,6 +351,43 @@) + let first_hunk = &files[0].hunks[0]; + assert_eq!(first_hunk.old_start, 351); + assert_eq!(first_hunk.new_start, 351); +} + +#[tokio::test] +async fn ripgrep_review_comments_parsed_correctly() { + let (server, forge) = setup().await; + let comments_body: serde_json::Value = + serde_json::from_str(&load_fixture("ripgrep_2900_comments.json")).unwrap(); + + Mock::given(method("GET")) + .and(path("/repos/BurntSushi/ripgrep/pulls/2900/comments")) + .respond_with(ResponseTemplate::new(200).set_body_json(&comments_body)) + .mount(&server) + .await; + + // Also mock empty issue comments + Mock::given(method("GET")) + .and(path("/repos/BurntSushi/ripgrep/issues/2900/comments")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([]))) + .mount(&server) + .await; + + let comments = forge.get_comments(&ripgrep_pr_id()).await.unwrap(); + assert_eq!(comments.len(), 3); + + // First comment by BurntSushi + assert_eq!(comments[0].author, "BurntSushi"); + assert_eq!(comments[0].path, Some("crates/globset/src/lib.rs".into())); + assert_eq!(comments[0].in_reply_to, None); + + // Second comment also by BurntSushi, different thread + assert_eq!(comments[1].author, "BurntSushi"); + assert_eq!(comments[1].in_reply_to, None); + + // Third comment is a reply to the second (in_reply_to_id = 1766859217) + assert_eq!(comments[2].author, "BurntSushi"); + assert_eq!(comments[2].in_reply_to, Some(1766859217)); +} + +#[tokio::test] +async fn ruff_medium_pr_13_files_parsed_correctly() { + let (server, forge) = setup().await; + let body: serde_json::Value = + serde_json::from_str(&load_fixture("ruff_16000_files.json")).unwrap(); + + Mock::given(method("GET")) + .and(path("/repos/astral-sh/ruff/pulls/16000/files")) + .respond_with(ResponseTemplate::new(200).set_body_json(&body)) + .mount(&server) + .await; + + let files = forge.get_pr_files(&ruff_pr_id()).await.unwrap(); + assert_eq!(files.len(), 13); + + // Check statuses: 12 modified + 1 added + let added_count = files + .iter() + .filter(|f| f.status == FileStatus::Added) + .count(); + let modified_count = files + .iter() + .filter(|f| f.status == FileStatus::Modified) + .count(); + assert_eq!(added_count, 1); + assert_eq!(modified_count, 12); + + // Verify the added file + let added = files + .iter() + .find(|f| f.status == FileStatus::Added) + .unwrap(); + assert_eq!( + added.new_path, + Some(std::path::PathBuf::from( + "crates/red_knot_project/src/metadata/settings.rs" + )) + ); + assert!(added.old_path.is_none()); + + // Verify patches are parsed into hunks + for file in &files { + assert!( + !file.hunks.is_empty(), + "File {:?} should have hunks parsed from patch", + file.new_path + ); + } +} + +#[tokio::test] +async fn ruff_3_review_comments_with_replies() { + let (server, forge) = setup().await; + let comments_body: serde_json::Value = + serde_json::from_str(&load_fixture("ruff_16000_comments.json")).unwrap(); + + Mock::given(method("GET")) + .and(path("/repos/astral-sh/ruff/pulls/16000/comments")) + .respond_with(ResponseTemplate::new(200).set_body_json(&comments_body)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path("/repos/astral-sh/ruff/issues/16000/comments")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([]))) + .mount(&server) + .await; + + let comments = forge.get_comments(&ruff_pr_id()).await.unwrap(); + assert_eq!(comments.len(), 3); + + // First comment starts a thread (no in_reply_to) + assert_eq!(comments[0].author, "dhruvmanila"); + assert_eq!(comments[0].id, 1946208119); + assert_eq!(comments[0].in_reply_to, None); + assert_eq!( + comments[0].path, + Some("crates/red_knot_project/src/metadata/settings.rs".into()) + ); + + // Second comment replies to the first + assert_eq!(comments[1].author, "MichaReiser"); + assert_eq!(comments[1].in_reply_to, Some(1946208119)); + + // Third comment also replies to the first + assert_eq!(comments[2].author, "dhruvmanila"); + assert_eq!(comments[2].in_reply_to, Some(1946208119)); +} diff --git a/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_comments.json b/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_comments.json new file mode 100644 index 0000000..e96177e --- /dev/null +++ b/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_comments.json @@ -0,0 +1 @@ +[{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766848243","pull_request_review_id":2315570568,"id":1766848243,"node_id":"PRRC_kwDOAzJbyc5pT_bz","diff_hunk":"@@ -351,6 +351,27 @@ impl GlobSet {\n false\n }\n \n+ /// Returns true if ALL globs in this set match the path given.","path":"crates/globset/src/lib.rs","commit_id":"c26831ce39d3cfa36db8f913998ab8b52be4d38c","original_commit_id":"3c17c22ef64e78064d8c621b118d7cdb3652fa76","user":{"login":"BurntSushi","id":456674,"node_id":"MDQ6VXNlcjQ1NjY3NA==","avatar_url":"https://avatars.githubusercontent.com/u/456674?v=4","gravatar_id":"","url":"https://api.github.com/users/BurntSushi","html_url":"https://github.com/BurntSushi","followers_url":"https://api.github.com/users/BurntSushi/followers","following_url":"https://api.github.com/users/BurntSushi/following{/other_user}","gists_url":"https://api.github.com/users/BurntSushi/gists{/gist_id}","starred_url":"https://api.github.com/users/BurntSushi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BurntSushi/subscriptions","organizations_url":"https://api.github.com/users/BurntSushi/orgs","repos_url":"https://api.github.com/users/BurntSushi/repos","events_url":"https://api.github.com/users/BurntSushi/events{/privacy}","received_events_url":"https://api.github.com/users/BurntSushi/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"I don't think we need to use caps for `ALL` here. Just \"all\" is fine.","created_at":"2024-09-19T13:34:43Z","updated_at":"2024-09-19T13:41:27Z","html_url":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766848243","pull_request_url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900","_links":{"self":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766848243"},"html":{"href":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766848243"},"pull_request":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900"}},"reactions":{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766848243/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":null,"original_line":354,"side":"RIGHT","author_association":"OWNER","original_position":4,"position":1,"subject_type":"line"},{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766859217","pull_request_review_id":2315570568,"id":1766859217,"node_id":"PRRC_kwDOAzJbyc5pUCHR","diff_hunk":"@@ -351,6 +351,27 @@ impl GlobSet {\n false\n }\n \n+ /// Returns true if ALL globs in this set match the path given.\n+ pub fn matches_all>(&self, path: P) -> bool {\n+ self.matches_all_candidate(&Candidate::new(path.as_ref()))\n+ }\n+\n+ /// Returns ture if all globs in this set match the path given.\n+ ///\n+ /// This takes a Candidate as input, which can be used to amortize the\n+ /// cost of peparing a path for matching.\n+ ///\n+ /// This will return true if the set of globs is empty, as in that case all `0` of\n+ /// the globs will match.","path":"crates/globset/src/lib.rs","commit_id":"c26831ce39d3cfa36db8f913998ab8b52be4d38c","original_commit_id":"3c17c22ef64e78064d8c621b118d7cdb3652fa76","user":{"login":"BurntSushi","id":456674,"node_id":"MDQ6VXNlcjQ1NjY3NA==","avatar_url":"https://avatars.githubusercontent.com/u/456674?v=4","gravatar_id":"","url":"https://api.github.com/users/BurntSushi","html_url":"https://github.com/BurntSushi","followers_url":"https://api.github.com/users/BurntSushi/followers","following_url":"https://api.github.com/users/BurntSushi/following{/other_user}","gists_url":"https://api.github.com/users/BurntSushi/gists{/gist_id}","starred_url":"https://api.github.com/users/BurntSushi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BurntSushi/subscriptions","organizations_url":"https://api.github.com/users/BurntSushi/orgs","repos_url":"https://api.github.com/users/BurntSushi/repos","events_url":"https://api.github.com/users/BurntSushi/events{/privacy}","received_events_url":"https://api.github.com/users/BurntSushi/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Please wrap lines to 79 columns (inclusive).","created_at":"2024-09-19T13:39:26Z","updated_at":"2024-09-19T13:41:27Z","html_url":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766859217","pull_request_url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900","_links":{"self":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766859217"},"html":{"href":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766859217"},"pull_request":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900"}},"reactions":{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766859217/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":null,"original_line":365,"side":"RIGHT","author_association":"OWNER","original_position":15,"position":1,"subject_type":"line"},{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766860858","pull_request_review_id":2315570568,"id":1766860858,"node_id":"PRRC_kwDOAzJbyc5pUCg6","diff_hunk":"@@ -351,6 +351,27 @@ impl GlobSet {\n false\n }\n \n+ /// Returns true if ALL globs in this set match the path given.\n+ pub fn matches_all>(&self, path: P) -> bool {\n+ self.matches_all_candidate(&Candidate::new(path.as_ref()))\n+ }\n+\n+ /// Returns ture if all globs in this set match the path given.\n+ ///\n+ /// This takes a Candidate as input, which can be used to amortize the\n+ /// cost of peparing a path for matching.\n+ ///\n+ /// This will return true if the set of globs is empty, as in that case all `0` of\n+ /// the globs will match.","path":"crates/globset/src/lib.rs","commit_id":"c26831ce39d3cfa36db8f913998ab8b52be4d38c","original_commit_id":"3c17c22ef64e78064d8c621b118d7cdb3652fa76","user":{"login":"BurntSushi","id":456674,"node_id":"MDQ6VXNlcjQ1NjY3NA==","avatar_url":"https://avatars.githubusercontent.com/u/456674?v=4","gravatar_id":"","url":"https://api.github.com/users/BurntSushi","html_url":"https://github.com/BurntSushi","followers_url":"https://api.github.com/users/BurntSushi/followers","following_url":"https://api.github.com/users/BurntSushi/following{/other_user}","gists_url":"https://api.github.com/users/BurntSushi/gists{/gist_id}","starred_url":"https://api.github.com/users/BurntSushi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BurntSushi/subscriptions","organizations_url":"https://api.github.com/users/BurntSushi/orgs","repos_url":"https://api.github.com/users/BurntSushi/repos","events_url":"https://api.github.com/users/BurntSushi/events{/privacy}","received_events_url":"https://api.github.com/users/BurntSushi/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Also, this note should be added to `matches_all` too.","created_at":"2024-09-19T13:39:52Z","updated_at":"2024-09-19T13:41:27Z","html_url":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766860858","pull_request_url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900","_links":{"self":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766860858"},"html":{"href":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766860858"},"pull_request":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900"}},"reactions":{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766860858/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":null,"original_line":365,"side":"RIGHT","in_reply_to_id":1766859217,"author_association":"OWNER","original_position":15,"position":1,"subject_type":"line"}] \ No newline at end of file diff --git a/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_commits.json b/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_commits.json new file mode 100644 index 0000000..41c6dd6 --- /dev/null +++ b/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_commits.json @@ -0,0 +1 @@ +[{"sha":"3c17c22ef64e78064d8c621b118d7cdb3652fa76","node_id":"C_kwDOCCrTRdoAKDNjMTdjMjJlZjY0ZTc4MDY0ZDhjNjIxYjExOGQ3Y2RiMzY1MmZhNzY","commit":{"author":{"name":"Thayne McCombs","email":"astrothayne@gmail.com","date":"2024-09-19T07:09:57Z"},"committer":{"name":"Thayne McCombs","email":"astrothayne@gmail.com","date":"2024-09-19T07:09:57Z"},"message":"globset: add matches_all method\n\nThis returns true if all globs in the set match the supplied file.\n\nFixes: #2869","tree":{"sha":"8d97feb8032afc99e07d89c249469ca2bea058b2","url":"https://api.github.com/repos/BurntSushi/ripgrep/git/trees/8d97feb8032afc99e07d89c249469ca2bea058b2"},"url":"https://api.github.com/repos/BurntSushi/ripgrep/git/commits/3c17c22ef64e78064d8c621b118d7cdb3652fa76","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/BurntSushi/ripgrep/commits/3c17c22ef64e78064d8c621b118d7cdb3652fa76","html_url":"https://github.com/BurntSushi/ripgrep/commit/3c17c22ef64e78064d8c621b118d7cdb3652fa76","comments_url":"https://api.github.com/repos/BurntSushi/ripgrep/commits/3c17c22ef64e78064d8c621b118d7cdb3652fa76/comments","author":{"login":"tmccombs","id":2541726,"node_id":"MDQ6VXNlcjI1NDE3MjY=","avatar_url":"https://avatars.githubusercontent.com/u/2541726?v=4","gravatar_id":"","url":"https://api.github.com/users/tmccombs","html_url":"https://github.com/tmccombs","followers_url":"https://api.github.com/users/tmccombs/followers","following_url":"https://api.github.com/users/tmccombs/following{/other_user}","gists_url":"https://api.github.com/users/tmccombs/gists{/gist_id}","starred_url":"https://api.github.com/users/tmccombs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tmccombs/subscriptions","organizations_url":"https://api.github.com/users/tmccombs/orgs","repos_url":"https://api.github.com/users/tmccombs/repos","events_url":"https://api.github.com/users/tmccombs/events{/privacy}","received_events_url":"https://api.github.com/users/tmccombs/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"tmccombs","id":2541726,"node_id":"MDQ6VXNlcjI1NDE3MjY=","avatar_url":"https://avatars.githubusercontent.com/u/2541726?v=4","gravatar_id":"","url":"https://api.github.com/users/tmccombs","html_url":"https://github.com/tmccombs","followers_url":"https://api.github.com/users/tmccombs/followers","following_url":"https://api.github.com/users/tmccombs/following{/other_user}","gists_url":"https://api.github.com/users/tmccombs/gists{/gist_id}","starred_url":"https://api.github.com/users/tmccombs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tmccombs/subscriptions","organizations_url":"https://api.github.com/users/tmccombs/orgs","repos_url":"https://api.github.com/users/tmccombs/repos","events_url":"https://api.github.com/users/tmccombs/events{/privacy}","received_events_url":"https://api.github.com/users/tmccombs/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"8bd595029656370927f846406ca4ce4ffe0b3e91","url":"https://api.github.com/repos/BurntSushi/ripgrep/commits/8bd595029656370927f846406ca4ce4ffe0b3e91","html_url":"https://github.com/BurntSushi/ripgrep/commit/8bd595029656370927f846406ca4ce4ffe0b3e91"}]},{"sha":"c26831ce39d3cfa36db8f913998ab8b52be4d38c","node_id":"C_kwDOCCrTRdoAKGMyNjgzMWNlMzlkM2NmYTM2ZGI4ZjkxMzk5OGFiOGI1MmJlNGQzOGM","commit":{"author":{"name":"Thayne McCombs","email":"astrothayne@gmail.com","date":"2024-09-20T06:49:04Z"},"committer":{"name":"Thayne McCombs","email":"astrothayne@gmail.com","date":"2024-09-20T06:49:04Z"},"message":"fixup! globset: add matches_all method","tree":{"sha":"1988491f1e8c0eaae223b712f2f1b1ab55e2c946","url":"https://api.github.com/repos/BurntSushi/ripgrep/git/trees/1988491f1e8c0eaae223b712f2f1b1ab55e2c946"},"url":"https://api.github.com/repos/BurntSushi/ripgrep/git/commits/c26831ce39d3cfa36db8f913998ab8b52be4d38c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/BurntSushi/ripgrep/commits/c26831ce39d3cfa36db8f913998ab8b52be4d38c","html_url":"https://github.com/BurntSushi/ripgrep/commit/c26831ce39d3cfa36db8f913998ab8b52be4d38c","comments_url":"https://api.github.com/repos/BurntSushi/ripgrep/commits/c26831ce39d3cfa36db8f913998ab8b52be4d38c/comments","author":{"login":"tmccombs","id":2541726,"node_id":"MDQ6VXNlcjI1NDE3MjY=","avatar_url":"https://avatars.githubusercontent.com/u/2541726?v=4","gravatar_id":"","url":"https://api.github.com/users/tmccombs","html_url":"https://github.com/tmccombs","followers_url":"https://api.github.com/users/tmccombs/followers","following_url":"https://api.github.com/users/tmccombs/following{/other_user}","gists_url":"https://api.github.com/users/tmccombs/gists{/gist_id}","starred_url":"https://api.github.com/users/tmccombs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tmccombs/subscriptions","organizations_url":"https://api.github.com/users/tmccombs/orgs","repos_url":"https://api.github.com/users/tmccombs/repos","events_url":"https://api.github.com/users/tmccombs/events{/privacy}","received_events_url":"https://api.github.com/users/tmccombs/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"tmccombs","id":2541726,"node_id":"MDQ6VXNlcjI1NDE3MjY=","avatar_url":"https://avatars.githubusercontent.com/u/2541726?v=4","gravatar_id":"","url":"https://api.github.com/users/tmccombs","html_url":"https://github.com/tmccombs","followers_url":"https://api.github.com/users/tmccombs/followers","following_url":"https://api.github.com/users/tmccombs/following{/other_user}","gists_url":"https://api.github.com/users/tmccombs/gists{/gist_id}","starred_url":"https://api.github.com/users/tmccombs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tmccombs/subscriptions","organizations_url":"https://api.github.com/users/tmccombs/orgs","repos_url":"https://api.github.com/users/tmccombs/repos","events_url":"https://api.github.com/users/tmccombs/events{/privacy}","received_events_url":"https://api.github.com/users/tmccombs/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"3c17c22ef64e78064d8c621b118d7cdb3652fa76","url":"https://api.github.com/repos/BurntSushi/ripgrep/commits/3c17c22ef64e78064d8c621b118d7cdb3652fa76","html_url":"https://github.com/BurntSushi/ripgrep/commit/3c17c22ef64e78064d8c621b118d7cdb3652fa76"}]}] \ No newline at end of file diff --git a/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_files.json b/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_files.json new file mode 100644 index 0000000..5962e01 --- /dev/null +++ b/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_files.json @@ -0,0 +1 @@ +[{"sha":"a2779df9e7c777f4cf09b6fda4bfbb8e26b9ca41","filename":"crates/globset/src/lib.rs","status":"modified","additions":69,"deletions":14,"changes":83,"blob_url":"https://github.com/BurntSushi/ripgrep/blob/c26831ce39d3cfa36db8f913998ab8b52be4d38c/crates%2Fglobset%2Fsrc%2Flib.rs","raw_url":"https://github.com/BurntSushi/ripgrep/raw/c26831ce39d3cfa36db8f913998ab8b52be4d38c/crates%2Fglobset%2Fsrc%2Flib.rs","contents_url":"https://api.github.com/repos/BurntSushi/ripgrep/contents/crates%2Fglobset%2Fsrc%2Flib.rs?ref=c26831ce39d3cfa36db8f913998ab8b52be4d38c","patch":"@@ -351,6 +351,43 @@ impl GlobSet {\n false\n }\n \n+ /// Returns true if all globs in this set match the path given.\n+ ///\n+ /// This will return true if the set of globs is empty, as in that case all\n+ /// `0` of the globs will match.\n+ ///\n+ /// ```\n+ /// use globset::{Glob, GlobSetBuilder};\n+ ///\n+ /// let mut builder = GlobSetBuilder::new();\n+ /// builder.add(Glob::new(\"src/*\").unwrap());\n+ /// builder.add(Glob::new(\"**/*.rs\").unwrap());\n+ /// let set = builder.build().unwrap();\n+ ///\n+ /// assert!(set.matches_all(\"src/foo.rs\"));\n+ /// assert!(!set.matches_all(\"src/bar.c\"));\n+ /// assert!(!set.matches_all(\"test.rs\"));\n+ /// ```\n+ pub fn matches_all>(&self, path: P) -> bool {\n+ self.matches_all_candidate(&Candidate::new(path.as_ref()))\n+ }\n+\n+ /// Returns ture if all globs in this set match the path given.\n+ ///\n+ /// This takes a Candidate as input, which can be used to amortize the cost\n+ /// of peparing a path for matching.\n+ ///\n+ /// This will return true if the set of globs is empty, as in that case all\n+ /// `0` of the globs will match.\n+ pub fn matches_all_candidate(&self, path: &Candidate<'_>) -> bool {\n+ for strat in &self.strats {\n+ if !strat.is_match(path) {\n+ return false;\n+ }\n+ }\n+ true\n+ }\n+\n /// Returns the sequence number of every glob pattern that matches the\n /// given path.\n pub fn matches>(&self, path: P) -> Vec {\n@@ -461,20 +498,33 @@ impl GlobSet {\n required_exts.0.len(),\n regexes.literals.len()\n );\n- Ok(GlobSet {\n- len: pats.len(),\n- strats: vec![\n- GlobSetMatchStrategy::Extension(exts),\n- GlobSetMatchStrategy::BasenameLiteral(base_lits),\n- GlobSetMatchStrategy::Literal(lits),\n- GlobSetMatchStrategy::Suffix(suffixes.suffix()),\n- GlobSetMatchStrategy::Prefix(prefixes.prefix()),\n- GlobSetMatchStrategy::RequiredExtension(\n- required_exts.build()?,\n- ),\n- GlobSetMatchStrategy::Regex(regexes.regex_set()?),\n- ],\n- })\n+ let mut strats = Vec::with_capacity(7);\n+ // Only add strategies that are populated\n+ if !exts.0.is_empty() {\n+ strats.push(GlobSetMatchStrategy::Extension(exts));\n+ }\n+ if !base_lits.0.is_empty() {\n+ strats.push(GlobSetMatchStrategy::BasenameLiteral(base_lits));\n+ }\n+ if !lits.0.is_empty() {\n+ strats.push(GlobSetMatchStrategy::Literal(lits));\n+ }\n+ if !suffixes.is_empty() {\n+ strats.push(GlobSetMatchStrategy::Suffix(suffixes.suffix()));\n+ }\n+ if !prefixes.is_empty() {\n+ strats.push(GlobSetMatchStrategy::Prefix(prefixes.prefix()));\n+ }\n+ if !required_exts.0.is_empty() {\n+ strats.push(GlobSetMatchStrategy::RequiredExtension(\n+ required_exts.build()?,\n+ ));\n+ }\n+ if !regexes.is_empty() {\n+ strats.push(GlobSetMatchStrategy::Regex(regexes.regex_set()?));\n+ }\n+\n+ Ok(GlobSet { len: pats.len(), strats })\n }\n }\n \n@@ -892,6 +942,10 @@ impl MultiStrategyBuilder {\n patset: Arc::new(Pool::new(create)),\n })\n }\n+\n+ fn is_empty(&self) -> bool {\n+ self.literals.is_empty()\n+ }\n }\n \n #[derive(Clone, Debug)]\n@@ -979,6 +1033,7 @@ mod tests {\n let set = GlobSetBuilder::new().build().unwrap();\n assert!(!set.is_match(\"\"));\n assert!(!set.is_match(\"a\"));\n+ assert!(set.matches_all(\"a\"));\n }\n \n #[test]"}] \ No newline at end of file diff --git a/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_pr.json b/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_pr.json new file mode 100644 index 0000000..0c5b759 --- /dev/null +++ b/crates/travelagent-forge-github/tests/fixtures/ripgrep_2900_pr.json @@ -0,0 +1 @@ +{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900","id":2080266118,"node_id":"PR_kwDOAzJbyc57_leG","html_url":"https://github.com/BurntSushi/ripgrep/pull/2900","diff_url":"https://github.com/BurntSushi/ripgrep/pull/2900.diff","patch_url":"https://github.com/BurntSushi/ripgrep/pull/2900.patch","issue_url":"https://api.github.com/repos/BurntSushi/ripgrep/issues/2900","number":2900,"state":"closed","locked":false,"title":"globset: add matches_all method","user":{"login":"tmccombs","id":2541726,"node_id":"MDQ6VXNlcjI1NDE3MjY=","avatar_url":"https://avatars.githubusercontent.com/u/2541726?v=4","gravatar_id":"","url":"https://api.github.com/users/tmccombs","html_url":"https://github.com/tmccombs","followers_url":"https://api.github.com/users/tmccombs/followers","following_url":"https://api.github.com/users/tmccombs/following{/other_user}","gists_url":"https://api.github.com/users/tmccombs/gists{/gist_id}","starred_url":"https://api.github.com/users/tmccombs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tmccombs/subscriptions","organizations_url":"https://api.github.com/users/tmccombs/orgs","repos_url":"https://api.github.com/users/tmccombs/repos","events_url":"https://api.github.com/users/tmccombs/events{/privacy}","received_events_url":"https://api.github.com/users/tmccombs/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"This returns true if all globs in the set match the supplied file.\r\n\r\nFixes: #2869","created_at":"2024-09-19T07:35:22Z","updated_at":"2025-09-20T01:46:07Z","closed_at":"2025-09-20T01:08:32Z","merged_at":null,"merge_commit_sha":"d16a4a060d2dcf4641811cfbd0efc59ed89d962c","assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":1849506066,"node_id":"MDU6TGFiZWwxODQ5NTA2MDY2","url":"https://api.github.com/repos/BurntSushi/ripgrep/labels/rollup","name":"rollup","color":"ffccee","default":false,"description":"A PR that has been merged with many others in a rollup."}],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900/commits","review_comments_url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900/comments","review_comment_url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments{/number}","comments_url":"https://api.github.com/repos/BurntSushi/ripgrep/issues/2900/comments","statuses_url":"https://api.github.com/repos/BurntSushi/ripgrep/statuses/c26831ce39d3cfa36db8f913998ab8b52be4d38c","head":{"label":"tmccombs:matches-all","ref":"matches-all","sha":"c26831ce39d3cfa36db8f913998ab8b52be4d38c","user":{"login":"tmccombs","id":2541726,"node_id":"MDQ6VXNlcjI1NDE3MjY=","avatar_url":"https://avatars.githubusercontent.com/u/2541726?v=4","gravatar_id":"","url":"https://api.github.com/users/tmccombs","html_url":"https://github.com/tmccombs","followers_url":"https://api.github.com/users/tmccombs/followers","following_url":"https://api.github.com/users/tmccombs/following{/other_user}","gists_url":"https://api.github.com/users/tmccombs/gists{/gist_id}","starred_url":"https://api.github.com/users/tmccombs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tmccombs/subscriptions","organizations_url":"https://api.github.com/users/tmccombs/orgs","repos_url":"https://api.github.com/users/tmccombs/repos","events_url":"https://api.github.com/users/tmccombs/events{/privacy}","received_events_url":"https://api.github.com/users/tmccombs/received_events","type":"User","user_view_type":"public","site_admin":false},"repo":{"id":137024325,"node_id":"MDEwOlJlcG9zaXRvcnkxMzcwMjQzMjU=","name":"ripgrep","full_name":"tmccombs/ripgrep","private":false,"owner":{"login":"tmccombs","id":2541726,"node_id":"MDQ6VXNlcjI1NDE3MjY=","avatar_url":"https://avatars.githubusercontent.com/u/2541726?v=4","gravatar_id":"","url":"https://api.github.com/users/tmccombs","html_url":"https://github.com/tmccombs","followers_url":"https://api.github.com/users/tmccombs/followers","following_url":"https://api.github.com/users/tmccombs/following{/other_user}","gists_url":"https://api.github.com/users/tmccombs/gists{/gist_id}","starred_url":"https://api.github.com/users/tmccombs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tmccombs/subscriptions","organizations_url":"https://api.github.com/users/tmccombs/orgs","repos_url":"https://api.github.com/users/tmccombs/repos","events_url":"https://api.github.com/users/tmccombs/events{/privacy}","received_events_url":"https://api.github.com/users/tmccombs/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/tmccombs/ripgrep","description":"ripgrep recursively searches directories for a regex pattern","fork":true,"url":"https://api.github.com/repos/tmccombs/ripgrep","forks_url":"https://api.github.com/repos/tmccombs/ripgrep/forks","keys_url":"https://api.github.com/repos/tmccombs/ripgrep/keys{/key_id}","collaborators_url":"https://api.github.com/repos/tmccombs/ripgrep/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/tmccombs/ripgrep/teams","hooks_url":"https://api.github.com/repos/tmccombs/ripgrep/hooks","issue_events_url":"https://api.github.com/repos/tmccombs/ripgrep/issues/events{/number}","events_url":"https://api.github.com/repos/tmccombs/ripgrep/events","assignees_url":"https://api.github.com/repos/tmccombs/ripgrep/assignees{/user}","branches_url":"https://api.github.com/repos/tmccombs/ripgrep/branches{/branch}","tags_url":"https://api.github.com/repos/tmccombs/ripgrep/tags","blobs_url":"https://api.github.com/repos/tmccombs/ripgrep/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/tmccombs/ripgrep/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/tmccombs/ripgrep/git/refs{/sha}","trees_url":"https://api.github.com/repos/tmccombs/ripgrep/git/trees{/sha}","statuses_url":"https://api.github.com/repos/tmccombs/ripgrep/statuses/{sha}","languages_url":"https://api.github.com/repos/tmccombs/ripgrep/languages","stargazers_url":"https://api.github.com/repos/tmccombs/ripgrep/stargazers","contributors_url":"https://api.github.com/repos/tmccombs/ripgrep/contributors","subscribers_url":"https://api.github.com/repos/tmccombs/ripgrep/subscribers","subscription_url":"https://api.github.com/repos/tmccombs/ripgrep/subscription","commits_url":"https://api.github.com/repos/tmccombs/ripgrep/commits{/sha}","git_commits_url":"https://api.github.com/repos/tmccombs/ripgrep/git/commits{/sha}","comments_url":"https://api.github.com/repos/tmccombs/ripgrep/comments{/number}","issue_comment_url":"https://api.github.com/repos/tmccombs/ripgrep/issues/comments{/number}","contents_url":"https://api.github.com/repos/tmccombs/ripgrep/contents/{+path}","compare_url":"https://api.github.com/repos/tmccombs/ripgrep/compare/{base}...{head}","merges_url":"https://api.github.com/repos/tmccombs/ripgrep/merges","archive_url":"https://api.github.com/repos/tmccombs/ripgrep/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/tmccombs/ripgrep/downloads","issues_url":"https://api.github.com/repos/tmccombs/ripgrep/issues{/number}","pulls_url":"https://api.github.com/repos/tmccombs/ripgrep/pulls{/number}","milestones_url":"https://api.github.com/repos/tmccombs/ripgrep/milestones{/number}","notifications_url":"https://api.github.com/repos/tmccombs/ripgrep/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/tmccombs/ripgrep/labels{/name}","releases_url":"https://api.github.com/repos/tmccombs/ripgrep/releases{/id}","deployments_url":"https://api.github.com/repos/tmccombs/ripgrep/deployments","created_at":"2018-06-12T06:02:18Z","updated_at":"2025-11-04T07:45:45Z","pushed_at":"2025-11-04T07:45:42Z","git_url":"git://github.com/tmccombs/ripgrep.git","ssh_url":"git@github.com:tmccombs/ripgrep.git","clone_url":"https://github.com/tmccombs/ripgrep.git","svn_url":"https://github.com/tmccombs/ripgrep","homepage":"","size":5295,"stargazers_count":0,"watchers_count":0,"language":"Rust","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"unlicense","name":"The Unlicense","spdx_id":"Unlicense","url":"https://api.github.com/licenses/unlicense","node_id":"MDc6TGljZW5zZTE1"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"BurntSushi:master","ref":"master","sha":"8bd595029656370927f846406ca4ce4ffe0b3e91","user":{"login":"BurntSushi","id":456674,"node_id":"MDQ6VXNlcjQ1NjY3NA==","avatar_url":"https://avatars.githubusercontent.com/u/456674?v=4","gravatar_id":"","url":"https://api.github.com/users/BurntSushi","html_url":"https://github.com/BurntSushi","followers_url":"https://api.github.com/users/BurntSushi/followers","following_url":"https://api.github.com/users/BurntSushi/following{/other_user}","gists_url":"https://api.github.com/users/BurntSushi/gists{/gist_id}","starred_url":"https://api.github.com/users/BurntSushi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BurntSushi/subscriptions","organizations_url":"https://api.github.com/users/BurntSushi/orgs","repos_url":"https://api.github.com/users/BurntSushi/repos","events_url":"https://api.github.com/users/BurntSushi/events{/privacy}","received_events_url":"https://api.github.com/users/BurntSushi/received_events","type":"User","user_view_type":"public","site_admin":false},"repo":{"id":53631945,"node_id":"MDEwOlJlcG9zaXRvcnk1MzYzMTk0NQ==","name":"ripgrep","full_name":"BurntSushi/ripgrep","private":false,"owner":{"login":"BurntSushi","id":456674,"node_id":"MDQ6VXNlcjQ1NjY3NA==","avatar_url":"https://avatars.githubusercontent.com/u/456674?v=4","gravatar_id":"","url":"https://api.github.com/users/BurntSushi","html_url":"https://github.com/BurntSushi","followers_url":"https://api.github.com/users/BurntSushi/followers","following_url":"https://api.github.com/users/BurntSushi/following{/other_user}","gists_url":"https://api.github.com/users/BurntSushi/gists{/gist_id}","starred_url":"https://api.github.com/users/BurntSushi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BurntSushi/subscriptions","organizations_url":"https://api.github.com/users/BurntSushi/orgs","repos_url":"https://api.github.com/users/BurntSushi/repos","events_url":"https://api.github.com/users/BurntSushi/events{/privacy}","received_events_url":"https://api.github.com/users/BurntSushi/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/BurntSushi/ripgrep","description":"ripgrep recursively searches directories for a regex pattern while respecting your gitignore","fork":false,"url":"https://api.github.com/repos/BurntSushi/ripgrep","forks_url":"https://api.github.com/repos/BurntSushi/ripgrep/forks","keys_url":"https://api.github.com/repos/BurntSushi/ripgrep/keys{/key_id}","collaborators_url":"https://api.github.com/repos/BurntSushi/ripgrep/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/BurntSushi/ripgrep/teams","hooks_url":"https://api.github.com/repos/BurntSushi/ripgrep/hooks","issue_events_url":"https://api.github.com/repos/BurntSushi/ripgrep/issues/events{/number}","events_url":"https://api.github.com/repos/BurntSushi/ripgrep/events","assignees_url":"https://api.github.com/repos/BurntSushi/ripgrep/assignees{/user}","branches_url":"https://api.github.com/repos/BurntSushi/ripgrep/branches{/branch}","tags_url":"https://api.github.com/repos/BurntSushi/ripgrep/tags","blobs_url":"https://api.github.com/repos/BurntSushi/ripgrep/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/BurntSushi/ripgrep/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/BurntSushi/ripgrep/git/refs{/sha}","trees_url":"https://api.github.com/repos/BurntSushi/ripgrep/git/trees{/sha}","statuses_url":"https://api.github.com/repos/BurntSushi/ripgrep/statuses/{sha}","languages_url":"https://api.github.com/repos/BurntSushi/ripgrep/languages","stargazers_url":"https://api.github.com/repos/BurntSushi/ripgrep/stargazers","contributors_url":"https://api.github.com/repos/BurntSushi/ripgrep/contributors","subscribers_url":"https://api.github.com/repos/BurntSushi/ripgrep/subscribers","subscription_url":"https://api.github.com/repos/BurntSushi/ripgrep/subscription","commits_url":"https://api.github.com/repos/BurntSushi/ripgrep/commits{/sha}","git_commits_url":"https://api.github.com/repos/BurntSushi/ripgrep/git/commits{/sha}","comments_url":"https://api.github.com/repos/BurntSushi/ripgrep/comments{/number}","issue_comment_url":"https://api.github.com/repos/BurntSushi/ripgrep/issues/comments{/number}","contents_url":"https://api.github.com/repos/BurntSushi/ripgrep/contents/{+path}","compare_url":"https://api.github.com/repos/BurntSushi/ripgrep/compare/{base}...{head}","merges_url":"https://api.github.com/repos/BurntSushi/ripgrep/merges","archive_url":"https://api.github.com/repos/BurntSushi/ripgrep/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/BurntSushi/ripgrep/downloads","issues_url":"https://api.github.com/repos/BurntSushi/ripgrep/issues{/number}","pulls_url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls{/number}","milestones_url":"https://api.github.com/repos/BurntSushi/ripgrep/milestones{/number}","notifications_url":"https://api.github.com/repos/BurntSushi/ripgrep/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/BurntSushi/ripgrep/labels{/name}","releases_url":"https://api.github.com/repos/BurntSushi/ripgrep/releases{/id}","deployments_url":"https://api.github.com/repos/BurntSushi/ripgrep/deployments","created_at":"2016-03-11T02:02:33Z","updated_at":"2026-04-17T03:37:26Z","pushed_at":"2026-02-27T16:25:19Z","git_url":"git://github.com/BurntSushi/ripgrep.git","ssh_url":"git@github.com:BurntSushi/ripgrep.git","clone_url":"https://github.com/BurntSushi/ripgrep.git","svn_url":"https://github.com/BurntSushi/ripgrep","homepage":"","size":5472,"stargazers_count":62511,"watchers_count":62511,"language":"Rust","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":true,"forks_count":2493,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":170,"license":{"key":"unlicense","name":"The Unlicense","spdx_id":"Unlicense","url":"https://api.github.com/licenses/unlicense","node_id":"MDc6TGljZW5zZTE1"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":["cli","command-line","command-line-tool","gitignore","grep","recursively-search","regex","ripgrep","rust","search"],"visibility":"public","forks":2493,"open_issues":170,"watchers":62511,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900"},"html":{"href":"https://github.com/BurntSushi/ripgrep/pull/2900"},"issue":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/issues/2900"},"comments":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/issues/2900/comments"},"review_comments":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900/comments"},"review_comment":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900/commits"},"statuses":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/statuses/c26831ce39d3cfa36db8f913998ab8b52be4d38c"}},"author_association":"CONTRIBUTOR","auto_merge":null,"assignee":null,"active_lock_reason":null,"merged":false,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":null,"comments":1,"review_comments":3,"maintainer_can_modify":false,"commits":2,"additions":69,"deletions":14,"changed_files":1} \ No newline at end of file diff --git a/crates/travelagent-forge-github/tests/fixtures/ruff_16000_comments.json b/crates/travelagent-forge-github/tests/fixtures/ruff_16000_comments.json new file mode 100644 index 0000000..332cc3a --- /dev/null +++ b/crates/travelagent-forge-github/tests/fixtures/ruff_16000_comments.json @@ -0,0 +1 @@ +[{"url":"https://api.github.com/repos/astral-sh/ruff/pulls/comments/1946208119","pull_request_review_id":2601201016,"id":1946208119,"node_id":"PRRC_kwDOHy0Bzc50AMd3","diff_hunk":"@@ -0,0 +1,53 @@\n+use std::sync::Arc;\n+\n+use red_knot_python_semantic::lint::RuleSelection;\n+\n+/// The resolved [`super::Options`] for the project.\n+///\n+/// Unlike [`super::Options`], the struct has default values filled in and\n+/// uses representations that are optimized for reads (instead of preserving the source representation).\n+/// It's also not required that this structure precisely resembles the TOML schema, although\n+/// it's encouraged to use a similar structure.\n+///\n+/// It's worth considering to adding a salsa query for specific settings to\n+/// limite the blast radius when only some settings change. For example,\n+/// changing the terminal settings shouldn't invalidate any core type-checking queries.\n+/// This can be achieved by adding a salsa query for the type checking specific settings.\n+///\n+/// Settings that are part of [`ProgramSettings`] are not included here.\n+#[derive(Clone, Debug, Eq, PartialEq)]\n+pub struct Settings {","path":"crates/red_knot_project/src/metadata/settings.rs","commit_id":"71e4b8938044fb18f88b6d07378d1d2069ed7792","original_commit_id":"9020e417b82e363c8d203d5362a7ab8969b7e187","user":{"login":"dhruvmanila","id":67177269,"node_id":"MDQ6VXNlcjY3MTc3MjY5","avatar_url":"https://avatars.githubusercontent.com/u/67177269?v=4","gravatar_id":"","url":"https://api.github.com/users/dhruvmanila","html_url":"https://github.com/dhruvmanila","followers_url":"https://api.github.com/users/dhruvmanila/followers","following_url":"https://api.github.com/users/dhruvmanila/following{/other_user}","gists_url":"https://api.github.com/users/dhruvmanila/gists{/gist_id}","starred_url":"https://api.github.com/users/dhruvmanila/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dhruvmanila/subscriptions","organizations_url":"https://api.github.com/users/dhruvmanila/orgs","repos_url":"https://api.github.com/users/dhruvmanila/repos","events_url":"https://api.github.com/users/dhruvmanila/events{/privacy}","received_events_url":"https://api.github.com/users/dhruvmanila/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"I believe this will help in converting the mdtest config options into the final resolved settings for the project?","created_at":"2025-02-07T09:12:23Z","updated_at":"2025-02-07T09:12:23Z","html_url":"https://github.com/astral-sh/ruff/pull/16000#discussion_r1946208119","pull_request_url":"https://api.github.com/repos/astral-sh/ruff/pulls/16000","_links":{"self":{"href":"https://api.github.com/repos/astral-sh/ruff/pulls/comments/1946208119"},"html":{"href":"https://github.com/astral-sh/ruff/pull/16000#discussion_r1946208119"},"pull_request":{"href":"https://api.github.com/repos/astral-sh/ruff/pulls/16000"}},"reactions":{"url":"https://api.github.com/repos/astral-sh/ruff/pulls/comments/1946208119/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":19,"original_line":19,"side":"RIGHT","author_association":"MEMBER","original_position":19,"position":19,"subject_type":"line"},{"url":"https://api.github.com/repos/astral-sh/ruff/pulls/comments/1946274087","pull_request_review_id":2601308845,"id":1946274087,"node_id":"PRRC_kwDOHy0Bzc50Ackn","diff_hunk":"@@ -0,0 +1,53 @@\n+use std::sync::Arc;\n+\n+use red_knot_python_semantic::lint::RuleSelection;\n+\n+/// The resolved [`super::Options`] for the project.\n+///\n+/// Unlike [`super::Options`], the struct has default values filled in and\n+/// uses representations that are optimized for reads (instead of preserving the source representation).\n+/// It's also not required that this structure precisely resembles the TOML schema, although\n+/// it's encouraged to use a similar structure.\n+///\n+/// It's worth considering to adding a salsa query for specific settings to\n+/// limite the blast radius when only some settings change. For example,\n+/// changing the terminal settings shouldn't invalidate any core type-checking queries.\n+/// This can be achieved by adding a salsa query for the type checking specific settings.\n+///\n+/// Settings that are part of [`ProgramSettings`] are not included here.\n+#[derive(Clone, Debug, Eq, PartialEq)]\n+pub struct Settings {","path":"crates/red_knot_project/src/metadata/settings.rs","commit_id":"71e4b8938044fb18f88b6d07378d1d2069ed7792","original_commit_id":"9020e417b82e363c8d203d5362a7ab8969b7e187","user":{"login":"MichaReiser","id":1203881,"node_id":"MDQ6VXNlcjEyMDM4ODE=","avatar_url":"https://avatars.githubusercontent.com/u/1203881?v=4","gravatar_id":"","url":"https://api.github.com/users/MichaReiser","html_url":"https://github.com/MichaReiser","followers_url":"https://api.github.com/users/MichaReiser/followers","following_url":"https://api.github.com/users/MichaReiser/following{/other_user}","gists_url":"https://api.github.com/users/MichaReiser/gists{/gist_id}","starred_url":"https://api.github.com/users/MichaReiser/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MichaReiser/subscriptions","organizations_url":"https://api.github.com/users/MichaReiser/orgs","repos_url":"https://api.github.com/users/MichaReiser/repos","events_url":"https://api.github.com/users/MichaReiser/events{/privacy}","received_events_url":"https://api.github.com/users/MichaReiser/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Probably not because the `mdtest` framework doesn't depend on `red_knot_project`, unless we decide to change that.","created_at":"2025-02-07T10:00:21Z","updated_at":"2025-02-07T10:00:21Z","html_url":"https://github.com/astral-sh/ruff/pull/16000#discussion_r1946274087","pull_request_url":"https://api.github.com/repos/astral-sh/ruff/pulls/16000","_links":{"self":{"href":"https://api.github.com/repos/astral-sh/ruff/pulls/comments/1946274087"},"html":{"href":"https://github.com/astral-sh/ruff/pull/16000#discussion_r1946274087"},"pull_request":{"href":"https://api.github.com/repos/astral-sh/ruff/pulls/16000"}},"reactions":{"url":"https://api.github.com/repos/astral-sh/ruff/pulls/comments/1946274087/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":19,"original_line":19,"side":"RIGHT","in_reply_to_id":1946208119,"author_association":"MEMBER","original_position":19,"position":19,"subject_type":"line"},{"url":"https://api.github.com/repos/astral-sh/ruff/pulls/comments/1946350354","pull_request_review_id":2601437666,"id":1946350354,"node_id":"PRRC_kwDOHy0Bzc50AvMS","diff_hunk":"@@ -0,0 +1,53 @@\n+use std::sync::Arc;\n+\n+use red_knot_python_semantic::lint::RuleSelection;\n+\n+/// The resolved [`super::Options`] for the project.\n+///\n+/// Unlike [`super::Options`], the struct has default values filled in and\n+/// uses representations that are optimized for reads (instead of preserving the source representation).\n+/// It's also not required that this structure precisely resembles the TOML schema, although\n+/// it's encouraged to use a similar structure.\n+///\n+/// It's worth considering to adding a salsa query for specific settings to\n+/// limite the blast radius when only some settings change. For example,\n+/// changing the terminal settings shouldn't invalidate any core type-checking queries.\n+/// This can be achieved by adding a salsa query for the type checking specific settings.\n+///\n+/// Settings that are part of [`ProgramSettings`] are not included here.\n+#[derive(Clone, Debug, Eq, PartialEq)]\n+pub struct Settings {","path":"crates/red_knot_project/src/metadata/settings.rs","commit_id":"71e4b8938044fb18f88b6d07378d1d2069ed7792","original_commit_id":"9020e417b82e363c8d203d5362a7ab8969b7e187","user":{"login":"dhruvmanila","id":67177269,"node_id":"MDQ6VXNlcjY3MTc3MjY5","avatar_url":"https://avatars.githubusercontent.com/u/67177269?v=4","gravatar_id":"","url":"https://api.github.com/users/dhruvmanila","html_url":"https://github.com/dhruvmanila","followers_url":"https://api.github.com/users/dhruvmanila/followers","following_url":"https://api.github.com/users/dhruvmanila/following{/other_user}","gists_url":"https://api.github.com/users/dhruvmanila/gists{/gist_id}","starred_url":"https://api.github.com/users/dhruvmanila/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dhruvmanila/subscriptions","organizations_url":"https://api.github.com/users/dhruvmanila/orgs","repos_url":"https://api.github.com/users/dhruvmanila/repos","events_url":"https://api.github.com/users/dhruvmanila/events{/privacy}","received_events_url":"https://api.github.com/users/dhruvmanila/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Ah right.","created_at":"2025-02-07T10:57:50Z","updated_at":"2025-02-07T10:57:51Z","html_url":"https://github.com/astral-sh/ruff/pull/16000#discussion_r1946350354","pull_request_url":"https://api.github.com/repos/astral-sh/ruff/pulls/16000","_links":{"self":{"href":"https://api.github.com/repos/astral-sh/ruff/pulls/comments/1946350354"},"html":{"href":"https://github.com/astral-sh/ruff/pull/16000#discussion_r1946350354"},"pull_request":{"href":"https://api.github.com/repos/astral-sh/ruff/pulls/16000"}},"reactions":{"url":"https://api.github.com/repos/astral-sh/ruff/pulls/comments/1946350354/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":19,"original_line":19,"side":"RIGHT","in_reply_to_id":1946208119,"author_association":"MEMBER","original_position":19,"position":19,"subject_type":"line"}] \ No newline at end of file diff --git a/crates/travelagent-forge-github/tests/fixtures/ruff_16000_commits.json b/crates/travelagent-forge-github/tests/fixtures/ruff_16000_commits.json new file mode 100644 index 0000000..590d680 --- /dev/null +++ b/crates/travelagent-forge-github/tests/fixtures/ruff_16000_commits.json @@ -0,0 +1 @@ +[{"sha":"e341600174b182133b3bf3c6e47b6d6de6c3285d","node_id":"C_kwDOHy0BzdoAKGUzNDE2MDAxNzRiMTgyMTMzYjNiZjNjNmU0N2I2ZDZkZTZjMzI4NWQ","commit":{"author":{"name":"Micha Reiser","email":"micha@reiser.io","date":"2025-02-06T18:00:49Z"},"committer":{"name":"Micha Reiser","email":"micha@reiser.io","date":"2025-02-07T10:01:47Z"},"message":"[red-knot] Resolve `Options` to `Settings`","tree":{"sha":"1276903a6d52d948f0946cf429f9bde0c8970c38","url":"https://api.github.com/repos/astral-sh/ruff/git/trees/1276903a6d52d948f0946cf429f9bde0c8970c38"},"url":"https://api.github.com/repos/astral-sh/ruff/git/commits/e341600174b182133b3bf3c6e47b6d6de6c3285d","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAgOa4MiU\nC4nSG1fD5GSwaFhzPllYi16mYgxcPYdz8uPQ0AAAAEc3NoOgAAAANnaXQAAAAAAAAABnNo\nYTUxMgAAAGcAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAABAN2TEZNysXbnWZF\nhlWsgisu3W3ibt7qfxDMcm6h/+bRFexq8pa25N1vVuznV+bR0UhY1c5d35AfLGoEYEKEgr\nDgEAARc4\n-----END SSH SIGNATURE-----","payload":"tree 1276903a6d52d948f0946cf429f9bde0c8970c38\nparent 26c37b1e0e745b104df51571e69f9c9357c2d3a6\nauthor Micha Reiser 1738864849 +0100\ncommitter Micha Reiser 1738922507 +0100\n\n[red-knot] Resolve `Options` to `Settings`\n","verified_at":"2025-02-07T10:03:54Z"}},"url":"https://api.github.com/repos/astral-sh/ruff/commits/e341600174b182133b3bf3c6e47b6d6de6c3285d","html_url":"https://github.com/astral-sh/ruff/commit/e341600174b182133b3bf3c6e47b6d6de6c3285d","comments_url":"https://api.github.com/repos/astral-sh/ruff/commits/e341600174b182133b3bf3c6e47b6d6de6c3285d/comments","author":{"login":"MichaReiser","id":1203881,"node_id":"MDQ6VXNlcjEyMDM4ODE=","avatar_url":"https://avatars.githubusercontent.com/u/1203881?v=4","gravatar_id":"","url":"https://api.github.com/users/MichaReiser","html_url":"https://github.com/MichaReiser","followers_url":"https://api.github.com/users/MichaReiser/followers","following_url":"https://api.github.com/users/MichaReiser/following{/other_user}","gists_url":"https://api.github.com/users/MichaReiser/gists{/gist_id}","starred_url":"https://api.github.com/users/MichaReiser/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MichaReiser/subscriptions","organizations_url":"https://api.github.com/users/MichaReiser/orgs","repos_url":"https://api.github.com/users/MichaReiser/repos","events_url":"https://api.github.com/users/MichaReiser/events{/privacy}","received_events_url":"https://api.github.com/users/MichaReiser/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"MichaReiser","id":1203881,"node_id":"MDQ6VXNlcjEyMDM4ODE=","avatar_url":"https://avatars.githubusercontent.com/u/1203881?v=4","gravatar_id":"","url":"https://api.github.com/users/MichaReiser","html_url":"https://github.com/MichaReiser","followers_url":"https://api.github.com/users/MichaReiser/followers","following_url":"https://api.github.com/users/MichaReiser/following{/other_user}","gists_url":"https://api.github.com/users/MichaReiser/gists{/gist_id}","starred_url":"https://api.github.com/users/MichaReiser/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MichaReiser/subscriptions","organizations_url":"https://api.github.com/users/MichaReiser/orgs","repos_url":"https://api.github.com/users/MichaReiser/repos","events_url":"https://api.github.com/users/MichaReiser/events{/privacy}","received_events_url":"https://api.github.com/users/MichaReiser/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"26c37b1e0e745b104df51571e69f9c9357c2d3a6","url":"https://api.github.com/repos/astral-sh/ruff/commits/26c37b1e0e745b104df51571e69f9c9357c2d3a6","html_url":"https://github.com/astral-sh/ruff/commit/26c37b1e0e745b104df51571e69f9c9357c2d3a6"}]},{"sha":"9a3f1b07415a256790fe186b6923fcca7d3ffd56","node_id":"C_kwDOHy0BzdoAKDlhM2YxYjA3NDE1YTI1Njc5MGZlMTg2YjY5MjNmY2NhN2QzZmZkNTY","commit":{"author":{"name":"Micha Reiser","email":"micha@reiser.io","date":"2025-02-07T10:03:44Z"},"committer":{"name":"Micha Reiser","email":"micha@reiser.io","date":"2025-02-07T10:03:44Z"},"message":"Implement JsonSchema for TerminalOptions","tree":{"sha":"6fe6cd4f5876a1c58b8ac5d68504cc4733f6a3d2","url":"https://api.github.com/repos/astral-sh/ruff/git/trees/6fe6cd4f5876a1c58b8ac5d68504cc4733f6a3d2"},"url":"https://api.github.com/repos/astral-sh/ruff/git/commits/9a3f1b07415a256790fe186b6923fcca7d3ffd56","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAgOa4MiU\nC4nSG1fD5GSwaFhzPllYi16mYgxcPYdz8uPQ0AAAAEc3NoOgAAAANnaXQAAAAAAAAABnNo\nYTUxMgAAAGcAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAABAG7ZBXWGlC7HIua\nP3/QwDfb6SmENMO/cQaAa9drpT7Ng/5yh52k0jwQbR9pRVDNgFGW/F8MTDjUfWBYBD1FVh\nCAEAARc7\n-----END SSH SIGNATURE-----","payload":"tree 6fe6cd4f5876a1c58b8ac5d68504cc4733f6a3d2\nparent e341600174b182133b3bf3c6e47b6d6de6c3285d\nauthor Micha Reiser 1738922624 +0100\ncommitter Micha Reiser 1738922624 +0100\n\nImplement JsonSchema for TerminalOptions\n","verified_at":"2025-02-07T10:03:54Z"}},"url":"https://api.github.com/repos/astral-sh/ruff/commits/9a3f1b07415a256790fe186b6923fcca7d3ffd56","html_url":"https://github.com/astral-sh/ruff/commit/9a3f1b07415a256790fe186b6923fcca7d3ffd56","comments_url":"https://api.github.com/repos/astral-sh/ruff/commits/9a3f1b07415a256790fe186b6923fcca7d3ffd56/comments","author":{"login":"MichaReiser","id":1203881,"node_id":"MDQ6VXNlcjEyMDM4ODE=","avatar_url":"https://avatars.githubusercontent.com/u/1203881?v=4","gravatar_id":"","url":"https://api.github.com/users/MichaReiser","html_url":"https://github.com/MichaReiser","followers_url":"https://api.github.com/users/MichaReiser/followers","following_url":"https://api.github.com/users/MichaReiser/following{/other_user}","gists_url":"https://api.github.com/users/MichaReiser/gists{/gist_id}","starred_url":"https://api.github.com/users/MichaReiser/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MichaReiser/subscriptions","organizations_url":"https://api.github.com/users/MichaReiser/orgs","repos_url":"https://api.github.com/users/MichaReiser/repos","events_url":"https://api.github.com/users/MichaReiser/events{/privacy}","received_events_url":"https://api.github.com/users/MichaReiser/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"MichaReiser","id":1203881,"node_id":"MDQ6VXNlcjEyMDM4ODE=","avatar_url":"https://avatars.githubusercontent.com/u/1203881?v=4","gravatar_id":"","url":"https://api.github.com/users/MichaReiser","html_url":"https://github.com/MichaReiser","followers_url":"https://api.github.com/users/MichaReiser/followers","following_url":"https://api.github.com/users/MichaReiser/following{/other_user}","gists_url":"https://api.github.com/users/MichaReiser/gists{/gist_id}","starred_url":"https://api.github.com/users/MichaReiser/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MichaReiser/subscriptions","organizations_url":"https://api.github.com/users/MichaReiser/orgs","repos_url":"https://api.github.com/users/MichaReiser/repos","events_url":"https://api.github.com/users/MichaReiser/events{/privacy}","received_events_url":"https://api.github.com/users/MichaReiser/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"e341600174b182133b3bf3c6e47b6d6de6c3285d","url":"https://api.github.com/repos/astral-sh/ruff/commits/e341600174b182133b3bf3c6e47b6d6de6c3285d","html_url":"https://github.com/astral-sh/ruff/commit/e341600174b182133b3bf3c6e47b6d6de6c3285d"}]},{"sha":"71e4b8938044fb18f88b6d07378d1d2069ed7792","node_id":"C_kwDOHy0BzdoAKDcxZTRiODkzODA0NGZiMThmODhiNmQwNzM3OGQxZDIwNjllZDc3OTI","commit":{"author":{"name":"Micha Reiser","email":"micha@reiser.io","date":"2025-02-07T10:10:14Z"},"committer":{"name":"Micha Reiser","email":"micha@reiser.io","date":"2025-02-07T10:15:10Z"},"message":"Fix fuzz","tree":{"sha":"8ffb8308c420598cc9dafffbc5440ebb8c08a0ff","url":"https://api.github.com/repos/astral-sh/ruff/git/trees/8ffb8308c420598cc9dafffbc5440ebb8c08a0ff"},"url":"https://api.github.com/repos/astral-sh/ruff/git/commits/71e4b8938044fb18f88b6d07378d1d2069ed7792","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAAEoAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAgOa4MiU\nC4nSG1fD5GSwaFhzPllYi16mYgxcPYdz8uPQ0AAAAEc3NoOgAAAANnaXQAAAAAAAAABnNo\nYTUxMgAAAGcAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAABAM2Pg03Tfa8Pd9n\nIHCcozPF8653n6EoAhGlrBM4ivBgEt7sasTuK3YkvNy9qMS3qKqshtxLsoXybLcT3vGeol\nCgEAARdH\n-----END SSH SIGNATURE-----","payload":"tree 8ffb8308c420598cc9dafffbc5440ebb8c08a0ff\nparent 9a3f1b07415a256790fe186b6923fcca7d3ffd56\nauthor Micha Reiser 1738923014 +0100\ncommitter Micha Reiser 1738923310 +0100\n\nFix fuzz\n","verified_at":"2025-02-07T10:15:16Z"}},"url":"https://api.github.com/repos/astral-sh/ruff/commits/71e4b8938044fb18f88b6d07378d1d2069ed7792","html_url":"https://github.com/astral-sh/ruff/commit/71e4b8938044fb18f88b6d07378d1d2069ed7792","comments_url":"https://api.github.com/repos/astral-sh/ruff/commits/71e4b8938044fb18f88b6d07378d1d2069ed7792/comments","author":{"login":"MichaReiser","id":1203881,"node_id":"MDQ6VXNlcjEyMDM4ODE=","avatar_url":"https://avatars.githubusercontent.com/u/1203881?v=4","gravatar_id":"","url":"https://api.github.com/users/MichaReiser","html_url":"https://github.com/MichaReiser","followers_url":"https://api.github.com/users/MichaReiser/followers","following_url":"https://api.github.com/users/MichaReiser/following{/other_user}","gists_url":"https://api.github.com/users/MichaReiser/gists{/gist_id}","starred_url":"https://api.github.com/users/MichaReiser/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MichaReiser/subscriptions","organizations_url":"https://api.github.com/users/MichaReiser/orgs","repos_url":"https://api.github.com/users/MichaReiser/repos","events_url":"https://api.github.com/users/MichaReiser/events{/privacy}","received_events_url":"https://api.github.com/users/MichaReiser/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"MichaReiser","id":1203881,"node_id":"MDQ6VXNlcjEyMDM4ODE=","avatar_url":"https://avatars.githubusercontent.com/u/1203881?v=4","gravatar_id":"","url":"https://api.github.com/users/MichaReiser","html_url":"https://github.com/MichaReiser","followers_url":"https://api.github.com/users/MichaReiser/followers","following_url":"https://api.github.com/users/MichaReiser/following{/other_user}","gists_url":"https://api.github.com/users/MichaReiser/gists{/gist_id}","starred_url":"https://api.github.com/users/MichaReiser/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MichaReiser/subscriptions","organizations_url":"https://api.github.com/users/MichaReiser/orgs","repos_url":"https://api.github.com/users/MichaReiser/repos","events_url":"https://api.github.com/users/MichaReiser/events{/privacy}","received_events_url":"https://api.github.com/users/MichaReiser/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"9a3f1b07415a256790fe186b6923fcca7d3ffd56","url":"https://api.github.com/repos/astral-sh/ruff/commits/9a3f1b07415a256790fe186b6923fcca7d3ffd56","html_url":"https://github.com/astral-sh/ruff/commit/9a3f1b07415a256790fe186b6923fcca7d3ffd56"}]}] \ No newline at end of file diff --git a/crates/travelagent-forge-github/tests/fixtures/ruff_16000_files.json b/crates/travelagent-forge-github/tests/fixtures/ruff_16000_files.json new file mode 100644 index 0000000..55ba33c --- /dev/null +++ b/crates/travelagent-forge-github/tests/fixtures/ruff_16000_files.json @@ -0,0 +1 @@ +[{"sha":"33ec3595c4bd70bc3dfc2a68adf03f036b4024d0","filename":"crates/red_knot/src/args.rs","status":"modified","additions":6,"deletions":3,"changes":9,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot%2Fsrc%2Fargs.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot%2Fsrc%2Fargs.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/crates%2Fred_knot%2Fsrc%2Fargs.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -1,7 +1,7 @@\n use crate::logging::Verbosity;\n use crate::python_version::PythonVersion;\n use clap::{ArgAction, ArgMatches, Error, Parser};\n-use red_knot_project::metadata::options::{EnvironmentOptions, Options};\n+use red_knot_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions};\n use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};\n use red_knot_python_semantic::lint;\n use ruff_db::system::SystemPathBuf;\n@@ -67,8 +67,8 @@ pub(crate) struct CheckCommand {\n pub(crate) rules: RulesArg,\n \n /// Use exit code 1 if there are any warning-level diagnostics.\n- #[arg(long, conflicts_with = \"exit_zero\")]\n- pub(crate) error_on_warning: bool,\n+ #[arg(long, conflicts_with = \"exit_zero\", default_missing_value = \"true\", num_args=0..1)]\n+ pub(crate) error_on_warning: Option,\n \n /// Always use exit code 0, even when there are error-level diagnostics.\n #[arg(long)]\n@@ -107,6 +107,9 @@ impl CheckCommand {\n }),\n ..EnvironmentOptions::default()\n }),\n+ terminal: Some(TerminalOptions {\n+ error_on_warning: self.error_on_warning,\n+ }),\n rules,\n ..Default::default()\n }"},{"sha":"3c1751e5408ccdbe6f86bb81481e2a5ba7c39ac9","filename":"crates/red_knot/src/main.rs","status":"modified","additions":11,"deletions":18,"changes":29,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot%2Fsrc%2Fmain.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot%2Fsrc%2Fmain.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/crates%2Fred_knot%2Fsrc%2Fmain.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -11,8 +11,8 @@ use clap::Parser;\n use colored::Colorize;\n use crossbeam::channel as crossbeam_channel;\n use red_knot_project::metadata::options::Options;\n-use red_knot_project::watch;\n use red_knot_project::watch::ProjectWatcher;\n+use red_knot_project::{watch, Db};\n use red_knot_project::{ProjectDatabase, ProjectMetadata};\n use red_knot_server::run_server;\n use ruff_db::diagnostic::{Diagnostic, Severity};\n@@ -97,19 +97,14 @@ fn run_check(args: CheckCommand) -> anyhow::Result {\n let system = OsSystem::new(cwd);\n let watch = args.watch;\n let exit_zero = args.exit_zero;\n- let min_error_severity = if args.error_on_warning {\n- Severity::Warning\n- } else {\n- Severity::Error\n- };\n \n let cli_options = args.into_options();\n let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;\n workspace_metadata.apply_cli_options(cli_options.clone());\n \n let mut db = ProjectDatabase::new(workspace_metadata, system)?;\n \n- let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options, min_error_severity);\n+ let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);\n \n // Listen to Ctrl+C and abort the watch mode.\n let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));\n@@ -167,18 +162,10 @@ struct MainLoop {\n watcher: Option,\n \n cli_options: Options,\n-\n- /// The minimum severity to consider an error when deciding the exit status.\n- ///\n- /// TODO(micha): Get from the terminal settings.\n- min_error_severity: Severity,\n }\n \n impl MainLoop {\n- fn new(\n- cli_options: Options,\n- min_error_severity: Severity,\n- ) -> (Self, MainLoopCancellationToken) {\n+ fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {\n let (sender, receiver) = crossbeam_channel::bounded(10);\n \n (\n@@ -187,7 +174,6 @@ impl MainLoop {\n receiver,\n watcher: None,\n cli_options,\n- min_error_severity,\n },\n MainLoopCancellationToken { sender },\n )\n@@ -245,9 +231,16 @@ impl MainLoop {\n result,\n revision: check_revision,\n } => {\n+ let min_error_severity =\n+ if db.project().settings(db).terminal().error_on_warning {\n+ Severity::Warning\n+ } else {\n+ Severity::Error\n+ };\n+\n let failed = result\n .iter()\n- .any(|diagnostic| diagnostic.severity() >= self.min_error_severity);\n+ .any(|diagnostic| diagnostic.severity() >= min_error_severity);\n \n if check_revision == revision {\n #[allow(clippy::print_stdout)]"},{"sha":"28360dfc878a17c09fa3d61b3b02889948b5c99c","filename":"crates/red_knot/tests/cli.rs","status":"modified","additions":31,"deletions":0,"changes":31,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot%2Ftests%2Fcli.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot%2Ftests%2Fcli.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/crates%2Fred_knot%2Ftests%2Fcli.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -575,6 +575,37 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {\n Ok(())\n }\n \n+#[test]\n+fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> {\n+ let case = TestCase::with_files([\n+ (\"test.py\", r\"print(x) # [unresolved-reference]\"),\n+ (\n+ \"knot.toml\",\n+ r#\"\n+ [terminal]\n+ error-on-warning = true\n+ \"#,\n+ ),\n+ ])?;\n+\n+ assert_cmd_snapshot!(case.command(), @r###\"\n+ success: false\n+ exit_code: 1\n+ ----- stdout -----\n+ warning: lint:unresolved-reference\n+ --> /test.py:1:7\n+ |\n+ 1 | print(x) # [unresolved-reference]\n+ | - Name `x` used when not defined\n+ |\n+\n+\n+ ----- stderr -----\n+ \"###);\n+\n+ Ok(())\n+}\n+\n #[test]\n fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {\n let case = TestCase::with_file("},{"sha":"96ff04301cbe6732ce2420e09e794d9f3eba7622","filename":"crates/red_knot_project/src/db.rs","status":"modified","additions":4,"deletions":6,"changes":10,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_project%2Fsrc%2Fdb.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_project%2Fsrc%2Fdb.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/crates%2Fred_knot_project%2Fsrc%2Fdb.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -114,8 +114,8 @@ impl SemanticDb for ProjectDatabase {\n project.is_file_open(self, file)\n }\n \n- fn rule_selection(&self) -> &RuleSelection {\n- self.project().rule_selection(self)\n+ fn rule_selection(&self) -> Arc {\n+ self.project().rules(self)\n }\n \n fn lint_registry(&self) -> &LintRegistry {\n@@ -186,7 +186,6 @@ pub(crate) mod tests {\n files: Files,\n system: TestSystem,\n vendored: VendoredFileSystem,\n- rule_selection: RuleSelection,\n project: Option,\n }\n \n@@ -198,7 +197,6 @@ pub(crate) mod tests {\n vendored: red_knot_vendored::file_system().clone(),\n files: Files::default(),\n events: Arc::default(),\n- rule_selection: RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY),\n project: None,\n };\n \n@@ -270,8 +268,8 @@ pub(crate) mod tests {\n !file.path(self).is_vendored_path()\n }\n \n- fn rule_selection(&self) -> &RuleSelection {\n- &self.rule_selection\n+ fn rule_selection(&self) -> Arc {\n+ self.project().rules(self)\n }\n \n fn lint_registry(&self) -> &LintRegistry {"},{"sha":"3d8ee4ae4ce17ec4d960742de1880d5821df5773","filename":"crates/red_knot_project/src/lib.rs","status":"modified","additions":35,"deletions":19,"changes":54,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_project%2Fsrc%2Flib.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_project%2Fsrc%2Flib.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/crates%2Fred_knot_project%2Fsrc%2Flib.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -3,6 +3,7 @@\n use crate::metadata::options::OptionDiagnostic;\n pub use db::{Db, ProjectDatabase};\n use files::{Index, Indexed, IndexedFiles};\n+use metadata::settings::Settings;\n pub use metadata::{ProjectDiscoveryError, ProjectMetadata};\n use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};\n use red_knot_python_semantic::register_lints;\n@@ -66,12 +67,22 @@ pub struct Project {\n /// The metadata describing the project, including the unresolved options.\n #[return_ref]\n pub metadata: ProjectMetadata,\n+\n+ /// The resolved project settings.\n+ #[return_ref]\n+ pub settings: Settings,\n+\n+ /// Diagnostics that were generated when resolving the project settings.\n+ #[return_ref]\n+ settings_diagnostics: Vec,\n }\n \n #[salsa::tracked]\n impl Project {\n pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Self {\n- Project::builder(metadata)\n+ let (settings, settings_diagnostics) = metadata.options().to_settings(db);\n+\n+ Project::builder(metadata, settings, settings_diagnostics)\n .durability(Durability::MEDIUM)\n .open_fileset_durability(Durability::LOW)\n .file_set_durability(Durability::LOW)\n@@ -86,30 +97,37 @@ impl Project {\n self.metadata(db).name()\n }\n \n+ /// Returns the resolved linter rules for the project.\n+ ///\n+ /// This is a salsa query to prevent re-computing queries if other, unrelated\n+ /// settings change. For example, we don't want that changing the terminal settings\n+ /// invalidates any type checking queries.\n+ #[salsa::tracked]\n+ pub fn rules(self, db: &dyn Db) -> Arc {\n+ self.settings(db).to_rules()\n+ }\n+\n pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) {\n tracing::debug!(\"Reloading project\");\n assert_eq!(self.root(db), metadata.root());\n \n if &metadata != self.metadata(db) {\n+ let (settings, settings_diagnostics) = metadata.options().to_settings(db);\n+\n+ if self.settings(db) != &settings {\n+ self.set_settings(db).to(settings);\n+ }\n+\n+ if self.settings_diagnostics(db) != &settings_diagnostics {\n+ self.set_settings_diagnostics(db).to(settings_diagnostics);\n+ }\n+\n self.set_metadata(db).to(metadata);\n }\n \n self.reload_files(db);\n }\n \n- pub fn rule_selection(self, db: &dyn Db) -> &RuleSelection {\n- let (selection, _) = self.rule_selection_with_diagnostics(db);\n- selection\n- }\n-\n- #[salsa::tracked(return_ref)]\n- fn rule_selection_with_diagnostics(\n- self,\n- db: &dyn Db,\n- ) -> (RuleSelection, Vec) {\n- self.metadata(db).options().to_rule_selection(db)\n- }\n-\n /// Checks all open files in the project and its dependencies.\n pub(crate) fn check(self, db: &ProjectDatabase) -> Vec> {\n let project_span = tracing::debug_span!(\"Project::check\");\n@@ -118,8 +136,7 @@ impl Project {\n tracing::debug!(\"Checking project '{name}'\", name = self.name(db));\n \n let mut diagnostics: Vec> = Vec::new();\n- let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);\n- diagnostics.extend(options_diagnostics.iter().map(|diagnostic| {\n+ diagnostics.extend(self.settings_diagnostics(db).iter().map(|diagnostic| {\n let diagnostic: Box = Box::new(diagnostic.clone());\n diagnostic\n }));\n@@ -151,9 +168,8 @@ impl Project {\n }\n \n pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec> {\n- let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);\n-\n- let mut file_diagnostics: Vec<_> = options_diagnostics\n+ let mut file_diagnostics: Vec<_> = self\n+ .settings_diagnostics(db)\n .iter()\n .map(|diagnostic| {\n let diagnostic: Box = Box::new(diagnostic.clone());"},{"sha":"c7f2009c0e244ce093b88a633464cf22e2dfc54b","filename":"crates/red_knot_project/src/metadata.rs","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_project%2Fsrc%2Fmetadata.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_project%2Fsrc%2Fmetadata.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/crates%2Fred_knot_project%2Fsrc%2Fmetadata.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -12,6 +12,7 @@ use options::Options;\n \n pub mod options;\n pub mod pyproject;\n+pub mod settings;\n pub mod value;\n \n #[derive(Debug, PartialEq, Eq)]"},{"sha":"401dcf1233345c3baeaada3cee5ecf9945e66a7c","filename":"crates/red_knot_project/src/metadata/options.rs","status":"modified","additions":31,"deletions":1,"changes":32,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_project%2Fsrc%2Fmetadata%2Foptions.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_project%2Fsrc%2Fmetadata%2Foptions.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/crates%2Fred_knot_project%2Fsrc%2Fmetadata%2Foptions.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -15,6 +15,8 @@ use std::borrow::Cow;\n use std::fmt::Debug;\n use thiserror::Error;\n \n+use super::settings::{Settings, TerminalSettings};\n+\n /// The options for the project.\n #[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]\n #[serde(rename_all = \"kebab-case\", deny_unknown_fields)]\n@@ -30,6 +32,9 @@ pub struct Options {\n /// Configures the enabled lints and their severity.\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub rules: Option,\n+\n+ #[serde(skip_serializing_if = \"Option::is_none\")]\n+ pub terminal: Option,\n }\n \n impl Options {\n@@ -110,7 +115,22 @@ impl Options {\n }\n \n #[must_use]\n- pub(crate) fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec) {\n+ pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec) {\n+ let (rules, diagnostics) = self.to_rule_selection(db);\n+\n+ let mut settings = Settings::new(rules);\n+\n+ if let Some(terminal) = self.terminal.as_ref() {\n+ settings.set_terminal(TerminalSettings {\n+ error_on_warning: terminal.error_on_warning.unwrap_or_default(),\n+ });\n+ }\n+\n+ (settings, diagnostics)\n+ }\n+\n+ #[must_use]\n+ fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec) {\n let registry = db.lint_registry();\n let mut diagnostics = Vec::new();\n \n@@ -244,6 +264,16 @@ impl FromIterator<(RangedValue, RangedValue)> for Rules {\n }\n }\n \n+#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]\n+#[serde(rename_all = \"kebab-case\", deny_unknown_fields)]\n+#[cfg_attr(feature = \"schemars\", derive(schemars::JsonSchema))]\n+pub struct TerminalOptions {\n+ /// Use exit code 1 if there are any warning-level diagnostics.\n+ ///\n+ /// Defaults to `false`.\n+ pub error_on_warning: Option,\n+}\n+\n #[cfg(feature = \"schemars\")]\n mod schema {\n use crate::DEFAULT_LINT_REGISTRY;"},{"sha":"c29d23ed61367a1a5cbe75970ebbc03606de66b0","filename":"crates/red_knot_project/src/metadata/settings.rs","status":"added","additions":53,"deletions":0,"changes":53,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_project%2Fsrc%2Fmetadata%2Fsettings.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_project%2Fsrc%2Fmetadata%2Fsettings.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/crates%2Fred_knot_project%2Fsrc%2Fmetadata%2Fsettings.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -0,0 +1,53 @@\n+use std::sync::Arc;\n+\n+use red_knot_python_semantic::lint::RuleSelection;\n+\n+/// The resolved [`super::Options`] for the project.\n+///\n+/// Unlike [`super::Options`], the struct has default values filled in and\n+/// uses representations that are optimized for reads (instead of preserving the source representation).\n+/// It's also not required that this structure precisely resembles the TOML schema, although\n+/// it's encouraged to use a similar structure.\n+///\n+/// It's worth considering to adding a salsa query for specific settings to\n+/// limit the blast radius when only some settings change. For example,\n+/// changing the terminal settings shouldn't invalidate any core type-checking queries.\n+/// This can be achieved by adding a salsa query for the type checking specific settings.\n+///\n+/// Settings that are part of [`red_knot_python_semantic::ProgramSettings`] are not included here.\n+#[derive(Clone, Debug, Eq, PartialEq)]\n+pub struct Settings {\n+ rules: Arc,\n+\n+ terminal: TerminalSettings,\n+}\n+\n+impl Settings {\n+ pub fn new(rules: RuleSelection) -> Self {\n+ Self {\n+ rules: Arc::new(rules),\n+ terminal: TerminalSettings::default(),\n+ }\n+ }\n+\n+ pub fn rules(&self) -> &RuleSelection {\n+ &self.rules\n+ }\n+\n+ pub fn to_rules(&self) -> Arc {\n+ self.rules.clone()\n+ }\n+\n+ pub fn terminal(&self) -> &TerminalSettings {\n+ &self.terminal\n+ }\n+\n+ pub fn set_terminal(&mut self, terminal: TerminalSettings) {\n+ self.terminal = terminal;\n+ }\n+}\n+\n+#[derive(Debug, Clone, PartialEq, Eq, Default)]\n+pub struct TerminalSettings {\n+ pub error_on_warning: bool,\n+}"},{"sha":"8b94da20ffcf328b1ddf7df3418ed56229964a0c","filename":"crates/red_knot_python_semantic/src/db.rs","status":"modified","additions":5,"deletions":3,"changes":8,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_python_semantic%2Fsrc%2Fdb.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_python_semantic%2Fsrc%2Fdb.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/crates%2Fred_knot_python_semantic%2Fsrc%2Fdb.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -1,3 +1,5 @@\n+use std::sync::Arc;\n+\n use crate::lint::{LintRegistry, RuleSelection};\n use ruff_db::files::File;\n use ruff_db::{Db as SourceDb, Upcast};\n@@ -7,7 +9,7 @@ use ruff_db::{Db as SourceDb, Upcast};\n pub trait Db: SourceDb + Upcast {\n fn is_file_open(&self, file: File) -> bool;\n \n- fn rule_selection(&self) -> &RuleSelection;\n+ fn rule_selection(&self) -> Arc;\n \n fn lint_registry(&self) -> &LintRegistry;\n }\n@@ -111,8 +113,8 @@ pub(crate) mod tests {\n !file.path(self).is_vendored_path()\n }\n \n- fn rule_selection(&self) -> &RuleSelection {\n- &self.rule_selection\n+ fn rule_selection(&self) -> Arc {\n+ self.rule_selection.clone()\n }\n \n fn lint_registry(&self) -> &LintRegistry {"},{"sha":"12b34df4fa8cc195bba10f910e6d4c2ed22f8e8f","filename":"crates/red_knot_test/src/db.rs","status":"modified","additions":6,"deletions":4,"changes":10,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_test%2Fsrc%2Fdb.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fred_knot_test%2Fsrc%2Fdb.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/crates%2Fred_knot_test%2Fsrc%2Fdb.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -1,3 +1,5 @@\n+use std::sync::Arc;\n+\n use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};\n use red_knot_python_semantic::{\n default_lint_registry, Db as SemanticDb, Program, ProgramSettings, PythonPlatform,\n@@ -16,7 +18,7 @@ pub(crate) struct Db {\n files: Files,\n system: TestSystem,\n vendored: VendoredFileSystem,\n- rule_selection: RuleSelection,\n+ rule_selection: Arc,\n }\n \n impl Db {\n@@ -29,7 +31,7 @@ impl Db {\n system: TestSystem::default(),\n vendored: red_knot_vendored::file_system().clone(),\n files: Files::default(),\n- rule_selection,\n+ rule_selection: Arc::new(rule_selection),\n };\n \n db.memory_file_system()\n@@ -94,8 +96,8 @@ impl SemanticDb for Db {\n !file.path(self).is_vendored_path()\n }\n \n- fn rule_selection(&self) -> &RuleSelection {\n- &self.rule_selection\n+ fn rule_selection(&self) -> Arc {\n+ self.rule_selection.clone()\n }\n \n fn lint_registry(&self) -> &LintRegistry {"},{"sha":"ac51abc8b253499edd90846a590f482d9a10b0be","filename":"crates/ruff_graph/src/db.rs","status":"modified","additions":2,"deletions":2,"changes":4,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fruff_graph%2Fsrc%2Fdb.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/crates%2Fruff_graph%2Fsrc%2Fdb.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/crates%2Fruff_graph%2Fsrc%2Fdb.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -79,8 +79,8 @@ impl Db for ModuleDb {\n !file.path(self).is_vendored_path()\n }\n \n- fn rule_selection(&self) -> &RuleSelection {\n- &self.rule_selection\n+ fn rule_selection(&self) -> Arc {\n+ self.rule_selection.clone()\n }\n \n fn lint_registry(&self) -> &LintRegistry {"},{"sha":"758746a5e01bb71909e28171639c469d253a2b3f","filename":"fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs","status":"modified","additions":6,"deletions":6,"changes":12,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/fuzz%2Ffuzz_targets%2Fred_knot_check_invalid_syntax.rs","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/fuzz%2Ffuzz_targets%2Fred_knot_check_invalid_syntax.rs","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/fuzz%2Ffuzz_targets%2Fred_knot_check_invalid_syntax.rs?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -3,7 +3,7 @@\n \n #![no_main]\n \n-use std::sync::{Mutex, OnceLock};\n+use std::sync::{Arc, Mutex, OnceLock};\n \n use libfuzzer_sys::{fuzz_target, Corpus};\n \n@@ -29,8 +29,8 @@ struct TestDb {\n files: Files,\n system: TestSystem,\n vendored: VendoredFileSystem,\n- events: std::sync::Arc>>,\n- rule_selection: std::sync::Arc,\n+ events: Arc>>,\n+ rule_selection: Arc,\n }\n \n impl TestDb {\n@@ -39,7 +39,7 @@ impl TestDb {\n storage: salsa::Storage::default(),\n system: TestSystem::default(),\n vendored: red_knot_vendored::file_system().clone(),\n- events: std::sync::Arc::default(),\n+ events: Arc::default(),\n files: Files::default(),\n rule_selection: RuleSelection::from_registry(default_lint_registry()).into(),\n }\n@@ -86,8 +86,8 @@ impl SemanticDb for TestDb {\n !file.path(self).is_vendored_path()\n }\n \n- fn rule_selection(&self) -> &RuleSelection {\n- &self.rule_selection\n+ fn rule_selection(&self) -> Arc {\n+ self.rule_selection.clone()\n }\n \n fn lint_registry(&self) -> &LintRegistry {"},{"sha":"08e2c57a189dfa7f6d4d50c76d4baa2fc6f7cb42","filename":"knot.schema.json","status":"modified","additions":23,"deletions":0,"changes":23,"blob_url":"https://github.com/astral-sh/ruff/blob/71e4b8938044fb18f88b6d07378d1d2069ed7792/knot.schema.json","raw_url":"https://github.com/astral-sh/ruff/raw/71e4b8938044fb18f88b6d07378d1d2069ed7792/knot.schema.json","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/knot.schema.json?ref=71e4b8938044fb18f88b6d07378d1d2069ed7792","patch":"@@ -35,6 +35,16 @@\n \"type\": \"null\"\n }\n ]\n+ },\n+ \"terminal\": {\n+ \"anyOf\": [\n+ {\n+ \"$ref\": \"#/definitions/TerminalOptions\"\n+ },\n+ {\n+ \"type\": \"null\"\n+ }\n+ ]\n }\n },\n \"additionalProperties\": false,\n@@ -688,6 +698,19 @@\n }\n },\n \"additionalProperties\": false\n+ },\n+ \"TerminalOptions\": {\n+ \"type\": \"object\",\n+ \"properties\": {\n+ \"error-on-warning\": {\n+ \"description\": \"Use exit code 1 if there are any warning-level diagnostics.\\n\\nDefaults to `false`.\",\n+ \"type\": [\n+ \"boolean\",\n+ \"null\"\n+ ]\n+ }\n+ },\n+ \"additionalProperties\": false\n }\n }\n }\n\\ No newline at end of file"}] \ No newline at end of file diff --git a/crates/travelagent-forge-github/tests/fixtures/ruff_16000_pr.json b/crates/travelagent-forge-github/tests/fixtures/ruff_16000_pr.json new file mode 100644 index 0000000..6829bf1 --- /dev/null +++ b/crates/travelagent-forge-github/tests/fixtures/ruff_16000_pr.json @@ -0,0 +1 @@ +{"url":"https://api.github.com/repos/astral-sh/ruff/pulls/16000","id":2320557769,"node_id":"PR_kwDOHy0Bzc6KUObJ","html_url":"https://github.com/astral-sh/ruff/pull/16000","diff_url":"https://github.com/astral-sh/ruff/pull/16000.diff","patch_url":"https://github.com/astral-sh/ruff/pull/16000.patch","issue_url":"https://api.github.com/repos/astral-sh/ruff/issues/16000","number":16000,"state":"closed","locked":false,"title":"[red-knot] Resolve `Options` to `Settings`","user":{"login":"MichaReiser","id":1203881,"node_id":"MDQ6VXNlcjEyMDM4ODE=","avatar_url":"https://avatars.githubusercontent.com/u/1203881?v=4","gravatar_id":"","url":"https://api.github.com/users/MichaReiser","html_url":"https://github.com/MichaReiser","followers_url":"https://api.github.com/users/MichaReiser/followers","following_url":"https://api.github.com/users/MichaReiser/following{/other_user}","gists_url":"https://api.github.com/users/MichaReiser/gists{/gist_id}","starred_url":"https://api.github.com/users/MichaReiser/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MichaReiser/subscriptions","organizations_url":"https://api.github.com/users/MichaReiser/orgs","repos_url":"https://api.github.com/users/MichaReiser/repos","events_url":"https://api.github.com/users/MichaReiser/events{/privacy}","received_events_url":"https://api.github.com/users/MichaReiser/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"## Summary\r\n\r\nThis PR generalize the idea that we may want to emit diagnostics for \r\ninvalid or incompatible configuration values similar to how we already \r\ndo it for `rules`. \r\n\r\nThis PR introduces a new `Settings` struct that is similar to `Options` but, unlike\r\n`Options`, are fields have their default values filled in and they use a representation optimized for reads. \r\n\r\nThe diagnostics created during loading the `Settings` are stored on the `Project` so that we can emit them when calling `check`. \r\n\r\nThe motivation for this work is that it simplifies adding new settings. That's also why I went ahead and added the `terminal.error-on-warning` setting to demonstrate how new settings are added.\r\n\r\n## Test Plan\r\n\r\nExisting tests, new CLI test.\r\n","created_at":"2025-02-06T18:03:02Z","updated_at":"2025-02-10T14:28:49Z","closed_at":"2025-02-10T14:28:45Z","merged_at":"2025-02-10T14:28:45Z","merge_commit_sha":"678b0c2d393733ec33574db94e21d3eb4bdf2396","assignees":[],"requested_reviewers":[{"login":"carljm","id":61586,"node_id":"MDQ6VXNlcjYxNTg2","avatar_url":"https://avatars.githubusercontent.com/u/61586?v=4","gravatar_id":"","url":"https://api.github.com/users/carljm","html_url":"https://github.com/carljm","followers_url":"https://api.github.com/users/carljm/followers","following_url":"https://api.github.com/users/carljm/following{/other_user}","gists_url":"https://api.github.com/users/carljm/gists{/gist_id}","starred_url":"https://api.github.com/users/carljm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/carljm/subscriptions","organizations_url":"https://api.github.com/users/carljm/orgs","repos_url":"https://api.github.com/users/carljm/repos","events_url":"https://api.github.com/users/carljm/events{/privacy}","received_events_url":"https://api.github.com/users/carljm/received_events","type":"User","user_view_type":"public","site_admin":false},{"login":"sharkdp","id":4209276,"node_id":"MDQ6VXNlcjQyMDkyNzY=","avatar_url":"https://avatars.githubusercontent.com/u/4209276?v=4","gravatar_id":"","url":"https://api.github.com/users/sharkdp","html_url":"https://github.com/sharkdp","followers_url":"https://api.github.com/users/sharkdp/followers","following_url":"https://api.github.com/users/sharkdp/following{/other_user}","gists_url":"https://api.github.com/users/sharkdp/gists{/gist_id}","starred_url":"https://api.github.com/users/sharkdp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sharkdp/subscriptions","organizations_url":"https://api.github.com/users/sharkdp/orgs","repos_url":"https://api.github.com/users/sharkdp/repos","events_url":"https://api.github.com/users/sharkdp/events{/privacy}","received_events_url":"https://api.github.com/users/sharkdp/received_events","type":"User","user_view_type":"public","site_admin":false},{"login":"AlexWaygood","id":66076021,"node_id":"MDQ6VXNlcjY2MDc2MDIx","avatar_url":"https://avatars.githubusercontent.com/u/66076021?v=4","gravatar_id":"","url":"https://api.github.com/users/AlexWaygood","html_url":"https://github.com/AlexWaygood","followers_url":"https://api.github.com/users/AlexWaygood/followers","following_url":"https://api.github.com/users/AlexWaygood/following{/other_user}","gists_url":"https://api.github.com/users/AlexWaygood/gists{/gist_id}","starred_url":"https://api.github.com/users/AlexWaygood/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AlexWaygood/subscriptions","organizations_url":"https://api.github.com/users/AlexWaygood/orgs","repos_url":"https://api.github.com/users/AlexWaygood/repos","events_url":"https://api.github.com/users/AlexWaygood/events{/privacy}","received_events_url":"https://api.github.com/users/AlexWaygood/received_events","type":"User","user_view_type":"public","site_admin":false}],"requested_teams":[],"labels":[{"id":4961509542,"node_id":"LA_kwDOHy0Bzc8AAAABJ7qgpg","url":"https://api.github.com/repos/astral-sh/ruff/labels/cli","name":"cli","color":"EF2BB2","default":false,"description":"Related to the command-line interface"},{"id":7024288340,"node_id":"LA_kwDOHy0Bzc8AAAABoq4iVA","url":"https://api.github.com/repos/astral-sh/ruff/labels/ty","name":"ty","color":"46EBE1","default":false,"description":"Multi-file analysis & type inference"}],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/astral-sh/ruff/pulls/16000/commits","review_comments_url":"https://api.github.com/repos/astral-sh/ruff/pulls/16000/comments","review_comment_url":"https://api.github.com/repos/astral-sh/ruff/pulls/comments{/number}","comments_url":"https://api.github.com/repos/astral-sh/ruff/issues/16000/comments","statuses_url":"https://api.github.com/repos/astral-sh/ruff/statuses/71e4b8938044fb18f88b6d07378d1d2069ed7792","head":{"label":"astral-sh:micha/settings","ref":"micha/settings","sha":"71e4b8938044fb18f88b6d07378d1d2069ed7792","user":{"login":"astral-sh","id":115962839,"node_id":"O_kgDOBulz1w","avatar_url":"https://avatars.githubusercontent.com/u/115962839?v=4","gravatar_id":"","url":"https://api.github.com/users/astral-sh","html_url":"https://github.com/astral-sh","followers_url":"https://api.github.com/users/astral-sh/followers","following_url":"https://api.github.com/users/astral-sh/following{/other_user}","gists_url":"https://api.github.com/users/astral-sh/gists{/gist_id}","starred_url":"https://api.github.com/users/astral-sh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/astral-sh/subscriptions","organizations_url":"https://api.github.com/users/astral-sh/orgs","repos_url":"https://api.github.com/users/astral-sh/repos","events_url":"https://api.github.com/users/astral-sh/events{/privacy}","received_events_url":"https://api.github.com/users/astral-sh/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":523043277,"node_id":"R_kgDOHy0BzQ","name":"ruff","full_name":"astral-sh/ruff","private":false,"owner":{"login":"astral-sh","id":115962839,"node_id":"O_kgDOBulz1w","avatar_url":"https://avatars.githubusercontent.com/u/115962839?v=4","gravatar_id":"","url":"https://api.github.com/users/astral-sh","html_url":"https://github.com/astral-sh","followers_url":"https://api.github.com/users/astral-sh/followers","following_url":"https://api.github.com/users/astral-sh/following{/other_user}","gists_url":"https://api.github.com/users/astral-sh/gists{/gist_id}","starred_url":"https://api.github.com/users/astral-sh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/astral-sh/subscriptions","organizations_url":"https://api.github.com/users/astral-sh/orgs","repos_url":"https://api.github.com/users/astral-sh/repos","events_url":"https://api.github.com/users/astral-sh/events{/privacy}","received_events_url":"https://api.github.com/users/astral-sh/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/astral-sh/ruff","description":"An extremely fast Python linter and code formatter, written in Rust.","fork":false,"url":"https://api.github.com/repos/astral-sh/ruff","forks_url":"https://api.github.com/repos/astral-sh/ruff/forks","keys_url":"https://api.github.com/repos/astral-sh/ruff/keys{/key_id}","collaborators_url":"https://api.github.com/repos/astral-sh/ruff/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/astral-sh/ruff/teams","hooks_url":"https://api.github.com/repos/astral-sh/ruff/hooks","issue_events_url":"https://api.github.com/repos/astral-sh/ruff/issues/events{/number}","events_url":"https://api.github.com/repos/astral-sh/ruff/events","assignees_url":"https://api.github.com/repos/astral-sh/ruff/assignees{/user}","branches_url":"https://api.github.com/repos/astral-sh/ruff/branches{/branch}","tags_url":"https://api.github.com/repos/astral-sh/ruff/tags","blobs_url":"https://api.github.com/repos/astral-sh/ruff/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/astral-sh/ruff/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/astral-sh/ruff/git/refs{/sha}","trees_url":"https://api.github.com/repos/astral-sh/ruff/git/trees{/sha}","statuses_url":"https://api.github.com/repos/astral-sh/ruff/statuses/{sha}","languages_url":"https://api.github.com/repos/astral-sh/ruff/languages","stargazers_url":"https://api.github.com/repos/astral-sh/ruff/stargazers","contributors_url":"https://api.github.com/repos/astral-sh/ruff/contributors","subscribers_url":"https://api.github.com/repos/astral-sh/ruff/subscribers","subscription_url":"https://api.github.com/repos/astral-sh/ruff/subscription","commits_url":"https://api.github.com/repos/astral-sh/ruff/commits{/sha}","git_commits_url":"https://api.github.com/repos/astral-sh/ruff/git/commits{/sha}","comments_url":"https://api.github.com/repos/astral-sh/ruff/comments{/number}","issue_comment_url":"https://api.github.com/repos/astral-sh/ruff/issues/comments{/number}","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/{+path}","compare_url":"https://api.github.com/repos/astral-sh/ruff/compare/{base}...{head}","merges_url":"https://api.github.com/repos/astral-sh/ruff/merges","archive_url":"https://api.github.com/repos/astral-sh/ruff/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/astral-sh/ruff/downloads","issues_url":"https://api.github.com/repos/astral-sh/ruff/issues{/number}","pulls_url":"https://api.github.com/repos/astral-sh/ruff/pulls{/number}","milestones_url":"https://api.github.com/repos/astral-sh/ruff/milestones{/number}","notifications_url":"https://api.github.com/repos/astral-sh/ruff/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/astral-sh/ruff/labels{/name}","releases_url":"https://api.github.com/repos/astral-sh/ruff/releases{/id}","deployments_url":"https://api.github.com/repos/astral-sh/ruff/deployments","created_at":"2022-08-09T17:17:44Z","updated_at":"2026-04-17T03:25:25Z","pushed_at":"2026-04-17T02:09:11Z","git_url":"git://github.com/astral-sh/ruff.git","ssh_url":"git@github.com:astral-sh/ruff.git","clone_url":"https://github.com/astral-sh/ruff.git","svn_url":"https://github.com/astral-sh/ruff","homepage":"https://docs.astral.sh/ruff","size":137909,"stargazers_count":47112,"watchers_count":47112,"language":"Rust","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":true,"forks_count":2010,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1949,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":["linter","pep8","python","python3","ruff","rust","rustpython","static-analysis","static-code-analysis","style-guide","styleguide"],"visibility":"public","forks":2010,"open_issues":1949,"watchers":47112,"default_branch":"main"}},"base":{"label":"astral-sh:main","ref":"main","sha":"38351e00ee969fdd2b5613978cdd26044ca2dba6","user":{"login":"astral-sh","id":115962839,"node_id":"O_kgDOBulz1w","avatar_url":"https://avatars.githubusercontent.com/u/115962839?v=4","gravatar_id":"","url":"https://api.github.com/users/astral-sh","html_url":"https://github.com/astral-sh","followers_url":"https://api.github.com/users/astral-sh/followers","following_url":"https://api.github.com/users/astral-sh/following{/other_user}","gists_url":"https://api.github.com/users/astral-sh/gists{/gist_id}","starred_url":"https://api.github.com/users/astral-sh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/astral-sh/subscriptions","organizations_url":"https://api.github.com/users/astral-sh/orgs","repos_url":"https://api.github.com/users/astral-sh/repos","events_url":"https://api.github.com/users/astral-sh/events{/privacy}","received_events_url":"https://api.github.com/users/astral-sh/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":523043277,"node_id":"R_kgDOHy0BzQ","name":"ruff","full_name":"astral-sh/ruff","private":false,"owner":{"login":"astral-sh","id":115962839,"node_id":"O_kgDOBulz1w","avatar_url":"https://avatars.githubusercontent.com/u/115962839?v=4","gravatar_id":"","url":"https://api.github.com/users/astral-sh","html_url":"https://github.com/astral-sh","followers_url":"https://api.github.com/users/astral-sh/followers","following_url":"https://api.github.com/users/astral-sh/following{/other_user}","gists_url":"https://api.github.com/users/astral-sh/gists{/gist_id}","starred_url":"https://api.github.com/users/astral-sh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/astral-sh/subscriptions","organizations_url":"https://api.github.com/users/astral-sh/orgs","repos_url":"https://api.github.com/users/astral-sh/repos","events_url":"https://api.github.com/users/astral-sh/events{/privacy}","received_events_url":"https://api.github.com/users/astral-sh/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/astral-sh/ruff","description":"An extremely fast Python linter and code formatter, written in Rust.","fork":false,"url":"https://api.github.com/repos/astral-sh/ruff","forks_url":"https://api.github.com/repos/astral-sh/ruff/forks","keys_url":"https://api.github.com/repos/astral-sh/ruff/keys{/key_id}","collaborators_url":"https://api.github.com/repos/astral-sh/ruff/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/astral-sh/ruff/teams","hooks_url":"https://api.github.com/repos/astral-sh/ruff/hooks","issue_events_url":"https://api.github.com/repos/astral-sh/ruff/issues/events{/number}","events_url":"https://api.github.com/repos/astral-sh/ruff/events","assignees_url":"https://api.github.com/repos/astral-sh/ruff/assignees{/user}","branches_url":"https://api.github.com/repos/astral-sh/ruff/branches{/branch}","tags_url":"https://api.github.com/repos/astral-sh/ruff/tags","blobs_url":"https://api.github.com/repos/astral-sh/ruff/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/astral-sh/ruff/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/astral-sh/ruff/git/refs{/sha}","trees_url":"https://api.github.com/repos/astral-sh/ruff/git/trees{/sha}","statuses_url":"https://api.github.com/repos/astral-sh/ruff/statuses/{sha}","languages_url":"https://api.github.com/repos/astral-sh/ruff/languages","stargazers_url":"https://api.github.com/repos/astral-sh/ruff/stargazers","contributors_url":"https://api.github.com/repos/astral-sh/ruff/contributors","subscribers_url":"https://api.github.com/repos/astral-sh/ruff/subscribers","subscription_url":"https://api.github.com/repos/astral-sh/ruff/subscription","commits_url":"https://api.github.com/repos/astral-sh/ruff/commits{/sha}","git_commits_url":"https://api.github.com/repos/astral-sh/ruff/git/commits{/sha}","comments_url":"https://api.github.com/repos/astral-sh/ruff/comments{/number}","issue_comment_url":"https://api.github.com/repos/astral-sh/ruff/issues/comments{/number}","contents_url":"https://api.github.com/repos/astral-sh/ruff/contents/{+path}","compare_url":"https://api.github.com/repos/astral-sh/ruff/compare/{base}...{head}","merges_url":"https://api.github.com/repos/astral-sh/ruff/merges","archive_url":"https://api.github.com/repos/astral-sh/ruff/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/astral-sh/ruff/downloads","issues_url":"https://api.github.com/repos/astral-sh/ruff/issues{/number}","pulls_url":"https://api.github.com/repos/astral-sh/ruff/pulls{/number}","milestones_url":"https://api.github.com/repos/astral-sh/ruff/milestones{/number}","notifications_url":"https://api.github.com/repos/astral-sh/ruff/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/astral-sh/ruff/labels{/name}","releases_url":"https://api.github.com/repos/astral-sh/ruff/releases{/id}","deployments_url":"https://api.github.com/repos/astral-sh/ruff/deployments","created_at":"2022-08-09T17:17:44Z","updated_at":"2026-04-17T03:25:25Z","pushed_at":"2026-04-17T02:09:11Z","git_url":"git://github.com/astral-sh/ruff.git","ssh_url":"git@github.com:astral-sh/ruff.git","clone_url":"https://github.com/astral-sh/ruff.git","svn_url":"https://github.com/astral-sh/ruff","homepage":"https://docs.astral.sh/ruff","size":137909,"stargazers_count":47112,"watchers_count":47112,"language":"Rust","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":true,"forks_count":2010,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1949,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":["linter","pep8","python","python3","ruff","rust","rustpython","static-analysis","static-code-analysis","style-guide","styleguide"],"visibility":"public","forks":2010,"open_issues":1949,"watchers":47112,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/astral-sh/ruff/pulls/16000"},"html":{"href":"https://github.com/astral-sh/ruff/pull/16000"},"issue":{"href":"https://api.github.com/repos/astral-sh/ruff/issues/16000"},"comments":{"href":"https://api.github.com/repos/astral-sh/ruff/issues/16000/comments"},"review_comments":{"href":"https://api.github.com/repos/astral-sh/ruff/pulls/16000/comments"},"review_comment":{"href":"https://api.github.com/repos/astral-sh/ruff/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/astral-sh/ruff/pulls/16000/commits"},"statuses":{"href":"https://api.github.com/repos/astral-sh/ruff/statuses/71e4b8938044fb18f88b6d07378d1d2069ed7792"}},"author_association":"MEMBER","auto_merge":null,"assignee":null,"active_lock_reason":null,"merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"MichaReiser","id":1203881,"node_id":"MDQ6VXNlcjEyMDM4ODE=","avatar_url":"https://avatars.githubusercontent.com/u/1203881?v=4","gravatar_id":"","url":"https://api.github.com/users/MichaReiser","html_url":"https://github.com/MichaReiser","followers_url":"https://api.github.com/users/MichaReiser/followers","following_url":"https://api.github.com/users/MichaReiser/following{/other_user}","gists_url":"https://api.github.com/users/MichaReiser/gists{/gist_id}","starred_url":"https://api.github.com/users/MichaReiser/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MichaReiser/subscriptions","organizations_url":"https://api.github.com/users/MichaReiser/orgs","repos_url":"https://api.github.com/users/MichaReiser/repos","events_url":"https://api.github.com/users/MichaReiser/events{/privacy}","received_events_url":"https://api.github.com/users/MichaReiser/received_events","type":"User","user_view_type":"public","site_admin":false},"comments":1,"review_comments":3,"maintainer_can_modify":false,"commits":3,"additions":214,"deletions":62,"changed_files":13} \ No newline at end of file diff --git a/crates/travelagent-forge-github/tests/live_smoke.rs b/crates/travelagent-forge-github/tests/live_smoke.rs new file mode 100644 index 0000000..f134caf --- /dev/null +++ b/crates/travelagent-forge-github/tests/live_smoke.rs @@ -0,0 +1,64 @@ +use travelagent_core::forge::*; +use travelagent_forge_github::GitHubForge; + +#[tokio::test] +#[ignore] // Requires GITHUB_TOKEN; run with: cargo test -- --ignored +async fn live_github_fetch_ripgrep_pr() { + let forge = match GitHubForge::new() { + Ok(f) => f, + Err(_) => { + eprintln!("Skipping: no GitHub token"); + return; + } + }; + let pr_id = PrId { + owner: "BurntSushi".into(), + repo: "ripgrep".into(), + number: 2900, + }; + + let meta = forge.get_pr(&pr_id).await.unwrap(); + assert_eq!(meta.author, "tmccombs"); + assert!(!meta.title.is_empty()); + + let files = forge.get_pr_files(&pr_id).await.unwrap(); + assert!(!files.is_empty()); + + let commits = forge.get_pr_commits(&pr_id).await.unwrap(); + assert!(!commits.is_empty()); + + let comments = forge.get_comments(&pr_id).await.unwrap(); + // May or may not have comments, just verify it doesn't error + let _ = comments; +} + +#[tokio::test] +#[ignore] +async fn live_github_fetch_large_pr() { + // rust-lang/rust#128440 -- 666 files + let forge = match GitHubForge::new() { + Ok(f) => f, + Err(_) => { + eprintln!("Skipping: no GitHub token"); + return; + } + }; + let pr_id = PrId { + owner: "rust-lang".into(), + repo: "rust".into(), + number: 128440, + }; + + let meta = forge.get_pr(&pr_id).await.unwrap(); + assert_eq!(meta.state, PrState::Closed); // This PR was closed + + let files = forge.get_pr_files(&pr_id).await.unwrap(); + assert!( + files.len() > 100, + "Expected 600+ files, got {}", + files.len() + ); + + let commits = forge.get_pr_commits(&pr_id).await.unwrap(); + assert!(commits.len() >= 4); +} diff --git a/crates/travelagent-forge-gitlab/tests/fixture_tests.rs b/crates/travelagent-forge-gitlab/tests/fixture_tests.rs new file mode 100644 index 0000000..49ab9ba --- /dev/null +++ b/crates/travelagent-forge-gitlab/tests/fixture_tests.rs @@ -0,0 +1,168 @@ +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +use travelagent_core::forge::*; +use travelagent_core::model::FileStatus; +use travelagent_forge_gitlab::GitLabForge; + +fn load_fixture(name: &str) -> String { + std::fs::read_to_string(format!("tests/fixtures/{name}")).unwrap() +} + +fn gitlab_mr_id() -> PrId { + PrId { + owner: "gitlab-org".into(), + repo: "gitlab".into(), + number: 229380, + } +} + +async fn setup() -> (MockServer, GitLabForge) { + let server = MockServer::start().await; + let forge = GitLabForge::with_token(&server.uri(), "test-token".into()).unwrap(); + (server, forge) +} + +#[tokio::test] +async fn gitlab_mr_metadata_parsed_correctly() { + let (server, forge) = setup().await; + let body: serde_json::Value = + serde_json::from_str(&load_fixture("gitlab_229380_mr.json")).unwrap(); + + Mock::given(method("GET")) + .and(path( + "/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/229380", + )) + .respond_with(ResponseTemplate::new(200).set_body_json(&body)) + .mount(&server) + .await; + + let pr = forge.get_pr(&gitlab_mr_id()).await.unwrap(); + assert_eq!( + pr.title, + "Add Gitaly client wrapper for pool repository discovery" + ); + assert_eq!(pr.author, "gloria_odipo"); + assert_eq!(pr.state, PrState::Merged); + assert_eq!(pr.base_branch, "master"); + assert_eq!(pr.head_branch, "pool-repos-2-gitaly-client"); + assert!(!pr.is_draft); +} + +#[tokio::test] +async fn gitlab_commits_parsed_correctly() { + let (server, forge) = setup().await; + let body: serde_json::Value = + serde_json::from_str(&load_fixture("gitlab_229380_commits.json")).unwrap(); + + Mock::given(method("GET")) + .and(path( + "/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/229380/commits", + )) + .respond_with(ResponseTemplate::new(200).set_body_json(&body)) + .mount(&server) + .await; + + let commits = forge.get_pr_commits(&gitlab_mr_id()).await.unwrap(); + assert_eq!(commits.len(), 13); + + // First commit (most recent) + assert_eq!(commits[0].id, "4b231e4f10405511f0ff35433d0f811831fda677"); + assert_eq!(commits[0].short_id, "4b231e4f"); + assert_eq!( + commits[0].summary, + "Remove redundant fetch_pools_for_repos_batch and rename service" + ); + assert_eq!(commits[0].author, "gloria_odipo"); + + // Last commit + assert_eq!( + commits[12].summary, + "Add core utility classes for pool repository orphan discovery" + ); +} + +#[tokio::test] +async fn gitlab_changes_parsed_into_diff_files() { + let (server, forge) = setup().await; + let body: serde_json::Value = + serde_json::from_str(&load_fixture("gitlab_229380_changes.json")).unwrap(); + + Mock::given(method("GET")) + .and(path( + "/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/229380/changes", + )) + .respond_with(ResponseTemplate::new(200).set_body_json(&body)) + .mount(&server) + .await; + + let files = forge.get_pr_files(&gitlab_mr_id()).await.unwrap(); + assert_eq!(files.len(), 8); + + // All 8 files are new + let new_count = files + .iter() + .filter(|f| f.status == FileStatus::Added) + .count(); + assert_eq!(new_count, 8); + + // Verify new file flags: old_path is None for added files, new_path is set + for file in &files { + assert!( + file.old_path.is_none(), + "Added file should have no old_path" + ); + assert!(file.new_path.is_some(), "Added file should have a new_path"); + } + + // Verify hunks are parsed for files with diffs + let csv_writer = files + .iter() + .find(|f| { + f.new_path + .as_ref() + .map_or(false, |p| p.to_str().unwrap().contains("csv_writer.rb")) + }) + .unwrap(); + assert!( + !csv_writer.hunks.is_empty(), + "csv_writer.rb should have parsed hunks" + ); +} + +#[tokio::test] +async fn gitlab_discussions_parsed_into_comments_and_threads() { + let (server, forge) = setup().await; + let body: serde_json::Value = + serde_json::from_str(&load_fixture("gitlab_229380_discussions.json")).unwrap(); + + Mock::given(method("GET")) + .and(path( + "/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/229380/discussions", + )) + .respond_with(ResponseTemplate::new(200).set_body_json(&body)) + .mount(&server) + .await; + + let comments = forge.get_comments(&gitlab_mr_id()).await.unwrap(); + // 2 discussions: first has 2 notes, second has 1 note = 3 total + assert_eq!(comments.len(), 3); + + // First note in first discussion (root) + assert_eq!(comments[0].id, 100001); + assert_eq!(comments[0].author, "reviewer1"); + assert_eq!(comments[0].path, Some("app/models/gitaly_client.rb".into())); + assert_eq!(comments[0].line, Some(15)); + assert_eq!(comments[0].in_reply_to, None); + + // Second note in first discussion (reply to root) + assert_eq!(comments[1].id, 100002); + assert_eq!(comments[1].author, "author1"); + assert_eq!(comments[1].in_reply_to, Some(100001)); + + // Single note in second discussion (non-resolvable, general note) + assert_eq!(comments[2].id, 100003); + assert_eq!(comments[2].author, "reviewer2"); + assert_eq!(comments[2].in_reply_to, None); + assert_eq!(comments[2].path, None); +} diff --git a/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_changes.json b/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_changes.json new file mode 100644 index 0000000..ce71ae2 --- /dev/null +++ b/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_changes.json @@ -0,0 +1 @@ +{"id":468243450,"iid":229380,"project_id":278964,"title":"Add Gitaly client wrapper for pool repository discovery","description":"## What does this MR do and why?\n\n- Adds Gitaly client wrapper for pool repository discovery\n\n- Adds GitalyClient class that wraps Gitaly RPC calls for:\n\n - Fetching object pool information for repositories\n - Listing all repositories on a storage\n - Caching pool disk paths for efficient lookups\n\n- This client provides the Gitaly integration layer needed to compare Rails database records with actual Gitaly storage state.\n\n## References\n\nhttps://gitlab.com/gitlab-org/gitlab/-/work_items/573591+\n\n\u003c!--\nInclude [links](https://handbook.gitlab.com/handbook/communication/#start-with-a-merge-request:~:text=Cross%20link%20issues,alternate%20if%20duplicate.) to any resources that are relevant to this MR.\nThis will give reviewers and future readers helpful context.\n--\u003e\n\n## Screenshots or screen recordings\n\n\u003c!---\nScreenshots are required for UI changes, and strongly recommended for all other merge requests.\n--\u003e\n\n| Before | After |\n| ------ | ------ |\n| | |\n\n\u003c!--\nOPTIONAL: For responsive UI changes, you can use the viewport size table below.\nDelete this table if not needed or delete rows that are not relevant to your changes.\n\n| Viewport size | Before | After |\n| ----------------| ---------- | ---------- |\n| `xs` (\u003c576px) | | |\n| `sm` (\u003e=576px) | | |\n| `md` (\u003e=768px) | | |\n| `lg` (\u003e=992px) | | |\n| `xl` (\u003e=1200px) | | |\n--\u003e\n\n## How to set up and validate locally\n\n\u003c!--\nNumbered steps to set up and validate the change are strongly suggested.\n\nExample:\n\n1. In rails console enable the feature flag\n ```ruby\n Feature.enable(:member_areas_of_focus)\n ```\n1. Visit any group or project member pages such as `http://127.0.0.1:3000/groups/flightjs/-/group_members`\n1. Click the `invite members` button.\n--\u003e\n\n## MR acceptance checklist\n\nEvaluate this MR against the [MR acceptance checklist](https://docs.gitlab.com/development/code_review/#acceptance-checklist).\nIt helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.","state":"merged","created_at":"2026-03-29T10:39:17.528Z","updated_at":"2026-04-17T06:27:17.197Z","merged_by":{"id":421631,"username":"vyaklushin","public_email":"viakliushin@gitlab.com","name":"Vasilii Iakliushin","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/421631/avatar.png","web_url":"https://gitlab.com/vyaklushin"},"merge_user":{"id":421631,"username":"vyaklushin","public_email":"viakliushin@gitlab.com","name":"Vasilii Iakliushin","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/421631/avatar.png","web_url":"https://gitlab.com/vyaklushin"},"merged_at":"2026-04-16T18:47:39.368Z","closed_by":null,"closed_at":null,"target_branch":"master","source_branch":"pool-repos-2-gitaly-client","user_notes_count":22,"upvotes":0,"downvotes":0,"author":{"id":29040103,"username":"gloria_odipo","public_email":"","name":"Gloria Odipo","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/29040103/avatar.png","web_url":"https://gitlab.com/gloria_odipo"},"assignees":[{"id":29040103,"username":"gloria_odipo","public_email":"","name":"Gloria Odipo","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/29040103/avatar.png","web_url":"https://gitlab.com/gloria_odipo"}],"assignee":{"id":29040103,"username":"gloria_odipo","public_email":"","name":"Gloria Odipo","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/29040103/avatar.png","web_url":"https://gitlab.com/gloria_odipo"},"reviewers":[{"id":421631,"username":"vyaklushin","public_email":"viakliushin@gitlab.com","name":"Vasilii Iakliushin","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/421631/avatar.png","web_url":"https://gitlab.com/vyaklushin"},{"id":21751328,"username":"freinink","public_email":"freinink@gitlab.com","name":"Fred Reinink","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/21751328/avatar.png","web_url":"https://gitlab.com/freinink"},{"id":21826781,"username":"GitLabDuo","public_email":null,"name":"GitLab Duo","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/21826781/duo-bot.png","web_url":"https://gitlab.com/GitLabDuo"}],"source_project_id":278964,"target_project_id":278964,"labels":["Category:Source Code Management","Deliverable","SLO::Missed","backend","bug::functional","devops::create","group::source code","missed-deliverable","missed:18.11","pipeline::tier-3","pipeline:as-if-foss-run-once","pipeline:mr-approved","rspec:slow test detected","section::dev","severity::3","type::bug","workflow::post-deploy-db-staging"],"draft":false,"imported":false,"imported_from":"none","work_in_progress":false,"milestone":{"id":5981229,"iid":131,"group_id":9970,"title":"19.0","description":"| Milestone | Quarter | Start Date - Code Merging | End Date - Code Merging | Release Date |\r\n|-----------|------------|---------------------------|-------------------------|---------------|\r\n| 19.0 | FY27-Q2-M1 | 2026-05-07 | 2026-05-08 | 2026-05-14 |","state":"active","created_at":"2025-04-21T21:49:02.622Z","updated_at":"2025-06-30T16:29:51.458Z","due_date":"2026-05-15","start_date":"2026-04-11","expired":false,"web_url":"https://gitlab.com/groups/gitlab-org/-/milestones/131"},"merge_when_pipeline_succeeds":true,"merge_status":"can_be_merged","detailed_merge_status":"not_open","merge_after":null,"sha":"4b231e4f10405511f0ff35433d0f811831fda677","merge_commit_sha":"7a0acccd2ddde48660e01633e59ac7a88426663e","squash_commit_sha":"08e32d31c1ac1cfb25d3657f0b9e9bcbf2fc5bef","discussion_locked":null,"should_remove_source_branch":true,"force_remove_source_branch":true,"prepared_at":"2026-03-29T10:39:50.768Z","reference":"!229380","references":{"short":"!229380","relative":"!229380","full":"gitlab-org/gitlab!229380"},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/merge_requests/229380","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":true,"squash_on_merge":true,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null,"subscribed":false,"changes_count":"8","latest_build_started_at":"2026-04-16T18:47:20.992Z","latest_build_finished_at":"2026-04-16T18:47:36.946Z","first_deployed_to_production_at":"2026-04-16T20:53:55.384Z","pipeline":{"id":2458683981,"iid":5518244,"project_id":278964,"sha":"b7c57c7bf0439fbf26d8bce9d05be080f46046a5","ref":"refs/merge-requests/229380/train","status":"success","source":"merge_request_event","created_at":"2026-04-16T18:47:19.141Z","updated_at":"2026-04-16T18:47:37.013Z","web_url":"https://gitlab.com/gitlab-org/gitlab/-/pipelines/2458683981"},"head_pipeline":{"id":2458683981,"iid":5518244,"project_id":278964,"sha":"b7c57c7bf0439fbf26d8bce9d05be080f46046a5","ref":"refs/merge-requests/229380/train","status":"success","source":"merge_request_event","created_at":"2026-04-16T18:47:19.141Z","updated_at":"2026-04-16T18:47:37.013Z","web_url":"https://gitlab.com/gitlab-org/gitlab/-/pipelines/2458683981","before_sha":"b7c57c7bf0439fbf26d8bce9d05be080f46046a5","tag":false,"yaml_errors":null,"user":{"id":421631,"username":"vyaklushin","public_email":"viakliushin@gitlab.com","name":"Vasilii Iakliushin","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/421631/avatar.png","web_url":"https://gitlab.com/vyaklushin"},"started_at":"2026-04-16T18:47:20.992Z","finished_at":"2026-04-16T18:47:36.946Z","committed_at":null,"duration":15,"queued_duration":1,"coverage":null,"detailed_status":{"icon":"status_success","text":"Passed","label":"passed","group":"success","tooltip":"passed","has_details":true,"details_path":"/gitlab-org/gitlab/-/pipelines/2458683981","illustration":null,"favicon":"/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"},"archived":false},"diff_refs":{"base_sha":"21984d789212dabc1c72a3f388f2b94c95f5329f","head_sha":"4b231e4f10405511f0ff35433d0f811831fda677","start_sha":"d365f8c375fcd25efa9704c2df08c0466d3ccb0f"},"merge_error":null,"user":{"can_merge":false},"changes":[{"diff":"@@ -0,0 +1,37 @@\n+# frozen_string_literal: true\n+\n+require 'csv'\n+\n+module Gitlab\n+ module PoolRepositories\n+ class CsvWriter\n+ COLUMNS = [\n+ { key: :pool_id, header: 'Pool ID' },\n+ { key: :disk_path, header: 'Disk Path' },\n+ { key: :relative_path, header: 'Relative Path (Gitaly)' },\n+ { key: :source_project_id, header: 'Source Project ID' },\n+ { key: :state, header: 'State' },\n+ { key: :reason_codes, header: 'Reason Codes' },\n+ { key: :reasons, header: 'Reasons' },\n+ { key: :member_projects_count, header: 'Member Projects Count' },\n+ { key: :shard_name, header: 'Shard Name' }\n+ ].freeze\n+\n+ CSV_HEADERS = COLUMNS.pluck(:header).freeze # rubocop:disable CodeReuse/ActiveRecord -- COLUMNS is a plain Ruby array, not ActiveRecord\n+\n+ def initialize(output_file)\n+ @csv = CSV.open(output_file, 'w')\n+ @csv \u003c\u003c CSV_HEADERS\n+ end\n+\n+ def write_row(record)\n+ @csv \u003c\u003c COLUMNS.map { |c| record[c[:key]] }\n+ end\n+\n+ def close\n+ @csv\u0026.close\n+ @csv = nil\n+ end\n+ end\n+ end\n+end\n","collapsed":false,"too_large":false,"new_path":"lib/gitlab/pool_repositories/csv_writer.rb","old_path":"lib/gitlab/pool_repositories/csv_writer.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"generated_file":false},{"diff":"@@ -0,0 +1,41 @@\n+# frozen_string_literal: true\n+\n+module Gitlab\n+ module PoolRepositories\n+ class DiscoveryService\n+ def initialize(logger, verbose)\n+ @logger = logger\n+ @verbose = verbose\n+ # rubocop:disable CodeReuse/ActiveRecord -- Pre-loading pool disk paths for discovery\n+ @pool_disk_paths_cache = Set.new(PoolRepository.pluck(:disk_path))\n+ # rubocop:enable CodeReuse/ActiveRecord\n+ end\n+\n+ def pool_disk_path_exists?(disk_path)\n+ @pool_disk_paths_cache.include?(disk_path)\n+ end\n+\n+ def scan_pool_metadata(storage_name)\n+ request = Gitaly::ScanPoolMetadataRequest.new(storage_name: storage_name)\n+\n+ response = Gitlab::GitalyClient.call(\n+ storage_name,\n+ :object_pool_service,\n+ :scan_pool_metadata,\n+ request,\n+ timeout: Gitlab::GitalyClient.long_timeout\n+ )\n+\n+ response.map do |entry|\n+ {\n+ relative_path: entry.relative_path,\n+ pool_disk_path: entry.pool_disk_path.presence\n+ }\n+ end\n+ rescue StandardError =\u003e e\n+ @logger.debug \"Failed to scan pool metadata on storage #{storage_name}: #{e.message}\" if @verbose\n+ []\n+ end\n+ end\n+ end\n+end\n","collapsed":false,"too_large":false,"new_path":"lib/gitlab/pool_repositories/discovery_service.rb","old_path":"lib/gitlab/pool_repositories/discovery_service.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"generated_file":false},{"diff":"@@ -0,0 +1,56 @@\n+# frozen_string_literal: true\n+\n+module Gitlab\n+ module PoolRepositories\n+ class OrphanRecord\n+ ORPHAN_REASONS = {\n+ pool_in_db_no_projects: 'Pool exists in Rails DB but no projects reference it',\n+ pool_no_source_project: 'Pool exists in Rails DB with no source_project_id set',\n+ pool_on_gitaly_missing_db: 'Pool exists on Gitaly but missing from Rails DB',\n+ disk_path_mismatch: 'Disk path mismatch between Rails DB and Gitaly',\n+ pool_in_obsolete_state: 'Pool marked as obsolete in Rails DB'\n+ }.freeze\n+\n+ def self.from_pool(pool_repository, reasons, gitaly_relative_path = nil)\n+ reasons_array = Array(reasons)\n+ validate_reasons(reasons_array)\n+ reason_codes = reasons_array.map(\u0026:to_s).join('|')\n+ reason_texts = reasons_array.map { |r| ORPHAN_REASONS[r] || \"Unknown reason: #{r}\" }.join('; ')\n+\n+ {\n+ pool_id: pool_repository.id,\n+ disk_path: pool_repository.disk_path,\n+ relative_path: gitaly_relative_path || 'N/A',\n+ source_project_id: pool_repository.source_project_id,\n+ state: pool_repository.state,\n+ reason_codes: reason_codes,\n+ reasons: reason_texts,\n+ member_projects_count: pool_repository.member_projects.count,\n+ shard_name: pool_repository.shard_name\n+ }\n+ end\n+\n+ def self.from_gitaly(pool_disk_path, storage_name)\n+ {\n+ pool_id: 'N/A',\n+ disk_path: pool_disk_path,\n+ relative_path: \"#{pool_disk_path}.git\",\n+ source_project_id: nil,\n+ state: 'unknown',\n+ reason_codes: 'pool_on_gitaly_missing_db',\n+ reasons: ORPHAN_REASONS[:pool_on_gitaly_missing_db],\n+ member_projects_count: 0,\n+ shard_name: storage_name\n+ }\n+ end\n+\n+ def self.validate_reasons(reasons_array)\n+ invalid_reasons = reasons_array.reject { |r| ORPHAN_REASONS.key?(r) }\n+ return if invalid_reasons.empty?\n+\n+ Gitlab::AppLogger.warn(\"Unknown orphan reason(s): #{invalid_reasons.join(', ')}\")\n+ end\n+ private_class_method :validate_reasons\n+ end\n+ end\n+end\n","collapsed":false,"too_large":false,"new_path":"lib/gitlab/pool_repositories/orphan_record.rb","old_path":"lib/gitlab/pool_repositories/orphan_record.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"generated_file":false},{"diff":"@@ -0,0 +1,19 @@\n+# frozen_string_literal: true\n+\n+require 'logger'\n+\n+module Gitlab\n+ module PoolRepositories\n+ module RakeTask\n+ def self.logger\n+ if Rails.env.development? || Rails.env.production?\n+ stdout_logger = Logger.new($stdout)\n+ stdout_logger.level = Logger::INFO\n+ ActiveSupport::BroadcastLogger.new(stdout_logger, Rails.logger)\n+ else\n+ Rails.logger\n+ end\n+ end\n+ end\n+ end\n+end\n","collapsed":false,"too_large":false,"new_path":"lib/gitlab/pool_repositories/rake_task.rb","old_path":"lib/gitlab/pool_repositories/rake_task.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"generated_file":false},{"diff":"@@ -0,0 +1,85 @@\n+# frozen_string_literal: true\n+\n+require 'fast_spec_helper'\n+\n+RSpec.describe Gitlab::PoolRepositories::CsvWriter, feature_category: :source_code_management do\n+ let(:temp_file) { Tempfile.new('orphaned_pools.csv') }\n+ let(:csv_writer) { described_class.new(temp_file.path) }\n+ let(:record) do\n+ {\n+ pool_id: 1,\n+ disk_path: '@pools/test',\n+ relative_path: '@pools/test.git',\n+ source_project_id: 123,\n+ state: 'ready',\n+ reason_codes: 'test_reason',\n+ reasons: 'Test reason description',\n+ member_projects_count: 5,\n+ shard_name: 'default'\n+ }\n+ end\n+\n+ after do\n+ csv_writer.close\n+ temp_file.close!\n+ end\n+\n+ describe '#write_row' do\n+ context 'when writing a single row' do\n+ it 'writes a properly formatted CSV row with all fields' do\n+ csv_writer.write_row(record)\n+ csv_writer.close\n+\n+ rows = CSV.read(temp_file.path)\n+ expect(rows.size).to eq(2)\n+ expect(rows[1]).to eq(\n+ ['1', '@pools/test', '@pools/test.git', '123', 'ready', 'test_reason', 'Test reason description', '5',\n+ 'default']\n+ )\n+ end\n+ end\n+\n+ context 'when writing multiple rows' do\n+ let(:second_record) do\n+ {\n+ pool_id: 2,\n+ disk_path: '@pools/other',\n+ relative_path: '@pools/other.git',\n+ source_project_id: 456,\n+ state: 'obsolete',\n+ reason_codes: 'pool_in_obsolete_state',\n+ reasons: 'Pool marked as obsolete',\n+ member_projects_count: 0,\n+ shard_name: 'default'\n+ }\n+ end\n+\n+ it 'writes all rows in order' do\n+ csv_writer.write_row(record)\n+ csv_writer.write_row(second_record)\n+ csv_writer.close\n+\n+ rows = CSV.read(temp_file.path)\n+ expect(rows.size).to eq(3)\n+ expect(rows[1]).to include('@pools/test')\n+ expect(rows[2]).to include('@pools/other')\n+ end\n+ end\n+ end\n+\n+ describe '#close' do\n+ it 'does not raise when called' do\n+ expect { csv_writer.close }.not_to raise_error\n+ end\n+ end\n+\n+ describe 'initialization' do\n+ it 'writes CSV headers as the first line' do\n+ csv_writer.close\n+\n+ rows = CSV.read(temp_file.path)\n+ expect(rows.size).to eq(1)\n+ expect(rows[0]).to eq(described_class::CSV_HEADERS)\n+ end\n+ end\n+end\n","collapsed":false,"too_large":false,"new_path":"spec/lib/gitlab/pool_repositories/csv_writer_spec.rb","old_path":"spec/lib/gitlab/pool_repositories/csv_writer_spec.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"generated_file":false},{"diff":"@@ -0,0 +1,117 @@\n+# frozen_string_literal: true\n+\n+require 'spec_helper'\n+\n+RSpec.describe Gitlab::PoolRepositories::DiscoveryService, feature_category: :source_code_management do\n+ let(:logger) { instance_double(Logger) }\n+ let(:verbose) { false }\n+ let(:discovery_service) { described_class.new(logger, verbose) }\n+ let(:storage_name) { 'default' }\n+\n+ before do\n+ allow(logger).to receive(:info)\n+ allow(logger).to receive(:error)\n+ allow(logger).to receive(:debug)\n+ end\n+\n+ shared_examples 'error handling with verbose toggle' do |expected_default, log_pattern|\n+ context 'when verbose' do\n+ let(:verbose) { true }\n+\n+ it 'returns the safe default and logs the error' do\n+ expect(subject).to eq(expected_default)\n+ expect(logger).to have_received(:debug).with(log_pattern)\n+ end\n+ end\n+\n+ context 'when not verbose' do\n+ it 'returns the safe default without logging' do\n+ expect(subject).to eq(expected_default)\n+ expect(logger).not_to have_received(:debug)\n+ end\n+ end\n+ end\n+\n+ def build_scan_pool_metadata_response(relative_path:, pool_disk_path: nil)\n+ Gitaly::ScanPoolMetadataResponse.new(relative_path: relative_path, pool_disk_path: pool_disk_path)\n+ end\n+\n+ describe '#initialize' do\n+ it 'loads all pool disk paths into cache' do\n+ create(:pool_repository, disk_path: '@pools/path1')\n+ create(:pool_repository, disk_path: '@pools/path2')\n+\n+ client = described_class.new(logger, verbose)\n+\n+ expect(client.pool_disk_path_exists?('@pools/path1')).to be true\n+ expect(client.pool_disk_path_exists?('@pools/path2')).to be true\n+ end\n+ end\n+\n+ describe '#pool_disk_path_exists?' do\n+ subject { discovery_service.pool_disk_path_exists?(path) }\n+\n+ context 'when the path exists in cache' do\n+ before do\n+ create(:pool_repository, disk_path: '@pools/test')\n+ end\n+\n+ let(:discovery_service) { described_class.new(logger, verbose) }\n+ let(:path) { '@pools/test' }\n+\n+ it { is_expected.to be true }\n+ end\n+\n+ context 'when the path does not exist in cache' do\n+ let(:path) { '@pools/other' }\n+\n+ it { is_expected.to be false }\n+ end\n+ end\n+\n+ describe '#scan_pool_metadata' do\n+ subject(:scan_result) { discovery_service.scan_pool_metadata(storage_name) }\n+\n+ context 'when repositories with pools are returned' do\n+ before do\n+ messages = [\n+ build_scan_pool_metadata_response(relative_path: 'repo1.git', pool_disk_path: '@pools/pool1.git'),\n+ build_scan_pool_metadata_response(relative_path: 'repo2.git', pool_disk_path: '@pools/pool2.git')\n+ ]\n+ allow(Gitlab::GitalyClient).to receive(:call).and_return(messages)\n+ end\n+\n+ it 'returns relative paths with their pool disk paths' do\n+ expect(scan_result).to match_array([\n+ { relative_path: 'repo1.git', pool_disk_path: '@pools/pool1.git' },\n+ { relative_path: 'repo2.git', pool_disk_path: '@pools/pool2.git' }\n+ ])\n+ end\n+ end\n+\n+ context 'when some repositories have no pools' do\n+ before do\n+ messages = [\n+ build_scan_pool_metadata_response(relative_path: 'repo1.git', pool_disk_path: '@pools/pool1.git'),\n+ build_scan_pool_metadata_response(relative_path: 'repo2.git', pool_disk_path: '')\n+ ]\n+ allow(Gitlab::GitalyClient).to receive(:call).and_return(messages)\n+ end\n+\n+ it 'returns nil for pool_disk_path when empty' do\n+ expect(scan_result).to match_array([\n+ { relative_path: 'repo1.git', pool_disk_path: '@pools/pool1.git' },\n+ { relative_path: 'repo2.git', pool_disk_path: nil }\n+ ])\n+ end\n+ end\n+\n+ context 'when the Gitaly call raises an error' do\n+ before do\n+ allow(Gitlab::GitalyClient).to receive(:call).and_raise(StandardError, 'Scan failed')\n+ end\n+\n+ include_examples 'error handling with verbose toggle', [], /Failed to scan pool metadata/\n+ end\n+ end\n+end\n","collapsed":false,"too_large":false,"new_path":"spec/lib/gitlab/pool_repositories/discovery_service_spec.rb","old_path":"spec/lib/gitlab/pool_repositories/discovery_service_spec.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"generated_file":false},{"diff":"@@ -0,0 +1,109 @@\n+# frozen_string_literal: true\n+\n+require 'spec_helper'\n+\n+RSpec.describe Gitlab::PoolRepositories::OrphanRecord, feature_category: :source_code_management do\n+ let_it_be(:project) { create(:project) }\n+ let_it_be(:pool_repository) { create(:pool_repository, source_project: project) }\n+\n+ describe '.from_pool' do\n+ let(:reasons) { :pool_no_source_project }\n+ let(:gitaly_relative_path) { nil }\n+\n+ subject { described_class.from_pool(pool_repository, reasons, gitaly_relative_path) }\n+\n+ context 'with a single reason' do\n+ it 'returns the expected hash with default relative_path' do\n+ is_expected.to eq(\n+ pool_id: pool_repository.id,\n+ disk_path: pool_repository.disk_path,\n+ relative_path: 'N/A',\n+ source_project_id: project.id,\n+ state: 'none',\n+ reason_codes: 'pool_no_source_project',\n+ reasons: 'Pool exists in Rails DB with no source_project_id set',\n+ member_projects_count: 1,\n+ shard_name: 'default'\n+ )\n+ end\n+ end\n+\n+ context 'with multiple reasons' do\n+ let(:reasons) { [:pool_no_source_project, :pool_in_obsolete_state] }\n+\n+ it 'joins reason codes and texts' do\n+ is_expected.to eq(\n+ pool_id: pool_repository.id,\n+ disk_path: pool_repository.disk_path,\n+ relative_path: 'N/A',\n+ source_project_id: project.id,\n+ state: 'none',\n+ reason_codes: 'pool_no_source_project|pool_in_obsolete_state',\n+ reasons: 'Pool exists in Rails DB with no source_project_id set; Pool marked as obsolete in Rails DB',\n+ member_projects_count: 1,\n+ shard_name: 'default'\n+ )\n+ end\n+ end\n+\n+ context 'when gitaly_relative_path is provided' do\n+ let(:gitaly_relative_path) { '@pools/test.git' }\n+\n+ it 'uses the provided relative_path' do\n+ is_expected.to eq(\n+ pool_id: pool_repository.id,\n+ disk_path: pool_repository.disk_path,\n+ relative_path: '@pools/test.git',\n+ source_project_id: project.id,\n+ state: 'none',\n+ reason_codes: 'pool_no_source_project',\n+ reasons: 'Pool exists in Rails DB with no source_project_id set',\n+ member_projects_count: 1,\n+ shard_name: 'default'\n+ )\n+ end\n+ end\n+\n+ context 'with unknown reason' do\n+ it 'logs a warning and includes unknown reason in output' do\n+ expect(Gitlab::AppLogger).to receive(:warn).with(/Unknown orphan reason\\(s\\): unknown_reason/)\n+\n+ result = described_class.from_pool(pool_repository, :unknown_reason)\n+\n+ expect(result[:reason_codes]).to eq('unknown_reason')\n+ expect(result[:reasons]).to eq('Unknown reason: unknown_reason')\n+ end\n+ end\n+\n+ context 'with mixed valid and invalid reasons' do\n+ it 'logs a warning listing only invalid reasons' do\n+ expect(Gitlab::AppLogger).to receive(:warn).with(/Unknown orphan reason\\(s\\): invalid_reason/)\n+\n+ result = described_class.from_pool(pool_repository, [:pool_no_source_project, :invalid_reason])\n+\n+ expect(result[:reason_codes]).to eq('pool_no_source_project|invalid_reason')\n+ end\n+ end\n+ end\n+\n+ describe '.from_gitaly' do\n+ let(:pool_disk_path) { '@pools/4e/07/4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce' }\n+ let(:storage_name) { 'default' }\n+\n+ subject { described_class.from_gitaly(pool_disk_path, storage_name) }\n+\n+ it 'returns the expected hash for a Gitaly-only pool' do\n+ is_expected.to eq(\n+ pool_id: 'N/A',\n+ disk_path: pool_disk_path,\n+ relative_path: \"#{pool_disk_path}.git\",\n+ source_project_id: nil,\n+ state: 'unknown',\n+ reason_codes: 'pool_on_gitaly_missing_db',\n+ reasons: 'Pool exists on Gitaly but missing from Rails DB',\n+ member_projects_count: 0,\n+ shard_name: 'default'\n+ )\n+ end\n+ end\n+end\n","collapsed":false,"too_large":false,"new_path":"spec/lib/gitlab/pool_repositories/orphan_record_spec.rb","old_path":"spec/lib/gitlab/pool_repositories/orphan_record_spec.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"generated_file":false},{"diff":"@@ -0,0 +1,43 @@\n+# frozen_string_literal: true\n+\n+require 'spec_helper'\n+\n+RSpec.describe Gitlab::PoolRepositories::RakeTask, feature_category: :source_code_management do\n+ describe '.logger' do\n+ context 'when in development environment' do\n+ before do\n+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))\n+ end\n+\n+ it 'returns a BroadcastLogger combining stdout and Rails logger' do\n+ logger = described_class.logger\n+\n+ expect(logger).to be_a(ActiveSupport::BroadcastLogger)\n+ end\n+ end\n+\n+ context 'when in production environment' do\n+ before do\n+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))\n+ end\n+\n+ it 'returns a BroadcastLogger combining stdout and Rails logger' do\n+ logger = described_class.logger\n+\n+ expect(logger).to be_a(ActiveSupport::BroadcastLogger)\n+ end\n+ end\n+\n+ context 'when in other environments' do\n+ before do\n+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('staging'))\n+ end\n+\n+ it 'returns Rails logger' do\n+ logger = described_class.logger\n+\n+ expect(logger).to be(Rails.logger)\n+ end\n+ end\n+ end\n+end\n","collapsed":false,"too_large":false,"new_path":"spec/lib/gitlab/pool_repositories/rake_task_spec.rb","old_path":"spec/lib/gitlab/pool_repositories/rake_task_spec.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"generated_file":false}],"overflow":false} \ No newline at end of file diff --git a/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_commits.json b/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_commits.json new file mode 100644 index 0000000..4123639 --- /dev/null +++ b/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_commits.json @@ -0,0 +1 @@ +[{"id":"4b231e4f10405511f0ff35433d0f811831fda677","short_id":"4b231e4f","created_at":"2026-04-14T09:10:05.000+03:00","parent_ids":["06521262b15486944ccb238e3227a789599fc0a4"],"title":"Remove redundant fetch_pools_for_repos_batch and rename service","message":"Remove redundant fetch_pools_for_repos_batch and rename service\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-04-13T20:23:36.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:10:05.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/4b231e4f10405511f0ff35433d0f811831fda677"},{"id":"06521262b15486944ccb238e3227a789599fc0a4","short_id":"06521262","created_at":"2026-04-14T09:10:02.000+03:00","parent_ids":["5f415c9ca7a60773f5b311c843d960a301a59b57"],"title":"Apply code review suggestions for Gitaly client","message":"Apply code review suggestions for Gitaly client\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-04-13T10:03:15.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:10:02.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/06521262b15486944ccb238e3227a789599fc0a4"},{"id":"5f415c9ca7a60773f5b311c843d960a301a59b57","short_id":"5f415c9c","created_at":"2026-04-14T09:09:59.000+03:00","parent_ids":["f27719550b9dbddc4f18f88f5f4dba2b13af4075"],"title":"Add tests for fetch_pool_for_repo","message":"Add tests for fetch_pool_for_repo\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-04-06T23:09:17.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:09:59.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/5f415c9ca7a60773f5b311c843d960a301a59b57"},{"id":"f27719550b9dbddc4f18f88f5f4dba2b13af4075","short_id":"f2771955","created_at":"2026-04-14T09:09:57.000+03:00","parent_ids":["665111f890a1137be81e121b096ce276491f58bc"],"title":"Implement several refactors","message":"Implement several refactors\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-04-06T16:12:46.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:09:57.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/f27719550b9dbddc4f18f88f5f4dba2b13af4075"},{"id":"665111f890a1137be81e121b096ce276491f58bc","short_id":"665111f8","created_at":"2026-04-14T09:09:54.000+03:00","parent_ids":["d698dbaf8550945a4512acfa35253f6576475617"],"title":"Add Gitaly client wrapper for pool repository discovery","message":"Add Gitaly client wrapper for pool repository discovery\n\nAdd GitalyClient class that wraps Gitaly RPC calls for:\n\n- Fetching object pool information for repositories\n- Listing all repositories on a storage\n- Caching pool disk paths for efficient lookups\n\nThis client provides the Gitaly integration layer needed to\ncompare Rails database records with actual Gitaly storage state.\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-03-29T13:25:47.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:09:54.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/665111f890a1137be81e121b096ce276491f58bc"},{"id":"d698dbaf8550945a4512acfa35253f6576475617","short_id":"d698dbaf","created_at":"2026-04-14T09:08:06.000+03:00","parent_ids":["62f4f9e65675b3a32752b0f3cfd591dcca1e5c04"],"title":"Remove memozation in rake task","message":"Remove memozation in rake task\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-04-13T19:51:43.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:08:06.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/d698dbaf8550945a4512acfa35253f6576475617"},{"id":"62f4f9e65675b3a32752b0f3cfd591dcca1e5c04","short_id":"62f4f9e6","created_at":"2026-04-14T09:08:03.000+03:00","parent_ids":["c185a044101358a77873e9b336b0ece6928b53a1"],"title":"Make close idempotent","message":"Make close idempotent\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-04-13T09:28:18.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:08:03.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/62f4f9e65675b3a32752b0f3cfd591dcca1e5c04"},{"id":"c185a044101358a77873e9b336b0ece6928b53a1","short_id":"c185a044","created_at":"2026-04-14T09:07:59.000+03:00","parent_ids":["aff3fc79423523de83c1b958b878254a9229b6e4"],"title":"Use pluck over map","message":"Use pluck over map\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-04-02T16:52:06.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:07:59.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/c185a044101358a77873e9b336b0ece6928b53a1"},{"id":"aff3fc79423523de83c1b958b878254a9229b6e4","short_id":"aff3fc79","created_at":"2026-04-14T09:07:56.000+03:00","parent_ids":["8cb98ed867d07c8b463375d910d509cbd725de97"],"title":"Add tests for rake task","message":"Add tests for rake task\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-04-02T16:19:48.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:07:56.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/aff3fc79423523de83c1b958b878254a9229b6e4"},{"id":"8cb98ed867d07c8b463375d910d509cbd725de97","short_id":"8cb98ed8","created_at":"2026-04-14T09:07:53.000+03:00","parent_ids":["e886e712992b4b828d115d4671e96b18c9a53c37"],"title":"Refactor orphan_record","message":"Refactor orphan_record\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-04-01T10:31:49.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:07:53.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/8cb98ed867d07c8b463375d910d509cbd725de97"},{"id":"e886e712992b4b828d115d4671e96b18c9a53c37","short_id":"e886e712","created_at":"2026-04-14T09:07:51.000+03:00","parent_ids":["62081d895c4f9557daf77d4045db16795050789c"],"title":"Refactor to improve code quality","message":"Refactor to improve code quality\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-03-31T09:11:34.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:07:51.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/e886e712992b4b828d115d4671e96b18c9a53c37"},{"id":"62081d895c4f9557daf77d4045db16795050789c","short_id":"62081d89","created_at":"2026-04-14T09:07:48.000+03:00","parent_ids":["c863959254c59c1d1fc98593f330dbdf5646d7b8"],"title":"Refactor to log errors","message":"Refactor to log errors\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-03-30T16:10:21.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:07:48.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/62081d895c4f9557daf77d4045db16795050789c"},{"id":"c863959254c59c1d1fc98593f330dbdf5646d7b8","short_id":"c8639592","created_at":"2026-04-14T09:07:45.000+03:00","parent_ids":["21984d789212dabc1c72a3f388f2b94c95f5329f"],"title":"Add core utility classes for pool repository orphan discovery","message":"Add core utility classes for pool repository orphan discovery\n\nAdd foundational classes that will be used by the orphaned pool\nrepository discovery tool:\n\n- OrphanRecord: Creates standardized orphan record hashes\n- CsvWriter: Handles CSV output for discovered orphans\n- RakeTask: Logger factory for rake task output\n\nThese classes have no internal dependencies and provide the basic\nbuilding blocks for the discovery tool.\n","author_name":"gloria_odipo","author_email":"godipo@gitlab.com","authored_date":"2026-03-29T13:25:03.000+03:00","committer_name":"gloria_odipo","committer_email":"godipo@gitlab.com","committed_date":"2026-04-14T09:07:45.000+03:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/commit/c863959254c59c1d1fc98593f330dbdf5646d7b8"}] \ No newline at end of file diff --git a/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_discussions.json b/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_discussions.json new file mode 100644 index 0000000..da28dc7 --- /dev/null +++ b/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_discussions.json @@ -0,0 +1,60 @@ +[ + { + "id": "disc-001", + "notes": [ + { + "id": 100001, + "body": "This looks like it might break backward compatibility. Can you add a migration?", + "author": { + "username": "reviewer1", + "id": 42 + }, + "created_at": "2024-06-15T10:00:00Z", + "resolvable": true, + "resolved": false, + "position": { + "new_path": "app/models/gitaly_client.rb", + "old_path": "app/models/gitaly_client.rb", + "new_line": 15, + "old_line": null, + "position_type": "text", + "head_sha": "abc123", + "base_sha": "def456", + "start_sha": "def456" + }, + "type": "DiffNote" + }, + { + "id": 100002, + "body": "Good point. Added migration in the latest commit.", + "author": { + "username": "author1", + "id": 99 + }, + "created_at": "2024-06-15T12:00:00Z", + "resolvable": true, + "resolved": false, + "position": null, + "type": "DiffNote" + } + ] + }, + { + "id": "disc-002", + "notes": [ + { + "id": 100003, + "body": "LGTM! Nice clean implementation.", + "author": { + "username": "reviewer2", + "id": 55 + }, + "created_at": "2024-06-15T14:00:00Z", + "resolvable": false, + "resolved": null, + "position": null, + "type": "DiscussionNote" + } + ] + } +] \ No newline at end of file diff --git a/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_mr.json b/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_mr.json new file mode 100644 index 0000000..d7ae748 --- /dev/null +++ b/crates/travelagent-forge-gitlab/tests/fixtures/gitlab_229380_mr.json @@ -0,0 +1 @@ +{"id":468243450,"iid":229380,"project_id":278964,"title":"Add Gitaly client wrapper for pool repository discovery","description":"## What does this MR do and why?\n\n- Adds Gitaly client wrapper for pool repository discovery\n\n- Adds GitalyClient class that wraps Gitaly RPC calls for:\n\n - Fetching object pool information for repositories\n - Listing all repositories on a storage\n - Caching pool disk paths for efficient lookups\n\n- This client provides the Gitaly integration layer needed to compare Rails database records with actual Gitaly storage state.\n\n## References\n\nhttps://gitlab.com/gitlab-org/gitlab/-/work_items/573591+\n\n\u003c!--\nInclude [links](https://handbook.gitlab.com/handbook/communication/#start-with-a-merge-request:~:text=Cross%20link%20issues,alternate%20if%20duplicate.) to any resources that are relevant to this MR.\nThis will give reviewers and future readers helpful context.\n--\u003e\n\n## Screenshots or screen recordings\n\n\u003c!---\nScreenshots are required for UI changes, and strongly recommended for all other merge requests.\n--\u003e\n\n| Before | After |\n| ------ | ------ |\n| | |\n\n\u003c!--\nOPTIONAL: For responsive UI changes, you can use the viewport size table below.\nDelete this table if not needed or delete rows that are not relevant to your changes.\n\n| Viewport size | Before | After |\n| ----------------| ---------- | ---------- |\n| `xs` (\u003c576px) | | |\n| `sm` (\u003e=576px) | | |\n| `md` (\u003e=768px) | | |\n| `lg` (\u003e=992px) | | |\n| `xl` (\u003e=1200px) | | |\n--\u003e\n\n## How to set up and validate locally\n\n\u003c!--\nNumbered steps to set up and validate the change are strongly suggested.\n\nExample:\n\n1. In rails console enable the feature flag\n ```ruby\n Feature.enable(:member_areas_of_focus)\n ```\n1. Visit any group or project member pages such as `http://127.0.0.1:3000/groups/flightjs/-/group_members`\n1. Click the `invite members` button.\n--\u003e\n\n## MR acceptance checklist\n\nEvaluate this MR against the [MR acceptance checklist](https://docs.gitlab.com/development/code_review/#acceptance-checklist).\nIt helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.","state":"merged","created_at":"2026-03-29T10:39:17.528Z","updated_at":"2026-04-17T03:47:28.111Z","merged_by":{"id":421631,"username":"vyaklushin","public_email":"viakliushin@gitlab.com","name":"Vasilii Iakliushin","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/421631/avatar.png","web_url":"https://gitlab.com/vyaklushin"},"merge_user":{"id":421631,"username":"vyaklushin","public_email":"viakliushin@gitlab.com","name":"Vasilii Iakliushin","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/421631/avatar.png","web_url":"https://gitlab.com/vyaklushin"},"merged_at":"2026-04-16T18:47:39.368Z","closed_by":null,"closed_at":null,"target_branch":"master","source_branch":"pool-repos-2-gitaly-client","user_notes_count":22,"upvotes":0,"downvotes":0,"author":{"id":29040103,"username":"gloria_odipo","public_email":"","name":"Gloria Odipo","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/29040103/avatar.png","web_url":"https://gitlab.com/gloria_odipo"},"assignees":[{"id":29040103,"username":"gloria_odipo","public_email":"","name":"Gloria Odipo","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/29040103/avatar.png","web_url":"https://gitlab.com/gloria_odipo"}],"assignee":{"id":29040103,"username":"gloria_odipo","public_email":"","name":"Gloria Odipo","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/29040103/avatar.png","web_url":"https://gitlab.com/gloria_odipo"},"reviewers":[{"id":421631,"username":"vyaklushin","public_email":"viakliushin@gitlab.com","name":"Vasilii Iakliushin","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/421631/avatar.png","web_url":"https://gitlab.com/vyaklushin"},{"id":21751328,"username":"freinink","public_email":"freinink@gitlab.com","name":"Fred Reinink","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/21751328/avatar.png","web_url":"https://gitlab.com/freinink"},{"id":21826781,"username":"GitLabDuo","public_email":null,"name":"GitLab Duo","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/21826781/duo-bot.png","web_url":"https://gitlab.com/GitLabDuo"}],"source_project_id":278964,"target_project_id":278964,"labels":["Category:Source Code Management","Deliverable","SLO::Missed","backend","bug::functional","devops::create","group::source code","missed-deliverable","missed:18.11","pipeline::tier-3","pipeline:as-if-foss-run-once","pipeline:mr-approved","rspec:slow test detected","section::dev","severity::3","type::bug","workflow::staging"],"draft":false,"imported":false,"imported_from":"none","work_in_progress":false,"milestone":{"id":5981229,"iid":131,"group_id":9970,"title":"19.0","description":"| Milestone | Quarter | Start Date - Code Merging | End Date - Code Merging | Release Date |\r\n|-----------|------------|---------------------------|-------------------------|---------------|\r\n| 19.0 | FY27-Q2-M1 | 2026-05-07 | 2026-05-08 | 2026-05-14 |","state":"active","created_at":"2025-04-21T21:49:02.622Z","updated_at":"2025-06-30T16:29:51.458Z","due_date":"2026-05-15","start_date":"2026-04-11","expired":false,"web_url":"https://gitlab.com/groups/gitlab-org/-/milestones/131"},"merge_when_pipeline_succeeds":true,"merge_status":"can_be_merged","detailed_merge_status":"not_open","merge_after":null,"sha":"4b231e4f10405511f0ff35433d0f811831fda677","merge_commit_sha":"7a0acccd2ddde48660e01633e59ac7a88426663e","squash_commit_sha":"08e32d31c1ac1cfb25d3657f0b9e9bcbf2fc5bef","discussion_locked":null,"should_remove_source_branch":true,"force_remove_source_branch":true,"prepared_at":"2026-03-29T10:39:50.768Z","reference":"!229380","references":{"short":"!229380","relative":"!229380","full":"gitlab-org/gitlab!229380"},"web_url":"https://gitlab.com/gitlab-org/gitlab/-/merge_requests/229380","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":true,"squash_on_merge":true,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null,"subscribed":false,"changes_count":"8","latest_build_started_at":"2026-04-16T18:47:20.992Z","latest_build_finished_at":"2026-04-16T18:47:36.946Z","first_deployed_to_production_at":"2026-04-16T20:53:55.384Z","pipeline":{"id":2458683981,"iid":5518244,"project_id":278964,"sha":"b7c57c7bf0439fbf26d8bce9d05be080f46046a5","ref":"refs/merge-requests/229380/train","status":"success","source":"merge_request_event","created_at":"2026-04-16T18:47:19.141Z","updated_at":"2026-04-16T18:47:37.013Z","web_url":"https://gitlab.com/gitlab-org/gitlab/-/pipelines/2458683981"},"head_pipeline":{"id":2458683981,"iid":5518244,"project_id":278964,"sha":"b7c57c7bf0439fbf26d8bce9d05be080f46046a5","ref":"refs/merge-requests/229380/train","status":"success","source":"merge_request_event","created_at":"2026-04-16T18:47:19.141Z","updated_at":"2026-04-16T18:47:37.013Z","web_url":"https://gitlab.com/gitlab-org/gitlab/-/pipelines/2458683981","before_sha":"b7c57c7bf0439fbf26d8bce9d05be080f46046a5","tag":false,"yaml_errors":null,"user":{"id":421631,"username":"vyaklushin","public_email":"viakliushin@gitlab.com","name":"Vasilii Iakliushin","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/421631/avatar.png","web_url":"https://gitlab.com/vyaklushin"},"started_at":"2026-04-16T18:47:20.992Z","finished_at":"2026-04-16T18:47:36.946Z","committed_at":null,"duration":15,"queued_duration":1,"coverage":null,"detailed_status":{"icon":"status_success","text":"Passed","label":"passed","group":"success","tooltip":"passed","has_details":true,"details_path":"/gitlab-org/gitlab/-/pipelines/2458683981","illustration":null,"favicon":"/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"},"archived":false},"diff_refs":{"base_sha":"21984d789212dabc1c72a3f388f2b94c95f5329f","head_sha":"4b231e4f10405511f0ff35433d0f811831fda677","start_sha":"d365f8c375fcd25efa9704c2df08c0466d3ccb0f"},"merge_error":null,"first_contribution":false,"user":{"can_merge":false}} \ No newline at end of file diff --git a/crates/travelagent-forge-gitlab/tests/live_smoke.rs b/crates/travelagent-forge-gitlab/tests/live_smoke.rs new file mode 100644 index 0000000..d863098 --- /dev/null +++ b/crates/travelagent-forge-gitlab/tests/live_smoke.rs @@ -0,0 +1,24 @@ +use travelagent_core::forge::*; +use travelagent_forge_gitlab::GitLabForge; + +#[tokio::test] +#[ignore] // Requires GITLAB_TOKEN for gitlab.com; run with: cargo test -- --ignored +async fn live_gitlab_fetch_mr() { + let forge = match GitLabForge::new() { + Ok(f) => f, + Err(_) => { + eprintln!("Skipping: no GitLab token"); + return; + } + }; + // gitlab-org/gitlab is a public project + let pr_id = PrId { + owner: "gitlab-org".into(), + repo: "gitlab".into(), + number: 229380, + }; + + let meta = forge.get_pr(&pr_id).await.unwrap(); + assert!(!meta.title.is_empty()); + assert_eq!(meta.state, PrState::Merged); +}