diff --git a/backend/app/git/branches/checkout.py b/backend/app/git/branches/checkout.py index 9c3e37a..2eed401 100644 --- a/backend/app/git/branches/checkout.py +++ b/backend/app/git/branches/checkout.py @@ -5,20 +5,31 @@ import logging logger = logging.getLogger(__name__) + def checkout_branch(repo_path, branch_name): try: logger.debug(f"Attempting to checkout branch: {branch_name}") repo = git.Repo(repo_path) - # Check if the branch exists - if branch_name not in repo.heads: - return False, f"Branch '{branch_name}' does not exist." - - # Checkout the branch - repo.git.checkout(branch_name) + # Check if the branch exists locally + if branch_name in repo.heads: + repo.git.checkout(branch_name) + else: + # Check if the branch exists in any of the remotes + for remote in repo.remotes: + remote_branch = f"{remote.name}/{branch_name}" + if remote_branch in repo.refs: + # Create a new local branch tracking the remote branch + repo.git.checkout('-b', branch_name, remote_branch) + break + else: + return False, f"Branch '{branch_name}' does not exist locally or in any remote." logger.debug(f"Successfully checked out branch: {branch_name}") - return True, {"message": f"Checked out branch: {branch_name}", "current_branch": branch_name} + return True, { + "message": f"Checked out branch: {branch_name}", + "current_branch": branch_name + } except Exception as e: logger.error(f"Error checking out branch: {str(e)}", exc_info=True) - return False, {"error": f"Error checking out branch: {str(e)}"} \ No newline at end of file + return False, {"error": f"Error checking out branch: {str(e)}"} diff --git a/backend/app/git/branches/delete.py b/backend/app/git/branches/delete.py index 74fc5ea..276f538 100644 --- a/backend/app/git/branches/delete.py +++ b/backend/app/git/branches/delete.py @@ -7,10 +7,12 @@ import logging logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) + def delete_branch(repo_path, branch_name): try: logger.debug(f"Attempting to delete branch: {branch_name}") - logger.debug(f"Attempting to delete branch from repo at path: {repo_path}") + logger.debug( + f"Attempting to delete branch from repo at path: {repo_path}") repo = git.Repo(repo_path) # Fetch updates from remote @@ -29,15 +31,24 @@ def delete_branch(repo_path, branch_name): repo.delete_head(branch_name, force=True) logger.debug(f"Local branch {branch_name} deleted") - # Attempt to delete remote branch - try: + # Check if remote branch exists + remote_branch = f"origin/{branch_name}" + if remote_branch in repo.refs: logger.debug(f"Attempting to delete remote branch: {branch_name}") - repo.git.push('origin', '--delete', branch_name) - logger.debug(f"Successfully deleted remote branch: {branch_name}") - return True, {"message": f"Deleted branch: {branch_name}", "current_branch": repo.active_branch.name} - except git.GitCommandError as e: - logger.error(f"Failed to delete remote branch: {branch_name}. Error: {str(e)}") - return False, f"Failed to delete remote branch: {branch_name}. It may not exist or there might be permission issues." + try: + repo.git.push('origin', '--delete', branch_name) + logger.debug( + f"Successfully deleted remote branch: {branch_name}") + except GitCommandError as e: + logger.error( + f"Failed to delete remote branch: {branch_name}. Error: {str(e)}" + ) + return False, f"Failed to delete remote branch: {branch_name}. There might be permission issues." + + return True, { + "message": f"Deleted branch: {branch_name}", + "current_branch": repo.active_branch.name + } except Exception as e: logger.error(f"Error deleting branch: {str(e)}", exc_info=True) diff --git a/frontend/src/components/settings/SettingsBranchModal.jsx b/frontend/src/components/settings/SettingsBranchModal.jsx index 51ab471..995f8bc 100644 --- a/frontend/src/components/settings/SettingsBranchModal.jsx +++ b/frontend/src/components/settings/SettingsBranchModal.jsx @@ -1,352 +1,432 @@ -import React, { useState, useEffect } from "react"; -import Modal from "../ui/Modal"; +import React, {useState, useEffect} from 'react'; +import Modal from '../ui/Modal'; import { - getBranches, - checkoutBranch, - createBranch, - deleteBranch, - pushBranchToRemote, -} from "../../api/api"; + getBranches, + checkoutBranch, + createBranch, + deleteBranch, + pushBranchToRemote +} from '../../api/api'; import { - ExternalLink, - Trash2, - GitBranchPlus, - ArrowRightCircle, - Loader, - CloudUpload, -} from "lucide-react"; -import Tooltip from "../ui/Tooltip"; -import Alert from "../ui/Alert"; + ExternalLink, + Trash2, + GitBranchPlus, + ArrowRightCircle, + Loader, + CloudUpload +} from 'lucide-react'; +import Tooltip from '../ui/Tooltip'; +import Alert from '../ui/Alert'; const SettingsBranchModal = ({ - isOpen, - onClose, - repoUrl, - currentBranch, - onBranchChange, + isOpen, + onClose, + repoUrl, + currentBranch, + onBranchChange, + isDevMode }) => { - const [branches, setBranches] = useState([]); - const [branchOffMode, setBranchOffMode] = useState(null); - const [newBranchName, setNewBranchName] = useState(""); - const [validBranchName, setValidBranchName] = useState(true); - const [branchToDelete, setBranchToDelete] = useState(null); - const [loadingAction, setLoadingAction] = useState(""); + const [branches, setBranches] = useState([]); + const [branchOffMode, setBranchOffMode] = useState(null); + const [newBranchName, setNewBranchName] = useState(''); + const [validBranchName, setValidBranchName] = useState(true); + const [branchToDelete, setBranchToDelete] = useState(null); + const [loadingAction, setLoadingAction] = useState(''); - useEffect(() => { - if (isOpen) { - fetchBranches(); - resetForm(); - } - }, [isOpen]); - - const fetchBranches = async () => { - try { - const response = await getBranches(); - if (response.success && response.data.branches) { - setBranches(response.data.branches); - } else { - console.error("Error fetching branches:", response.data.error); - } - } catch (error) { - console.error("Error fetching branches:", error); - } - }; - - const resetForm = () => { - setBranchOffMode(null); - setNewBranchName(""); - setValidBranchName(true); - setBranchToDelete(null); - setLoadingAction(""); - }; - - const handleCheckout = async (branchName) => { - setLoadingAction(`checkout-${branchName}`); - try { - const response = await checkoutBranch(branchName); - if (response.success) { - await fetchBranches(); - onBranchChange(); - Alert.success("Branch checked out successfully"); - onClose(); - } else { - Alert.error(response.error); - } - } catch (error) { - if (error.response && error.response.status === 400 && error.response.data.error) { - Alert.error(error.response.data.error); - } else { - Alert.error("An unexpected error occurred while checking out the branch."); - console.error("Error checking out branch:", error); - } - } finally { - setLoadingAction(""); - } - }; - - const handleBranchOff = async () => { - setLoadingAction("branchOff"); - if (newBranchName && validBranchName) { - try { - const response = await createBranch(newBranchName, branchOffMode); - if (response.success) { - await fetchBranches(); - onBranchChange(); - Alert.success("Branch created successfully"); - resetForm(); - } else { - Alert.error(response.error); + useEffect(() => { + if (isOpen) { + fetchBranches(); + resetForm(); } - } catch (error) { - if (error.response && error.response.status === 400 && error.response.data.error) { - Alert.error(error.response.data.error); - } else { - console.error("Error branching off:", error); - Alert.error("An unexpected error occurred while creating the branch. Please try again."); + }, [isOpen]); + + const fetchBranches = async () => { + try { + const response = await getBranches(); + if (response.success && response.data.branches) { + setBranches(response.data.branches); + } else { + console.error('Error fetching branches:', response.data.error); + } + } catch (error) { + console.error('Error fetching branches:', error); } - } finally { - setLoadingAction(""); - } - } else { - Alert.error("Please enter a valid branch name."); - } - }; + }; - const handleBranchOffClick = (branchName) => { - setBranchOffMode(branchName); - setNewBranchName(""); - setValidBranchName(true); - }; - - const validateBranchName = (name) => { - const isValid = /^[a-zA-Z0-9._-]+$/.test(name); - setValidBranchName(isValid); - setNewBranchName(name); - }; - - const handleOpenInGitHub = (branchName) => { - const branchUrl = `${repoUrl}/tree/${encodeURIComponent(branchName)}`; - window.open(branchUrl, "_blank"); - }; - - const confirmDeleteBranch = (branchName) => { - setBranchToDelete(branchName); - }; - - const handleDeleteBranch = async () => { - if (branchToDelete && branchToDelete.toLowerCase() === "main") { - Alert.warning("The 'main' branch cannot be deleted."); - return; - } - setLoadingAction(`delete-${branchToDelete}`); - try { - const response = await deleteBranch(branchToDelete); - if (response.success) { - onBranchChange(); - await fetchBranches(); - Alert.success(`Branch '${branchToDelete}' deleted successfully`); + const resetForm = () => { + setBranchOffMode(null); + setNewBranchName(''); + setValidBranchName(true); setBranchToDelete(null); - } else { - Alert.error(response.error); - } - } catch (error) { - Alert.error("An unexpected error occurred while deleting the branch."); - console.error("Error deleting branch:", error); - } finally { - setLoadingAction(""); - } - }; + setLoadingAction(''); + }; - const handlePushToRemote = async (branchName) => { - setLoadingAction(`push-${branchName}`); - try { - const response = await pushBranchToRemote(branchName); - if (response.success) { - Alert.success(`Branch '${branchName}' pushed to remote successfully`); - await fetchBranches(); - } else { - Alert.error(response.error); - } - } catch (error) { - Alert.error("An unexpected error occurred while pushing the branch to remote."); - console.error("Error pushing branch to remote:", error); - } finally { - setLoadingAction(""); - } - }; + const handleCheckout = async branchName => { + setLoadingAction(`checkout-${branchName}`); + try { + const response = await checkoutBranch(branchName); + if (response.success) { + await fetchBranches(); + onBranchChange(); + Alert.success('Branch checked out successfully'); + onClose(); + } else { + Alert.error(response.error); + } + } catch (error) { + if ( + error.response && + error.response.status === 400 && + error.response.data.error + ) { + Alert.error(error.response.data.error); + } else { + Alert.error( + 'An unexpected error occurred while checking out the branch.' + ); + console.error('Error checking out branch:', error); + } + } finally { + setLoadingAction(''); + } + }; - return ( - -
-
-

- Branches -

-
    - {branches.map((branch, index) => ( -
  • -
    -
    - - {branch.name || "Unknown Branch"} - - - {branch.isLocal && !branch.isRemote - ? "(Local)" - : !branch.isLocal && branch.isRemote - ? "(Remote)" - : "(Local & Remote)"} - -
    -
    - {branch.name !== currentBranch && ( - - - - )} - - - - {branch.isLocal && !branch.isRemote && ( - - - - )} - {branch.isLocal && branch.name !== currentBranch && branch.name.toLowerCase() !== "main" && ( - - - - )} - - - -
    -
  • - ))} -
-
- {branchOffMode && ( -
-

- Create New Branch -

-
- validateBranchName(e.target.value)} - placeholder={`New branch from ${branchOffMode}`} - className={`flex-grow p-2 border rounded bg-gray-800 text-gray-300 focus:ring-2 focus:ring-blue-500 transition-all ${ - !validBranchName ? "border-red-500" : "border-gray-600" - }`} - /> - + } catch (error) { + if ( + error.response && + error.response.status === 400 && + error.response.data.error + ) { + Alert.error(error.response.data.error); + } else { + console.error('Error branching off:', error); + Alert.error( + 'An unexpected error occurred while creating the branch. Please try again.' + ); + } + } finally { + setLoadingAction(''); + } + } else { + Alert.error('Please enter a valid branch name.'); + } + }; + + const handleBranchOffClick = branchName => { + setBranchOffMode(branchName); + setNewBranchName(''); + setValidBranchName(true); + }; + + const validateBranchName = name => { + const isValid = /^[a-zA-Z0-9._-]+$/.test(name); + setValidBranchName(isValid); + setNewBranchName(name); + }; + + const handleOpenInGitHub = branchName => { + const branchUrl = `${repoUrl}/tree/${encodeURIComponent(branchName)}`; + window.open(branchUrl, '_blank'); + }; + + const confirmDeleteBranch = branchName => { + setBranchToDelete(branchName); + }; + + const handleDeleteBranch = async () => { + if (branchToDelete && branchToDelete.toLowerCase() === 'main') { + Alert.warning("The 'main' branch cannot be deleted."); + return; + } + setLoadingAction(`delete-${branchToDelete}`); + try { + const response = await deleteBranch(branchToDelete); + if (response.success) { + onBranchChange(); + await fetchBranches(); + Alert.success( + `Branch '${branchToDelete}' deleted successfully` + ); + setBranchToDelete(null); + } else { + Alert.error(response.error); + } + } catch (error) { + Alert.error( + 'An unexpected error occurred while deleting the branch.' + ); + console.error('Error deleting branch:', error); + } finally { + setLoadingAction(''); + } + }; + + const handlePushToRemote = async branchName => { + setLoadingAction(`push-${branchName}`); + try { + const response = await pushBranchToRemote(branchName); + if (response.success) { + Alert.success( + `Branch '${branchName}' pushed to remote successfully` + ); + await fetchBranches(); + } else { + Alert.error(response.error); + } + } catch (error) { + Alert.error( + 'An unexpected error occurred while pushing the branch to remote.' + ); + console.error('Error pushing branch to remote:', error); + } finally { + setLoadingAction(''); + } + }; + + return ( + +
+
+

+ Branches +

+
    + {branches.map((branch, index) => ( +
  • +
    +
    + + {branch.name || 'Unknown Branch'} + + + {branch.isLocal && !branch.isRemote + ? '(Local)' + : !branch.isLocal && branch.isRemote + ? '(Remote)' + : '(Local & Remote)'} + +
    +
    + {branch.name !== currentBranch && ( + + + + )} + + + + {branch.isLocal && + !branch.isRemote && + isDevMode && ( + + + + )} + {(branch.isLocal || + (branch.isRemote && isDevMode)) && + branch.name !== currentBranch && + branch.name.toLowerCase() !== + 'stable' && ( + + + + )} + + {branch.isRemote && ( + + + + )} +
    +
  • + ))} +
+
+ {branchOffMode && ( +
+

+ Create New Branch +

+
+ + validateBranchName(e.target.value) + } + placeholder={`New branch from ${branchOffMode}`} + className={`flex-grow p-2 border rounded bg-gray-800 text-gray-300 focus:ring-2 focus:ring-blue-500 transition-all ${ + !validBranchName + ? 'border-red-500' + : 'border-gray-600' + }`} + /> + +
+
+ )} + {branchToDelete && ( +
+

+ Are you sure you want to delete the branch{' '} + + {branchToDelete} + + ? This action cannot be undone. +

+
+ + +
+
+ )}
-
- )} - {branchToDelete && ( -
-

- Are you sure you want to delete the branch{" "} - {branchToDelete}? This - action cannot be undone. -

-
- - -
-
- )} -
- - ); + + ); }; -export default SettingsBranchModal; \ No newline at end of file +export default SettingsBranchModal; diff --git a/frontend/src/components/settings/SettingsPage.jsx b/frontend/src/components/settings/SettingsPage.jsx index 127eecd..ca88330 100644 --- a/frontend/src/components/settings/SettingsPage.jsx +++ b/frontend/src/components/settings/SettingsPage.jsx @@ -872,6 +872,7 @@ const SettingsPage = () => { type={currentChange.type} name={currentChange.name} commitMessage={currentChange.commit_message} + isDevMode={isDevMode} /> )}