fix: delete and checkout now work properly - checkout local only / remote only branches - delete local only / not remote branches if !dev mode

This commit is contained in:
Sam Chau
2024-09-08 01:57:04 +09:30
parent 450fcb4973
commit 15cd66a775
4 changed files with 456 additions and 353 deletions

View File

@@ -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)}"}
return False, {"error": f"Error checking out branch: {str(e)}"}

View File

@@ -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)

View File

@@ -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 (
<Modal isOpen={isOpen} onClose={onClose} title="Manage Git Branches">
<div className="space-y-6 text-sm">
<div className="bg-gray-800 rounded-lg p-4 shadow-inner">
<h3 className="text-lg font-semibold mb-3 text-gray-100 border-b border-gray-700 pb-2">
Branches
</h3>
<ul className="space-y-3">
{branches.map((branch, index) => (
<li
key={index}
className={`flex items-center justify-between p-3 rounded-md transition-all duration-200 ${
branch.name === currentBranch
? "bg-blue-900/30 border border-blue-500 shadow-md"
: "bg-gray-700 hover:bg-gray-650"
}`}
>
<div className="flex items-center space-x-3">
<div
className={`w-2 h-2 rounded-full ${
branch.name === currentBranch
? "bg-blue-400"
: "bg-gray-400"
}`}
></div>
<span
className={`font-medium ${
branch.name === currentBranch
? "text-blue-200"
: "text-gray-200"
}`}
>
{branch.name || "Unknown Branch"}
</span>
<span className="text-xs text-gray-400">
{branch.isLocal && !branch.isRemote
? "(Local)"
: !branch.isLocal && branch.isRemote
? "(Remote)"
: "(Local & Remote)"}
</span>
</div>
<div className="flex items-center space-x-2">
{branch.name !== currentBranch && (
<Tooltip content="Checkout">
<button
onClick={() => handleCheckout(branch.name)}
className="p-1.5 bg-blue-500 text-white rounded-md hover:bg-blue-600 hover:scale-105 transition-all duration-200 shadow-sm"
disabled={loadingAction === `checkout-${branch.name}`}
>
{loadingAction === `checkout-${branch.name}` ? (
<Loader size={16} className="animate-spin" />
) : (
<ArrowRightCircle size={16} />
)}
</button>
</Tooltip>
)}
<Tooltip content="Branch Off">
<button
onClick={() => handleBranchOffClick(branch.name)}
className="p-1.5 bg-green-500 text-white rounded-md hover:bg-green-600 hover:scale-105 transition-all duration-200 shadow-sm"
disabled={loadingAction === "branchOff"}
>
{loadingAction === "branchOff" ? (
<Loader size={16} className="animate-spin" />
) : (
<GitBranchPlus size={16} />
)}
</button>
</Tooltip>
{branch.isLocal && !branch.isRemote && (
<Tooltip content="Push to Remote">
<button
onClick={() => handlePushToRemote(branch.name)}
className="p-1.5 bg-purple-500 text-white rounded-md hover:bg-purple-600 hover:scale-105 transition-all duration-200 shadow-sm"
disabled={loadingAction === `push-${branch.name}`}
>
{loadingAction === `push-${branch.name}` ? (
<Loader size={16} className="animate-spin" />
) : (
<CloudUpload size={16} />
)}
</button>
</Tooltip>
)}
{branch.isLocal && branch.name !== currentBranch && branch.name.toLowerCase() !== "main" && (
<Tooltip content="Delete">
<button
onClick={() => confirmDeleteBranch(branch.name)}
className="p-1.5 bg-red-500 text-white rounded-md hover:bg-red-600 hover:scale-105 transition-all duration-200 shadow-sm"
disabled={loadingAction === `delete-${branch.name}`}
>
{loadingAction === `delete-${branch.name}` ? (
<Loader size={16} className="animate-spin" />
) : (
<Trash2 size={16} />
)}
</button>
</Tooltip>
)}
<Tooltip content="View on GitHub">
<button
onClick={() => handleOpenInGitHub(branch.name)}
className="p-1.5 bg-gray-600 text-white rounded-md hover:bg-gray-500 hover:scale-105 transition-all duration-200 shadow-sm"
>
<ExternalLink size={16} />
</button>
</Tooltip>
</div>
</li>
))}
</ul>
</div>
{branchOffMode && (
<div className="bg-gray-700 p-4 rounded-lg shadow-md">
<h4 className="text-sm font-semibold mb-2 text-gray-200">
Create New Branch
</h4>
<div className="flex items-center space-x-2">
<input
type="text"
value={newBranchName}
onChange={(e) => 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"
}`}
/>
<button
onClick={handleBranchOff}
disabled={
!newBranchName ||
!validBranchName ||
loadingAction === "branchOff"
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);
}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm font-medium shadow-sm"
>
{loadingAction === "branchOff" ? "Creating..." : "Create"}
</button>
} 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 (
<Modal isOpen={isOpen} onClose={onClose} title='Manage Git Branches'>
<div className='space-y-6 text-sm'>
<div className='bg-gray-800 rounded-lg p-4 shadow-inner'>
<h3 className='text-lg font-semibold mb-3 text-gray-100 border-b border-gray-700 pb-2'>
Branches
</h3>
<ul className='space-y-3'>
{branches.map((branch, index) => (
<li
key={index}
className={`flex items-center justify-between p-3 rounded-md transition-all duration-200 ${
branch.name === currentBranch
? 'bg-blue-900/30 border border-blue-500 shadow-md'
: 'bg-gray-700 hover:bg-gray-650'
}`}>
<div className='flex items-center space-x-3'>
<div
className={`w-2 h-2 rounded-full ${
branch.name === currentBranch
? 'bg-blue-400'
: 'bg-gray-400'
}`}></div>
<span
className={`font-medium ${
branch.name === currentBranch
? 'text-blue-200'
: 'text-gray-200'
}`}>
{branch.name || 'Unknown Branch'}
</span>
<span className='text-xs text-gray-400'>
{branch.isLocal && !branch.isRemote
? '(Local)'
: !branch.isLocal && branch.isRemote
? '(Remote)'
: '(Local & Remote)'}
</span>
</div>
<div className='flex items-center space-x-2'>
{branch.name !== currentBranch && (
<Tooltip content='Checkout'>
<button
onClick={() =>
handleCheckout(branch.name)
}
className='p-1.5 bg-blue-500 text-white rounded-md hover:bg-blue-600 hover:scale-105 transition-all duration-200 shadow-sm'
disabled={
loadingAction ===
`checkout-${branch.name}`
}>
{loadingAction ===
`checkout-${branch.name}` ? (
<Loader
size={16}
className='animate-spin'
/>
) : (
<ArrowRightCircle
size={16}
/>
)}
</button>
</Tooltip>
)}
<Tooltip content='Branch Off'>
<button
onClick={() =>
handleBranchOffClick(
branch.name
)
}
className='p-1.5 bg-green-500 text-white rounded-md hover:bg-green-600 hover:scale-105 transition-all duration-200 shadow-sm'
disabled={
loadingAction === 'branchOff'
}>
{loadingAction === 'branchOff' ? (
<Loader
size={16}
className='animate-spin'
/>
) : (
<GitBranchPlus size={16} />
)}
</button>
</Tooltip>
{branch.isLocal &&
!branch.isRemote &&
isDevMode && (
<Tooltip content='Push to Remote'>
<button
onClick={() =>
handlePushToRemote(
branch.name
)
}
className='p-1.5 bg-purple-500 text-white rounded-md hover:bg-purple-600 hover:scale-105 transition-all duration-200 shadow-sm'
disabled={
loadingAction ===
`push-${branch.name}`
}>
{loadingAction ===
`push-${branch.name}` ? (
<Loader
size={16}
className='animate-spin'
/>
) : (
<CloudUpload
size={16}
/>
)}
</button>
</Tooltip>
)}
{(branch.isLocal ||
(branch.isRemote && isDevMode)) &&
branch.name !== currentBranch &&
branch.name.toLowerCase() !==
'stable' && (
<Tooltip content='Delete'>
<button
onClick={() =>
confirmDeleteBranch(
branch.name
)
}
className='p-1.5 bg-red-500 text-white rounded-md hover:bg-red-600 hover:scale-105 transition-all duration-200 shadow-sm'
disabled={
loadingAction ===
`delete-${branch.name}`
}>
{loadingAction ===
`delete-${branch.name}` ? (
<Loader
size={16}
className='animate-spin'
/>
) : (
<Trash2 size={16} />
)}
</button>
</Tooltip>
)}
{branch.isRemote && (
<Tooltip content='View on GitHub'>
<button
onClick={() =>
handleOpenInGitHub(
branch.name
)
}
className='p-1.5 bg-gray-600 text-white rounded-md hover:bg-gray-500 hover:scale-105 transition-all duration-200 shadow-sm'>
<ExternalLink size={16} />
</button>
</Tooltip>
)}
</div>
</li>
))}
</ul>
</div>
{branchOffMode && (
<div className='bg-gray-700 p-4 rounded-lg shadow-md'>
<h4 className='text-sm font-semibold mb-2 text-gray-200'>
Create New Branch
</h4>
<div className='flex items-center space-x-2'>
<input
type='text'
value={newBranchName}
onChange={e =>
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'
}`}
/>
<button
onClick={handleBranchOff}
disabled={
!newBranchName ||
!validBranchName ||
loadingAction === 'branchOff'
}
className='px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm font-medium shadow-sm'>
{loadingAction === 'branchOff'
? 'Creating...'
: 'Create'}
</button>
</div>
</div>
)}
{branchToDelete && (
<div className='bg-red-900/30 border border-red-500 rounded-lg p-4 mt-4 text-sm text-gray-200'>
<p className='mb-3'>
Are you sure you want to delete the branch{' '}
<strong className='text-red-300'>
{branchToDelete}
</strong>
? This action cannot be undone.
</p>
<div className='flex space-x-4'>
<button
onClick={handleDeleteBranch}
disabled={
loadingAction === `delete-${branchToDelete}`
}
className='px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm font-medium shadow-sm'>
{loadingAction === `delete-${branchToDelete}`
? 'Deleting...'
: 'Confirm Delete'}
</button>
<button
onClick={() => setBranchToDelete(null)}
className='px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors text-sm font-medium shadow-sm'>
Cancel
</button>
</div>
</div>
)}
</div>
</div>
)}
{branchToDelete && (
<div className="bg-red-900/30 border border-red-500 rounded-lg p-4 mt-4 text-sm text-gray-200">
<p className="mb-3">
Are you sure you want to delete the branch{" "}
<strong className="text-red-300">{branchToDelete}</strong>? This
action cannot be undone.
</p>
<div className="flex space-x-4">
<button
onClick={handleDeleteBranch}
disabled={loadingAction === `delete-${branchToDelete}`}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm font-medium shadow-sm"
>
{loadingAction === `delete-${branchToDelete}`
? "Deleting..."
: "Confirm Delete"}
</button>
<button
onClick={() => setBranchToDelete(null)}
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors text-sm font-medium shadow-sm"
>
Cancel
</button>
</div>
</div>
)}
</div>
</Modal>
);
</Modal>
);
};
export default SettingsBranchModal;
export default SettingsBranchModal;

View File

@@ -872,6 +872,7 @@ const SettingsPage = () => {
type={currentChange.type}
name={currentChange.name}
commitMessage={currentChange.commit_message}
isDevMode={isDevMode}
/>
)}
<LinkRepoModal