Skip to content

Commit

Permalink
Updated to include existing branches as a choice.
Browse files Browse the repository at this point in the history
  • Loading branch information
TheKrol committed Oct 16, 2024
1 parent 50ec5a1 commit bff018b
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 59 deletions.
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"reqwest"
]
}
56 changes: 55 additions & 1 deletion backend/src/gh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ struct AccessTokenResponse {
token: String,
}

#[derive(Deserialize)]
pub struct Branch {
pub name: String,
}

/// Request a github installation access token using the provided reqwest client.
/// The installation access token will expire after 1 hour.
/// Returns the new token, and the time of expiration
Expand Down Expand Up @@ -202,7 +207,7 @@ pub async fn create_pull_request(

// Ensure repo_path has both owner and repo
if repo_path.len() < 2 {
return Err(color_eyre::eyre::eyre!("Invalid REPO_URL format, must be <owner>/<repo>."));
bail!("Invalid REPO_URL format, must be <owner>/<repo>.");
}

let repo_name = format!("{}/{}", repo_path[1], repo_path[0]); // <owner>/<repo>
Expand Down Expand Up @@ -241,3 +246,52 @@ pub async fn create_pull_request(

Ok(())
}

/// Fetch a list of branches from the GitHub repository.
///
/// # Parameters:
/// - `req_client`: The `reqwest::Client` to make HTTP requests.
/// - `token`: The GitHub access token for authentication.
/// - `repo_name`: The repository name in the format `<owner>/<repo>`.
///
/// # Returns:
/// A `Result` containing a vector of branch names or an error.
pub async fn list_branches(req_client: &Client, token: &str) -> Result<Vec<String>> {
dotenv().ok();

let repo_url = env::var("REPO_URL")
.context("REPO_URL must be set in the .env file")?;

let repo_path = repo_url
.trim_end_matches(".git")
.rsplit('/')
.collect::<Vec<&str>>();

if repo_path.len() < 2 {
bail!("Invalid REPO_URL format, must be <owner>/<repo>.");
}

let repo_name = format!("{}/{}", repo_path[1], repo_path[0]); // <owner>/<repo>

let response = req_client
.get(format!("{}/repos/{}/branches", GITHUB_API_URL, repo_name))
.bearer_auth(token)
.header("User-Agent", "Hyde")
.send()
.await?;

// Handle the response based on the status code
if response.status().is_success() {
let branches: Vec<Branch> = serde_json::from_slice(&response.bytes().await?)?;
let branch_names = branches.into_iter().map(|b| b.name).collect();
Ok(branch_names)
} else {
let status = response.status();
let response_text = response.text().await?;
bail!(
"Failed to fetch branches: {}, Response: {}",
status,
response_text
);
}
}
37 changes: 37 additions & 0 deletions backend/src/handlers_prelude/github_list_branches.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! Getting Github Branches
use axum::{
extract::State,
http::StatusCode,
Json, Router,
};
use axum::routing::get;
use tracing::{error, info};
use serde_json::json;
use crate::gh::list_branches;
use crate::AppState;
use color_eyre::Result;

pub async fn list_branches_handler(
State(state): State<AppState>,
) -> Result<(StatusCode, Json<serde_json::Value>), (StatusCode, Json<serde_json::Value>)> {
// Get the access token
let token = state.gh_credentials.get(&state.reqwest_client).await.map_err(|err| {
error!("Failed to get GitHub token: {:?}", err);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "Failed to get access token"})))
})?;

// Call the function to fetch branches (repo_name is derived inside the function)
let branches = list_branches(&state.reqwest_client, &token).await.map_err(|err| {
error!("Error fetching branches: {:?}", err);
let error_message = format!("Failed to fetch branches: {:?}", err);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": error_message})))
})?;

info!("Fetched branches successfully.");
Ok((StatusCode::OK, Json(json!(branches))))
}

// Update the route to handle GET requests for branches
pub async fn list_github_branches_route() -> Router<AppState> {
Router::new().route("/branches", get(list_branches_handler))
}
1 change: 1 addition & 0 deletions backend/src/handlers_prelude/github_pull_request.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Github Pull Request are sent here.
use axum::{extract::State, http::StatusCode, Json, Router};
use axum::routing::post;
use tracing::{error, info};
Expand Down
2 changes: 2 additions & 0 deletions backend/src/handlers_prelude/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ mod reclone;
pub use reclone::*;
mod github_pull_request;
pub use github_pull_request::*;
mod github_list_branches;
pub use github_list_branches::*;

use color_eyre::{
eyre::{Context, ContextCompat},
Expand Down
3 changes: 2 additions & 1 deletion backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ async fn start_server(state: AppState, cli_args: Args) -> Result<()> {
.merge(create_github_route().await)
.merge(create_doc_route().await)
.merge(create_tree_route().await)
.merge(create_github_pull_request_route().await);
.merge(create_github_pull_request_route().await)
.merge(list_github_branches_route().await);

let app = Router::new()
.nest("/api", api_routes)
Expand Down
240 changes: 183 additions & 57 deletions frontend/src/lib/components/BranchButton.svelte
Original file line number Diff line number Diff line change
@@ -1,66 +1,192 @@
<!-- BranchButton.svelte -->
<script lang="ts">
import { branchName } from '$lib/main';
import { derived } from 'svelte/store';
export let initialBranchName: string; // Changed prop name for clarity
// Create a reactive derived store to automatically update the branch name
const currentBranch = derived(branchName, ($branchName) => $branchName || initialBranchName);
function setBranchName() {
const input = prompt('Enter the branch name:', $currentBranch); // Use derived store value
// Define validation rules
const maxLength = 255; // Maximum length for branch name
const invalidCharacters = /[~^:?*<>|]/; // Invalid special characters
const startsWithLetterOrNumber = /^[a-zA-Z0-9]/; // Starts with letter or number
const containsSpaces = /\s/; // Contains spaces
if (input && input !== $currentBranch) {
// Check if input is different and not empty
// Check for validation rules
if (containsSpaces.test(input)) {
alert('Branch names cannot contain spaces. Use dashes (-) or underscores (_) instead.'); // Inform the user
} else if (!startsWithLetterOrNumber.test(input)) {
alert('Branch names must start with a letter or a number.'); // Inform the user
} else if (invalidCharacters.test(input)) {
alert('Branch names cannot contain special characters like ~, ^, :, ?, *, and others.'); // Inform the user
} else if (input.length > maxLength) {
alert(`Branch names must be shorter than ${maxLength} characters.`); // Inform the user
} else {
branchName.set(input); // Update the store
console.log(`Branch name set to: ${input}`); // For debugging
}
} else if (!input) {
console.log('Branch name update canceled or input is empty.'); // Handle cancel or empty input
}
import { branchName } from '$lib/main';
import { derived } from 'svelte/store';
import { onMount } from 'svelte';
import { apiAddress } from '$lib/net';
export let initialBranchName: string;
let showMenu = false;
let existingBranches: string[] = [];
let newBranchName: string = '';
const currentBranch = derived(branchName, ($branchName) => $branchName || initialBranchName);
async function fetchExistingBranches() {
const response = await fetch(`${apiAddress}/api/branches`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
});
// Check if the response is successful
if (!response.ok) {
const errorMessage = await response.json();
throw new Error(`Error fetching branches: ${response.statusText}. ${JSON.stringify(errorMessage)}`);
}
existingBranches = await response.json();
}
onMount(() => {
fetchExistingBranches();
});
async function setBranchName(input: string) {
// Define validation rules
const maxLength = 255; // Maximum length for branch name
const invalidCharacters = /[~^:?*<>|]/; // Invalid special characters
const startsWithLetterOrNumber = /^[a-zA-Z0-9]/; // Starts with letter or number
const containsSpaces = /\s/; // Contains spaces
// Define an array of rules
const rules = [
"Branch names must start with a letter or a number.",
"Branch names cannot contain spaces. Use dashes (-) or underscores (_) instead.",
"Branch names cannot contain special characters like ~, ^, :, ?, *, and others.",
`Branch names must be shorter than ${maxLength} characters.`
];
let isValid = true;
if (input) {
if (containsSpaces.test(input) || !startsWithLetterOrNumber.test(input) ||
invalidCharacters.test(input) || input.length > maxLength) {
isValid = false;
}
if (!isValid) {
alert("Please ensure your branch name follows these rules:\n\n" + rules.join("\n"));
} else {
branchName.set(input);
newBranchName = '';
showMenu = false;
await fetchExistingBranches();
}
}
}
function toggleMenu() {
showMenu = !showMenu;
}
function closeMenu() {
showMenu = false;
}
</script>

<button
on:click={setBranchName}
class="branch-button"
title="Set Branch Name"
aria-label="Set Branch Name"
>
{$currentBranch}
</button>
<div class="branch-dropdown">
<button
on:click={toggleMenu}
class="branch-button"
title="Set Branch Name"
aria-label="Set Branch Name"
>
{$currentBranch}
</button>

{#if showMenu}
<div class="branch-menu">
<button class="close-button" on:click={closeMenu} aria-label="Close menu">✖</button>
<h4>Select Existing Branch</h4>
<ul class="branch-list">
{#each existingBranches as branch}
<li>
<button
class="branch-option"
on:click={() => setBranchName(branch)}
on:keydown={(e) => e.key === 'Enter' && setBranchName(branch)}
aria-label={`Select branch ${branch}`}
>
{branch} <!-- This line will display the branch name -->
</button>
</li>
{/each}
{#if existingBranches.length === 0}
<li>No branches available</li> <!-- Show message if no branches -->
{/if}
</ul>
<h4>Create New Branch</h4>
<input
type="text"
bind:value={newBranchName}
placeholder="Enter new branch name"
on:keydown={(e) => e.key === 'Enter' && setBranchName(newBranchName)}
/>
<button on:click={() => setBranchName(newBranchName)}>Create Branch</button>
</div>
{/if}
</div>

<style>
.branch-button {
background-color: var(--button-background);
color: var(--button-text);
border: none;
border-radius: 0.3rem;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: medium;
margin-right: 1rem;
}
.branch-dropdown {
position: relative;
}
.branch-button:hover {
background-color: var(--button-hover);
transition: background-color 0.3s ease; /* Add transition for a smoother effect */
}
.branch-button {
background-color: var(--button-background);
color: var(--button-text);
border: none;
border-radius: 0.3rem;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: medium;
margin-right: 1rem;
}
.branch-button:hover {
background-color: var(--button-hover);
transition: background-color 0.3s ease;
}
.branch-menu {
position: absolute;
background-color: white;
border: 1px solid #ccc;
padding: 1rem;
z-index: 1000;
}
.close-button {
background: none;
border: none;
color: #ff0000;
cursor: pointer;
font-size: 1.2rem;
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
.branch-option {
padding: 0.5rem;
cursor: pointer;
}
.branch-option:hover {
background-color: var(--button-hover);
}
input {
margin-top: 0.5rem;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 0.3rem;
}
button {
margin-top: 0.5rem;
padding: 0.5rem 1rem;
background-color: var(--button-background);
border: none;
border-radius: 0.3rem;
color: var(--button-text);
cursor: pointer;
}
button:hover {
background-color: var(--button-hover);
}
</style>

0 comments on commit bff018b

Please sign in to comment.