Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull request #314

Merged
merged 38 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
dca1ad8
Added a tab to the dashboard to allow people with permissions to edit…
TheKrol Nov 25, 2024
7584eed
Trying to get issues to work
TheKrol Dec 11, 2024
1b53bc0
Getting issue with the help of Node
TheKrol Dec 11, 2024
2adbe8b
Move pull request button to top bar
TheKrol Dec 11, 2024
a672190
Added issues and the ability to link them to a pull request
TheKrol Dec 17, 2024
4447d9f
Added a description
TheKrol Dec 17, 2024
0709331
Adding issue descriptions and a bunch of CSS updates
TheKrol Dec 17, 2024
f5e6f19
Some CSS updates
TheKrol Dec 17, 2024
1046687
Added a Check if the current branch already has a pull request
TheKrol Dec 19, 2024
66d28df
Added the ability to update the pull request, needs some work on doub…
TheKrol Dec 19, 2024
36d23a9
Update some logice for the update pull request
TheKrol Dec 19, 2024
d3f39b2
Update more logic
TheKrol Dec 19, 2024
24b67c0
Fixing loading icon issue
TheKrol Dec 19, 2024
f7c7d42
Fixing database permissions
TheKrol Dec 19, 2024
a063607
Remove vscode file
TheKrol Dec 19, 2024
a498af8
adding files
TheKrol Dec 19, 2024
3374741
Added the ability to close your own PR or if admin you can close any …
TheKrol Dec 23, 2024
7579289
Added the ability to change the base branch
TheKrol Dec 23, 2024
82a6f20
Changed some logic for update pull request
TheKrol Dec 23, 2024
484541f
Formatting update
TheKrol Dec 23, 2024
66bf4f3
Formatting update
TheKrol Dec 23, 2024
cc8d2cc
Fixing testing?
TheKrol Dec 23, 2024
474b18d
css tweaks
zleyyij Dec 25, 2024
3df4e21
style
zleyyij Dec 25, 2024
4e68ffd
Fixing some issues from feedback
TheKrol Dec 26, 2024
639e2ab
fix(frontend): style tweak for PR button
zleyyij Dec 27, 2024
ace5a19
style(frontend): fix style
zleyyij Dec 27, 2024
3aa7345
Fixing on feedback
TheKrol Dec 29, 2024
44e823c
Formatting update
TheKrol Dec 29, 2024
c2425ac
Formatting update
TheKrol Dec 29, 2024
40b51ae
refactor: frontend
zleyyij Dec 30, 2024
47b46f2
feat(frontend): more work on branch management
zleyyij Dec 30, 2024
f3a49e8
sync
zleyyij Jan 8, 2025
a15837e
fix(frontend): temporarily disable admin branch stuff
zleyyij Jan 8, 2025
38cc539
style(frontend): fix style
zleyyij Jan 9, 2025
210dadc
fix(frontend): failing build caused by stray troubleshooting
zleyyij Jan 9, 2025
e5a4a54
Merge branch 'main' into pull-request
zleyyij Jan 9, 2025
34bc735
style(frontend): fix style
zleyyij Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
TheKrol marked this conversation as resolved.
Show resolved Hide resolved
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>>,
TheKrol marked this conversation as resolved.
Show resolved Hide resolved
) -> 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);
zleyyij marked this conversation as resolved.
Show resolved Hide resolved
}

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

Ok(issues)
}

}
Loading
Loading