Skip to content

Commit afe2ccd

Browse files
committed
feat(BranchController, GitService, RepoSettingsModal): add default branch management
- Add setDefaultBranch endpoint to allow users to change repository default branch - Implement branch verification before updating HEAD reference in GitService - Add PUT route for /api/repos/:name/default-branch in repoRoutes - Integrate default branch selector in RepoSettingsModal with branch store - Add CodeBracketIcon import for branch settings UI - Refactor RepoSettingsModal layout with improved spacing and icon sizing - Update tab button styling for better visual hierarchy and consistency - Reduce modal minimum height from 500px to 400px for better UX - Add error handling for invalid or non-existent branches
1 parent 35d5e1f commit afe2ccd

4 files changed

Lines changed: 169 additions & 32 deletions

File tree

apps/backend/src/controllers/BranchController.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,43 @@ export class BranchController {
169169
}
170170
}
171171
}
172+
173+
async setDefaultBranch(req: AuthRequest, res: Response): Promise<void> {
174+
try {
175+
if (!req.username) {
176+
res.status(401).json({error: "Unauthorized"});
177+
return;
178+
}
179+
180+
const repoName = req.params.name;
181+
if (!isSingleParam(repoName)) {
182+
res.status(400).json({error: "Invalid repo name"});
183+
return;
184+
}
185+
186+
if (!repoService.repoExists(req.username, repoName)) {
187+
res.status(404).json({error: "Repo not found"});
188+
return;
189+
}
190+
191+
const {branch} = req.body as {branch?: string};
192+
if (!branch || !branch.trim()) {
193+
res.status(400).json({error: "branch is required"});
194+
return;
195+
}
196+
197+
await gitService.setDefaultBranch(req.username, repoName, branch.trim());
198+
res.json({message: "Default branch updated", branch: branch.trim()});
199+
} catch (err: any) {
200+
console.error("PUT /api/repos/:name/default-branch error:", err);
201+
if (
202+
err?.message?.includes("not a valid object name") ||
203+
err?.message?.includes("not a valid ref")
204+
) {
205+
res.status(400).json({error: "Branch does not exist"});
206+
} else {
207+
res.status(500).json({error: "Internal server error"});
208+
}
209+
}
210+
}
172211
}

apps/backend/src/routes/repoRoutes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ router.post("/:name/branches", (req, res) =>
5151
router.delete("/:name/branches/:branch", (req, res) =>
5252
branchController.deleteBranch(req, res),
5353
);
54+
router.put("/:name/default-branch", (req, res) =>
55+
branchController.setDefaultBranch(req, res),
56+
);
5457
router.get("/:name/current-branch", (req, res) =>
5558
branchController.getCurrentBranch(req, res),
5659
);

apps/backend/src/services/GitService.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,19 @@ export class GitService {
345345
throw err;
346346
}
347347
}
348+
349+
async setDefaultBranch(
350+
username: string,
351+
repoName: string,
352+
branchName: string,
353+
): Promise<void> {
354+
const repoPath = this.getRepoPath(username, repoName);
355+
const git = this.getGitInstance(repoPath);
356+
357+
// Verify branch exists
358+
await git.raw(["rev-parse", "--verify", branchName]);
359+
360+
// Update HEAD to point to the new default branch
361+
await git.raw(["symbolic-ref", "HEAD", `refs/heads/${branchName}`]);
362+
}
348363
}

apps/frontend/src/pages/repo/components/RepoSettingsModal.tsx

Lines changed: 112 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Cog6ToothIcon,
55
ShieldCheckIcon,
66
KeyIcon,
7+
CodeBracketIcon,
78
} from "@heroicons/react/24/outline";
89
import axios from "axios";
910
import React, {useEffect, useMemo, useState} from "react";
@@ -12,6 +13,7 @@ import {Alert} from "~/components/ui/Alert";
1213
import {Button} from "~/components/ui/Button";
1314
import {Input} from "~/components/ui/Input";
1415
import {Modal} from "~/components/ui/Modal";
16+
import {useBranchStore} from "~/stores/branchStore";
1517

1618
interface RepoMetadata {
1719
title?: string;
@@ -77,6 +79,11 @@ export const RepoSettingsModal: React.FC<RepoSettingsModalProps> = ({
7779
const [isDeleting, setIsDeleting] = useState(false);
7880
const [error, setError] = useState<string | null>(null);
7981

82+
// Default branch
83+
const {branches, currentBranch, fetchBranches} = useBranchStore();
84+
const [selectedDefaultBranch, setSelectedDefaultBranch] = useState("");
85+
const [isSavingBranch, setIsSavingBranch] = useState(false);
86+
8087
useEffect(() => {
8188
if (!isOpen) return;
8289

@@ -86,13 +93,14 @@ export const RepoSettingsModal: React.FC<RepoSettingsModalProps> = ({
8693
setTitle(repoMetadata?.title ?? "");
8794
setDescription(repoMetadata?.description ?? "");
8895
setNewRepoName(repoNameWithoutGit);
96+
setSelectedDefaultBranch(currentBranch || "");
8997

9098
// reset inline confirmations
9199
setRenameConfirmText("");
92100
setArchiveConfirmChecked(false);
93101
setDeleteExpanded(false);
94102
setDeleteConfirmText("");
95-
}, [isOpen, repoMetadata, repoNameWithoutGit]);
103+
}, [isOpen, repoMetadata, repoNameWithoutGit, currentBranch]);
96104

97105
const closeAndReset = () => {
98106
setError(null);
@@ -137,6 +145,26 @@ export const RepoSettingsModal: React.FC<RepoSettingsModalProps> = ({
137145
}
138146
};
139147

148+
const handleSetDefaultBranch = async () => {
149+
if (!selectedDefaultBranch || selectedDefaultBranch === currentBranch)
150+
return;
151+
152+
setError(null);
153+
setIsSavingBranch(true);
154+
try {
155+
await axios.put(
156+
`/api/repos/${encodeURIComponent(repoNameWithGit)}/default-branch`,
157+
{branch: selectedDefaultBranch},
158+
);
159+
// Refresh branches to reflect the change
160+
await fetchBranches(repoName);
161+
} catch (err: any) {
162+
setError(err?.response?.data?.error || "Failed to update default branch");
163+
} finally {
164+
setIsSavingBranch(false);
165+
}
166+
};
167+
140168
const handleRename = async () => {
141169
const trimmed = newRepoName.trim();
142170

@@ -222,7 +250,7 @@ export const RepoSettingsModal: React.FC<RepoSettingsModalProps> = ({
222250
) => (
223251
<button
224252
onClick={() => setActiveTab(key)}
225-
className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium transition-colors border-l-2 ${
253+
className={`w-full flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors border-l-2 ${
226254
activeTab === key
227255
? variant === "danger"
228256
? "bg-error/10 text-error border-error"
@@ -254,29 +282,29 @@ export const RepoSettingsModal: React.FC<RepoSettingsModalProps> = ({
254282

255283
<div
256284
data-testid="repo-settings-shell"
257-
className="flex flex-col md:flex-row h-full min-h-[500px] border-t border-app-border -mx-6"
285+
className="flex flex-col md:flex-row h-full min-h-[400px] border-t border-app-border -mx-6"
258286
>
259287
{/* Left Sidebar */}
260288
<nav
261289
data-testid="repo-settings-nav"
262-
className="w-full md:w-64 bg-app-surface border-b md:border-b-0 md:border-r border-app-border flex-shrink-0"
290+
className="w-full md:w-56 bg-app-surface border-b md:border-b-0 md:border-r border-app-border flex-shrink-0"
263291
>
264-
<div className="p-4 md:py-6 space-y-1">
292+
<div className="p-3 md:py-4 space-y-0.5">
265293
{renderTabButton(
266294
"general",
267295
"General",
268-
<Cog6ToothIcon className="w-5 h-5" />,
296+
<Cog6ToothIcon className="w-4 h-4" />,
269297
)}
270298
{renderTabButton(
271299
"access",
272300
"Access",
273-
<ShieldCheckIcon className="w-5 h-5" />,
301+
<ShieldCheckIcon className="w-4 h-4" />,
274302
)}
275-
<div className="h-px bg-app-border my-2 mx-4" />
303+
<div className="h-px bg-app-border my-1.5 mx-3" />
276304
{renderTabButton(
277305
"danger",
278306
"Danger Zone",
279-
<ExclamationTriangleIcon className="w-5 h-5" />,
307+
<ExclamationTriangleIcon className="w-4 h-4" />,
280308
"danger",
281309
)}
282310
</div>
@@ -287,24 +315,24 @@ export const RepoSettingsModal: React.FC<RepoSettingsModalProps> = ({
287315
data-testid="repo-settings-content"
288316
className="flex-1 overflow-y-auto bg-app-surface"
289317
>
290-
<div className="p-6 max-w-3xl mx-auto">
318+
<div className="p-4 max-w-3xl mx-auto">
291319
{activeTab === "general" && (
292-
<div className="space-y-8">
320+
<div className="space-y-5">
293321
<div>
294-
<h3 className="text-xl font-semibold text-text-primary mb-1">
322+
<h3 className="text-lg font-semibold text-text-primary mb-0.5">
295323
General Settings
296324
</h3>
297-
<p className="text-sm text-text-tertiary">
325+
<p className="text-xs text-text-tertiary">
298326
Manage your repository's main configuration.
299327
</p>
300328
</div>
301329

302-
<div className="space-y-6">
330+
<div className="space-y-4">
303331
{/* Metadata Section */}
304-
<section className="bg-app-surface/30 border border-app-border rounded-lg p-5 space-y-5">
305-
<div className="flex items-center gap-2 pb-3 border-b border-app-border">
306-
<PencilIcon className="w-5 h-5 text-app-accent" />
307-
<h4 className="font-medium text-text-primary">
332+
<section className="bg-app-surface/30 border border-app-border rounded-lg p-4 space-y-4">
333+
<div className="flex items-center gap-2 pb-2 border-b border-app-border">
334+
<PencilIcon className="w-4 h-4 text-app-accent" />
335+
<h4 className="text-sm font-medium text-text-primary">
308336
Repository Details
309337
</h4>
310338
</div>
@@ -354,10 +382,10 @@ export const RepoSettingsModal: React.FC<RepoSettingsModalProps> = ({
354382
</section>
355383

356384
{/* Rename Section */}
357-
<section className="bg-app-surface/30 border border-app-border rounded-lg p-5 space-y-5">
358-
<div className="flex items-center gap-2 pb-3 border-b border-app-border">
359-
<KeyIcon className="w-5 h-5 text-text-primary" />
360-
<h4 className="font-medium text-text-primary">
385+
<section className="bg-app-surface/30 border border-app-border rounded-lg p-4 space-y-4">
386+
<div className="flex items-center gap-2 pb-2 border-b border-app-border">
387+
<KeyIcon className="w-4 h-4 text-text-primary" />
388+
<h4 className="text-sm font-medium text-text-primary">
361389
Rename Repository
362390
</h4>
363391
</div>
@@ -407,20 +435,72 @@ export const RepoSettingsModal: React.FC<RepoSettingsModalProps> = ({
407435
)}
408436
</div>
409437
</section>
438+
439+
{/* Default Branch Section */}
440+
<section className="bg-app-surface/30 border border-app-border rounded-lg p-4 space-y-4">
441+
<div className="flex items-center gap-2 pb-2 border-b border-app-border">
442+
<CodeBracketIcon className="w-4 h-4 text-app-accent" />
443+
<h4 className="text-sm font-medium text-text-primary">
444+
Default Branch
445+
</h4>
446+
</div>
447+
448+
<div className="space-y-3">
449+
<p className="text-xs text-text-tertiary">
450+
The default branch is used when viewing files and
451+
creating new branches.
452+
</p>
453+
454+
<div>
455+
<label className="block text-sm font-medium text-text-primary mb-1.5">
456+
Default Branch
457+
</label>
458+
<select
459+
value={selectedDefaultBranch}
460+
onChange={(e) =>
461+
setSelectedDefaultBranch(e.target.value)
462+
}
463+
className="w-full h-9 px-3 bg-app-surface border border-app-border rounded text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-app-accent focus:border-app-accent transition-colors"
464+
>
465+
{branches.map((b: string) => (
466+
<option key={b} value={b}>
467+
{b}
468+
{b === currentBranch ? " (current default)" : ""}
469+
</option>
470+
))}
471+
</select>
472+
</div>
473+
474+
<div className="pt-2 flex justify-end">
475+
<Button
476+
onClick={handleSetDefaultBranch}
477+
disabled={
478+
isSavingBranch ||
479+
!selectedDefaultBranch ||
480+
selectedDefaultBranch === currentBranch
481+
}
482+
>
483+
{isSavingBranch
484+
? "Updating..."
485+
: "Update Default Branch"}
486+
</Button>
487+
</div>
488+
</div>
489+
</section>
410490
</div>
411491
</div>
412492
)}
413493

414494
{activeTab === "access" && (
415-
<div className="flex flex-col items-center justify-center h-full text-center py-12 space-y-4">
416-
<div className="w-16 h-16 rounded-full bg-app-surface flex items-center justify-center">
417-
<ShieldCheckIcon className="w-8 h-8 text-text-tertiary" />
495+
<div className="flex flex-col items-center justify-center h-full text-center py-8 space-y-3">
496+
<div className="w-12 h-12 rounded-full bg-app-surface flex items-center justify-center">
497+
<ShieldCheckIcon className="w-6 h-6 text-text-tertiary" />
418498
</div>
419499
<div>
420-
<h3 className="text-lg font-medium text-text-primary">
500+
<h3 className="text-base font-medium text-text-primary">
421501
Access Control
422502
</h3>
423-
<p className="text-text-tertiary max-w-sm mt-2">
503+
<p className="text-xs text-text-tertiary max-w-sm mt-1">
424504
Manage collaborators, teams, and deploy keys. This feature
425505
is currently under development.
426506
</p>
@@ -429,19 +509,19 @@ export const RepoSettingsModal: React.FC<RepoSettingsModalProps> = ({
429509
)}
430510

431511
{activeTab === "danger" && (
432-
<div className="space-y-8">
512+
<div className="space-y-5">
433513
<div>
434-
<h3 className="text-xl font-semibold text-error mb-1">
514+
<h3 className="text-lg font-semibold text-error mb-0.5">
435515
Danger Zone
436516
</h3>
437-
<p className="text-sm text-text-tertiary">
517+
<p className="text-xs text-text-tertiary">
438518
Destructive actions that affect your repository.
439519
</p>
440520
</div>
441521

442522
<div className="border border-error/30 rounded-lg overflow-hidden divide-y divide-error/30">
443523
{/* Archive Row */}
444-
<div className="p-5 bg-error/5 flex flex-col sm:flex-row sm:items-start justify-between gap-4">
524+
<div className="p-4 bg-error/5 flex flex-col sm:flex-row sm:items-start justify-between gap-3">
445525
<div className="space-y-1">
446526
<h4 className="text-sm font-medium text-text-primary">
447527
{isArchived
@@ -486,7 +566,7 @@ export const RepoSettingsModal: React.FC<RepoSettingsModalProps> = ({
486566
</div>
487567

488568
{/* Delete Row */}
489-
<div className="p-5 bg-error/10 flex flex-col sm:flex-row sm:items-start justify-between gap-4">
569+
<div className="p-4 bg-error/10 flex flex-col sm:flex-row sm:items-start justify-between gap-3">
490570
<div className="space-y-1 flex-1">
491571
<h4 className="text-sm font-medium text-text-primary">
492572
Delete this repository

0 commit comments

Comments
 (0)