Skip to content

Commit

Permalink
Merge pull request #314 from r-Techsupport/pull-request
Browse files Browse the repository at this point in the history
Pull request
  • Loading branch information
zleyyij authored Jan 9, 2025
2 parents b92ef97 + 34bc735 commit fb97d6e
Show file tree
Hide file tree
Showing 23 changed files with 1,433 additions and 176 deletions.
2 changes: 2 additions & 0 deletions backend/migrations/20241219200647_manage-branches.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Add migration script here
INSERT into group_permissions ( group_id, permission ) VALUES ( 1, "ManageBranches" );
2 changes: 1 addition & 1 deletion backend/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ mod tests {
let admin_permissions = mock_db.get_group_permissions(1).await.unwrap();
assert_eq!(
admin_permissions,
vec![Permission::ManageContent, Permission::ManageUsers],
vec![Permission::ManageContent, Permission::ManageUsers, Permission::ManageBranches],
"admin group should have the right permissions"
);
}
Expand Down
252 changes: 247 additions & 5 deletions backend/src/gh.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Code for interacting with GitHub (authentication, prs, et cetera)
use chrono::DateTime;
use color_eyre::eyre::{bail, Context};
use color_eyre::eyre::{bail, Context, ContextCompat};
use color_eyre::Result;
use fs_err as fs;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
Expand Down Expand Up @@ -250,16 +250,25 @@ impl GitHubClient {
base_branch: &str,
pr_title: &str,
pr_description: &str,
issue_numbers: Option<Vec<u64>>,
) -> Result<String> {
// Parse the repository name from self.repo_url
let repo_name = self.get_repo_name()?;

// Prepare the JSON body for the pull request
let pr_body = json!({
let mut pr_body = pr_description.to_string();

// If issue numbers are provided, add them to the body
if let Some(issues) = issue_numbers {
for issue in issues {
pr_body.push_str(&format!("\n\nCloses #{}", issue)); // Add "Closes #<issue_number>" for each issue
}
}

let pr_body_json = json!({
"title": pr_title,
"head": head_branch,
"base": base_branch,
"body": pr_description,
"body": pr_body,
});

debug!("Creating pull request to {}/repos/{}/pulls", GITHUB_API_URL, repo_name);
Expand All @@ -270,7 +279,7 @@ impl GitHubClient {
.post(format!("{}/repos/{}/pulls", GITHUB_API_URL, repo_name))
.bearer_auth(&self.token)
.header("User-Agent", "Hyde")
.json(&pr_body)
.json(&pr_body_json)
.send()
.await?;

Expand Down Expand Up @@ -299,6 +308,133 @@ impl GitHubClient {
}
}

/// Updates an existing pull request on GitHub with the specified details.
///
/// # Arguments
/// - `pr_number` - The pull request number to update.
/// - `pr_title` - Optional new title for the pull request.
/// - `pr_description` - Optional updated description for the pull request.
/// - `base_branch` - Optional target base branch to update the pull request against.
/// - `issue_numbers` - Optional list of issue numbers to link to the pull request. Each issue
/// will be referenced in the pull request description using the "Closes #<issue_number>" syntax.
///
/// # Returns
/// Returns the URL of the updated pull request if the operation is successful.
///
/// # Errors
/// Returns an error if:
/// - The repository name cannot be determined.
/// - The GitHub API request fails, including cases where the response does not contain the expected `html_url` field.
/// - Network or deserialization issues occur while processing the response.
pub async fn update_pull_request(
&self,
pr_number: u64,
pr_title: Option<&str>,
pr_description: Option<&str>,
base_branch: Option<&str>,
issue_numbers: Option<Vec<u64>>,
) -> Result<String> {
let repo_name = self.get_repo_name()?;

let mut pr_body_json = serde_json::Map::new();

if let Some(title) = pr_title {
pr_body_json.insert("title".to_string(), json!(title));
}

let mut pr_body = String::new();

// If description is provided, include it in the body
if let Some(description) = pr_description {
pr_body.push_str(description);
}

// If issue numbers are provided, add them to the body
if let Some(issues) = issue_numbers {
for issue in issues {
pr_body.push_str(&format!("\n\nCloses #{}", issue)); // Add "Closes #<issue_number>" for each issue
}
}

// Add the constructed body to the JSON body
pr_body_json.insert("body".to_string(), json!(pr_body));

if let Some(base) = base_branch {
pr_body_json.insert("base".to_string(), json!(base));
}

debug!("Updating pull request {} in {}/repos/{}/pulls", pr_number, GITHUB_API_URL, repo_name);

// Send the request to the GitHub API to update the pull request
let response = self
.client
.patch(format!("{}/repos/{}/pulls/{}", GITHUB_API_URL, repo_name, pr_number))
.bearer_auth(&self.token)
.header("User-Agent", "Hyde")
.json(&pr_body_json)
.send()
.await?;

// Handle the response based on the status code
if response.status().is_success() {
info!("Pull request #{} updated successfully", pr_number);

// Extract the response JSON to get the updated pull request URL
let response_json: Value = response.json().await?;
if let Some(url) = response_json.get("html_url").and_then(Value::as_str) {
Ok(url.to_string()) // Return the updated URL
} else {
bail!("Expected URL field not found in the response.");
}
} else {
let status = response.status();
let response_text = response.text().await?;
bail!(
"Failed to update pull request #{}: {}, Response: {}",
pr_number,
status,
response_text
);
}
}

pub async fn close_pull_request(&self, pr_number: u64) -> Result<()> {
// Get the repository name from the repository URL
let repo_name = self.get_repo_name()?;

info!("Closing pull request #{} in repository {}", pr_number, repo_name);

// Construct the JSON body to close the pull request
let pr_body_json = json!({
"state": "closed"
});

// Send the request to GitHub API to close the pull request
let response = self
.client
.patch(format!("{}/repos/{}/pulls/{}", GITHUB_API_URL, repo_name, pr_number))
.bearer_auth(&self.token)
.header("User-Agent", "Hyde")
.json(&pr_body_json)
.send()
.await?;

// Handle the response
if response.status().is_success() {
info!("Pull request #{} closed successfully", pr_number);
Ok(())
} else {
let status = response.status();
let response_text = response.text().await?;
bail!(
"Failed to close pull request #{}: {}, Response: {}",
pr_number,
status,
response_text
);
}
}

/// Fetches a complete list of branches from the specified GitHub repository.
///
/// This function retrieves all branches for a repository by sending paginated GET requests to the GitHub API.
Expand Down Expand Up @@ -445,4 +581,110 @@ impl GitHubClient {

Ok(branch_details)
}

/// Fetches the default branch of the repository associated with the authenticated user.
///
/// This function retrieves the repository name using `get_repo_name`,
/// sends a GET request to the GitHub API to fetch repository details,
/// and extracts the default branch from the response.
///
/// # Errors
/// Returns an error in the following cases:
/// - If the repository name cannot be retrieved.
/// - If the GET request to fetch repository details fails (e.g., network issues or API errors).
/// - If the response does not contain a valid `default_branch` field.
///
/// # Returns
/// - `Ok(String)` containing the default branch name if successful.
/// - `Err(anyhow::Error)` if any step in the process fails.
pub async fn get_default_branch(&self) -> Result<String> {
// Extract repository name from `repo_url`
let repo_name = self.get_repo_name()?;

// Make the GET request to fetch repository details
let response = self
.client
.get(format!("{}/repos/{}", GITHUB_API_URL, repo_name))
.bearer_auth(&self.token)
.header("User-Agent", "Hyde")
.send()
.await?;

// Check response status
if !response.status().is_success() {
let status = response.status();
let response_text = response.text().await?;
bail!("Failed to fetch repository details: {}, Response: {}", status, response_text);
}

// Deserialize the response to get the repository details
let repo_details: serde_json::Value = response.json().await?;

// Retrieve the default branch from the response
let default_branch = repo_details["default_branch"]
.as_str()
.map(ToString::to_string)
.context("'default_branch' field missing in the response")?;

Ok(default_branch)
}

/// Fetches issues from the GitHub repository.
///
/// This function retrieves issues from the specified repository using the GitHub API.
/// You can filter issues based on their state and associated labels.
///
/// # Parameters:
/// - `state`: A string slice representing the state of the issues to fetch (e.g., "open", "closed", "all").
/// Defaults to "open".
/// - `labels`: A comma-separated string slice representing labels to filter issues by. Defaults to `None`.
///
/// # Returns:
/// A `Result<Vec<Value>>`:
/// - `Ok(issues)`: A vector of JSON values representing the issues fetched from the repository.
/// - `Err(e)`: An error message if the request fails or the response cannot be parsed.
///
/// # Errors:
/// This function may return an error if:
/// - The `repo_url` is not in the expected format and cannot be parsed to derive the repository name.
/// - The request to fetch issues fails due to authentication issues, invalid input, or network problems.
/// - The GitHub API response cannot be parsed as a JSON array.
pub async fn get_issues(&self, state: Option<&str>, labels: Option<&str>) -> Result<Vec<Value>> {
let repo_name = self.get_repo_name()?;

let state = state.unwrap_or("open"); // Default state
let mut query_params = vec![format!("state={}", state)];
if let Some(labels) = labels {
query_params.push(format!("labels={}", labels));
}
let query_string = format!("?{}", query_params.join("&"));

let url = format!("{}/repos/{}/issues{}", GITHUB_API_URL, repo_name, query_string);
debug!("Request URL: {}", url);

let response = self
.client
.get(&url)
.bearer_auth(&self.token)
.header("Accept", "application/vnd.github+json")
.header("User-Agent", "Hyde")
.timeout(std::time::Duration::from_secs(10))
.send()
.await?;

if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
error!("GitHub API request failed with status {}: {}", status, error_text);
bail!("GitHub API request failed ({}): {}", status, error_text);
}

let issues: Vec<Value> = response.json().await.map_err(|e| {
error!("Failed to parse GitHub response JSON: {:?}", e);
e
})?;

Ok(issues)
}

}
Loading

0 comments on commit fb97d6e

Please sign in to comment.