Skip to content

Commit a8c605c

Browse files
committed
Use GraphQL to determine if a user is new.
1 parent 5df6772 commit a8c605c

File tree

1 file changed

+106
-12
lines changed

1 file changed

+106
-12
lines changed

src/github.rs

+106-12
Original file line numberDiff line numberDiff line change
@@ -1918,26 +1918,120 @@ impl GithubClient {
19181918
}
19191919
}
19201920

1921+
/// Issues an ad-hoc GraphQL query.
1922+
pub async fn graphql_query<T: serde::de::DeserializeOwned>(
1923+
&self,
1924+
query: &str,
1925+
vars: serde_json::Value,
1926+
) -> anyhow::Result<T> {
1927+
self.json(
1928+
self.post(Repository::GITHUB_GRAPHQL_API_URL)
1929+
.json(&serde_json::json!({
1930+
"query": query,
1931+
"variables": vars,
1932+
})),
1933+
)
1934+
.await
1935+
}
1936+
1937+
/// Returns the object ID of the given user.
1938+
///
1939+
/// Returns `None` if the user doesn't exist.
1940+
pub async fn user_object_id(&self, user: &str) -> anyhow::Result<Option<String>> {
1941+
let user_info: serde_json::Value = self
1942+
.graphql_query(
1943+
"query($user:String!) {
1944+
user(login:$user) {
1945+
id
1946+
}
1947+
}",
1948+
serde_json::json!({
1949+
"user": user,
1950+
}),
1951+
)
1952+
.await?;
1953+
if let Some(id) = user_info["data"]["user"]["id"].as_str() {
1954+
return Ok(Some(id.to_string()));
1955+
}
1956+
if let Some(errors) = user_info["errors"].as_array() {
1957+
if errors
1958+
.iter()
1959+
.any(|err| err["type"].as_str().unwrap_or_default() == "NOT_FOUND")
1960+
{
1961+
return Ok(None);
1962+
}
1963+
let messages: Vec<_> = errors
1964+
.iter()
1965+
.map(|err| err["message"].as_str().unwrap_or_default())
1966+
.collect();
1967+
anyhow::bail!("failed to query user: {}", messages.join("\n"));
1968+
}
1969+
anyhow::bail!("query for user {user} failed, no error message? {user_info:?}");
1970+
}
1971+
19211972
/// Returns whether or not the given GitHub login has made any commits to
19221973
/// the given repo.
19231974
pub async fn is_new_contributor(&self, repo: &Repository, author: &str) -> bool {
1924-
let url = format!(
1925-
"{}/repos/{}/commits?author={}",
1926-
Repository::GITHUB_API_URL,
1927-
repo.full_name,
1928-
author,
1929-
);
1930-
let req = self.get(&url);
1931-
match self.json::<Vec<GithubCommit>>(req).await {
1932-
// Note: This only returns results for the default branch.
1933-
// That should be fine in most cases since I think it is rare for
1934-
// new users to make their first commit to a different branch.
1935-
Ok(res) => res.is_empty(),
1975+
let user_id = match self.user_object_id(author).await {
1976+
Ok(None) => return true,
1977+
Ok(Some(id)) => id,
1978+
Err(e) => {
1979+
log::warn!("failed to query user: {e:?}");
1980+
return true;
1981+
}
1982+
};
1983+
// Note: This only returns results for the default branch. That should
1984+
// be fine in most cases since I think it is rare for new users to
1985+
// make their first commit to a different branch.
1986+
//
1987+
// Note: This is using GraphQL because the
1988+
// `/repos/ORG/REPO/commits?author=AUTHOR` API was having problems not
1989+
// finding users (https://github.com/rust-lang/triagebot/issues/1689).
1990+
// The other possibility is the `/search/commits?q=repo:{}+author:{}`
1991+
// API, but that endpoint has a very limited rate limit, and doesn't
1992+
// work on forks. This GraphQL query seems to work fairly reliably,
1993+
// and seems to cost only 1 point.
1994+
match self
1995+
.graphql_query::<serde_json::Value>(
1996+
"query($repository_owner:String!, $repository_name:String!, $user_id:ID!) {
1997+
repository(owner: $repository_owner, name: $repository_name) {
1998+
defaultBranchRef {
1999+
target {
2000+
... on Commit {
2001+
history(author: {id: $user_id}) {
2002+
totalCount
2003+
}
2004+
}
2005+
}
2006+
}
2007+
}
2008+
}",
2009+
serde_json::json!({
2010+
"repository_owner": repo.owner(),
2011+
"repository_name": repo.name(),
2012+
"user_id": user_id
2013+
}),
2014+
)
2015+
.await
2016+
{
2017+
Ok(c) => {
2018+
if let Some(c) = c["data"]["repository"]["defaultBranchRef"]["target"]["history"]
2019+
["totalCount"]
2020+
.as_i64()
2021+
{
2022+
return c == 0;
2023+
}
2024+
log::warn!("new user query failed: {c:?}");
2025+
false
2026+
}
19362027
Err(e) => {
19372028
log::warn!(
19382029
"failed to search for user commits in {} for author {author}: {e:?}",
19392030
repo.full_name
19402031
);
2032+
// Using `false` since if there is some underlying problem, we
2033+
// don't need to spam everyone with the "new user" welcome
2034+
// message.
19412035
false
19422036
}
19432037
}

0 commit comments

Comments
 (0)