Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commitc922868unknown_key

feat(M11): create branch from issue — one-click branch from issue detail

feat(M11): create branch from issue — one-click branch from issue detail

Adds POST /:owner/:repo/issues/:number/branch (write-access gated).
Pre-fills branch name as issue-<N>-<title-slug>; user can override.
Uses existing updateRef + resolveRef git plumbing — zero new DB tables.
"Create branch" details-dropdown button appears on open issues for
authenticated users with write access, next to the "Build with AI" CTA.

https://claude.ai/code/session_01ACsT2Pc8GRoZwZRF8SK68Y
Claude committed on May 28, 2026Parent: 534afdd
1 file changed+1030c922868d7e603f10680c3bde331dbc7fc12f7d23
1 changed file+103−0
Modifiedsrc/routes/issues.tsx+103−0View fileUnifiedSplit
5555 CommentForm,
5656 formatRelative,
5757} from "../views/ui";
58import { getDefaultBranch, resolveRef, updateRef } from "../git/repository";
5859
5960const issueRoutes = new Hono<AuthEnv>();
6061
14421443 Build with AI
14431444 </a>
14441445 )}
1446 {issue.state === "open" && user && (
1447 <details class="issue-branch-dropdown" style="position:relative;display:inline-block">
1448 <summary
1449 class="btn"
1450 style="font-size:13px;padding:6px 12px;cursor:pointer;list-style:none"
1451 title="Create a new branch for this issue"
1452 >
1453 Create branch
1454 </summary>
1455 <div style="position:absolute;right:0;top:calc(100% + 4px);background:var(--bg-elevated);border:1px solid var(--border);border-radius:8px;padding:12px 14px;min-width:280px;z-index:100;box-shadow:0 4px 12px rgba(0,0,0,.3)">
1456 <form method="post" action={`/${ownerName}/${repoName}/issues/${issue.number}/branch`}>
1457 <label style="display:block;font-size:12px;color:var(--fg-muted);margin-bottom:4px">Branch name</label>
1458 <input
1459 type="text"
1460 name="branchName"
1461 class="input"
1462 style="width:100%;font-size:13px;padding:5px 8px;margin-bottom:8px"
1463 value={`issue-${issue.number}-${issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40)}`}
1464 pattern="[a-zA-Z0-9._\\-/]+"
1465 required
1466 />
1467 <button type="submit" class="btn btn-primary" style="width:100%;font-size:13px">
1468 Create branch
1469 </button>
1470 </form>
1471 </div>
1472 </details>
1473 )}
14451474 </div>
14461475 </section>
14471476
18171846 }
18181847);
18191848
1849// ─── Create branch from issue ─────────────────────────────────────────────────
1850// POST /:owner/:repo/issues/:number/branch
1851// Creates a new git branch from the repo default branch, pre-named after the
1852// issue. Write access required. Zero-config — no new DB tables.
1853
1854issueRoutes.post(
1855 "/:owner/:repo/issues/:number/branch",
1856 softAuth,
1857 requireAuth,
1858 requireRepoAccess("write"),
1859 async (c) => {
1860 const { owner: ownerName, repo: repoName } = c.req.param();
1861 const issueNum = parseInt(c.req.param("number"), 10);
1862
1863 const resolved = await resolveRepo(ownerName, repoName);
1864 if (!resolved) return c.redirect(`/${ownerName}/${repoName}/issues`);
1865
1866 const [issue] = await db
1867 .select({ id: issues.id, number: issues.number, title: issues.title })
1868 .from(issues)
1869 .where(
1870 and(
1871 eq(issues.repositoryId, resolved.repo.id),
1872 eq(issues.number, issueNum)
1873 )
1874 )
1875 .limit(1);
1876
1877 if (!issue) {
1878 return c.redirect(`/${ownerName}/${repoName}/issues`);
1879 }
1880
1881 const body = await c.req.formData().catch(() => null);
1882 const rawName = (body?.get("branchName") as string | null)?.trim();
1883
1884 // Derive a slug from the issue title
1885 const titleSlug = issue.title
1886 .toLowerCase()
1887 .replace(/[^a-z0-9]+/g, "-")
1888 .replace(/^-+|-+$/g, "")
1889 .slice(0, 40);
1890 const suggested = `issue-${issue.number}-${titleSlug}`;
1891 const branchName = rawName && /^[a-zA-Z0-9._\-/]+$/.test(rawName)
1892 ? rawName
1893 : suggested;
1894
1895 const defaultBranch = (await getDefaultBranch(ownerName, repoName)) ?? "main";
1896 const sha = await resolveRef(ownerName, repoName, defaultBranch);
1897
1898 if (!sha) {
1899 return c.redirect(
1900 `/${ownerName}/${repoName}/issues/${issueNum}?info=${encodeURIComponent(
1901 "Cannot create branch: repository has no commits yet."
1902 )}`
1903 );
1904 }
1905
1906 const ok = await updateRef(ownerName, repoName, `refs/heads/${branchName}`, sha);
1907 if (!ok) {
1908 return c.redirect(
1909 `/${ownerName}/${repoName}/issues/${issueNum}?info=${encodeURIComponent(
1910 `Failed to create branch '${branchName}'. It may already exist.`
1911 )}`
1912 );
1913 }
1914
1915 return c.redirect(
1916 `/${ownerName}/${repoName}/tree/${branchName}?info=${encodeURIComponent(
1917 `Branch '${branchName}' created from ${defaultBranch}.`
1918 )}`
1919 );
1920 }
1921);
1922
18201923export default issueRoutes;
18211924export { IssueNav };
18221925