Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

editor.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

editor.tsxBlame339 lines · 2 contributors
0074234Claude1/**
2 * Web file editor — create and edit files directly in the browser.
3 */
4
5import { Hono } from "hono";
6import { Layout } from "../views/layout";
7import { RepoHeader, RepoNav, Breadcrumb } from "../views/components";
bb0f894Claude8import {
9 Container,
10 Flex,
11 Form,
12 FormGroup,
13 Input,
14 TextArea,
15 Button,
16 LinkButton,
17 EmptyState,
18 Text,
19} from "../views/ui";
0074234Claude20import {
21 getBlob,
22 getDefaultBranch,
23 getRepoPath,
24 repoExists,
25} from "../git/repository";
26import { softAuth, requireAuth } from "../middleware/auth";
27import type { AuthEnv } from "../middleware/auth";
04f6b7fClaude28import { requireRepoAccess } from "../middleware/repo-access";
0074234Claude29import { join } from "path";
30
31const editor = new Hono<AuthEnv>();
32
33editor.use("*", softAuth);
34
35// New file form
04f6b7fClaude36editor.get("/:owner/:repo/new/:ref{.+$}", requireAuth, requireRepoAccess("write"), async (c) => {
0074234Claude37 const { owner, repo } = c.req.param();
38 const user = c.get("user")!;
39 const refAndPath = c.req.param("ref");
40
41 // Parse ref — use first segment
42 const slashIdx = refAndPath.indexOf("/");
43 const ref = slashIdx === -1 ? refAndPath : refAndPath.slice(0, slashIdx);
44 const dirPath = slashIdx === -1 ? "" : refAndPath.slice(slashIdx + 1);
45
46 return c.html(
47 <Layout title={`New file — ${owner}/${repo}`} user={user}>
48 <RepoHeader owner={owner} repo={repo} />
49 <RepoNav owner={owner} repo={repo} active="code" />
bb0f894Claude50 <Container maxWidth={900}>
0074234Claude51 <h2 style="margin-bottom: 16px">Create new file</h2>
0316dbbClaude52 <Form method="post" action={`/${owner}/${repo}/new/${ref}`}>
0074234Claude53 <input type="hidden" name="dir_path" value={dirPath} />
bb0f894Claude54 <FormGroup label="File path">
55 <Flex align="center" gap={4}>
0074234Claude56 {dirPath && (
bb0f894Claude57 <Text muted size={14}>
0074234Claude58 {dirPath}/
bb0f894Claude59 </Text>
0074234Claude60 )}
bb0f894Claude61 <Input
0074234Claude62 name="filename"
63 required
64 placeholder="filename.ts"
65 style="flex: 1"
66 autocomplete="off"
2228c49copilot-swe-agent[bot]67 aria-label="File path"
0074234Claude68 />
bb0f894Claude69 </Flex>
70 </FormGroup>
71 <FormGroup label="Content">
72 <TextArea
0074234Claude73 name="content"
74 rows={20}
75 placeholder="Enter file content..."
bb0f894Claude76 mono
77 style="line-height: 1.5; tab-size: 2"
0074234Claude78 />
bb0f894Claude79 </FormGroup>
80 <FormGroup label="Commit message">
81 <Input
0074234Claude82 name="message"
83 placeholder="Create new file"
84 required
2228c49copilot-swe-agent[bot]85 aria-label="Commit message"
0074234Claude86 />
bb0f894Claude87 </FormGroup>
88 <Button type="submit" variant="primary">
0074234Claude89 Commit new file
bb0f894Claude90 </Button>
91 </Form>
92 </Container>
0074234Claude93 </Layout>
94 );
95});
96
97// Create file via commit
04f6b7fClaude98editor.post("/:owner/:repo/new/:ref", requireAuth, requireRepoAccess("write"), async (c) => {
0074234Claude99 const { owner, repo } = c.req.param();
100 const user = c.get("user")!;
101 const ref = c.req.param("ref");
102 const body = await c.req.parseBody();
103 const dirPath = String(body.dir_path || "").trim();
104 const filename = String(body.filename || "").trim();
105 const content = String(body.content || "");
106 const message = String(body.message || `Create ${filename}`).trim();
107
108 if (!filename) return c.redirect(`/${owner}/${repo}`);
109
110 const fullPath = dirPath ? `${dirPath}/${filename}` : filename;
111
112 // Use git hash-object + update-index + write-tree + commit-tree
113 const repoDir = getRepoPath(owner, repo);
114
115 const run = async (cmd: string[], cwd: string, stdin?: string) => {
116 const proc = Bun.spawn(cmd, {
117 cwd,
118 stdout: "pipe",
119 stderr: "pipe",
120 stdin: stdin !== undefined ? "pipe" : undefined,
121 });
122 if (stdin !== undefined && proc.stdin) {
123 proc.stdin.write(new TextEncoder().encode(stdin));
124 proc.stdin.end();
125 }
126 const stdout = await new Response(proc.stdout).text();
127 await proc.exited;
128 return stdout.trim();
129 };
130
131 // Hash the new file content
132 const blobSha = await run(
133 ["git", "hash-object", "-w", "--stdin"],
134 repoDir,
135 content
136 );
137
138 // Read current tree
139 const currentTreeSha = await run(
140 ["git", "rev-parse", `${ref}^{tree}`],
141 repoDir
142 );
143
144 // Read current tree and add new entry
145 const treeContent = await run(["git", "ls-tree", "-r", ref], repoDir);
146 const entries = treeContent
147 .split("\n")
148 .filter(Boolean)
149 .map((line) => line + "\n")
150 .join("");
151 const newEntry = `100644 blob ${blobSha}\t${fullPath}\n`;
152
153 const newTreeSha = await run(
154 ["git", "mktree"],
155 repoDir,
156 entries + newEntry
157 );
158
159 // Get parent commit
160 const parentSha = await run(
161 ["git", "rev-parse", ref],
162 repoDir
163 );
164
165 // Create commit
166 const env = {
167 GIT_AUTHOR_NAME: user.displayName || user.username,
168 GIT_AUTHOR_EMAIL: user.email,
169 GIT_COMMITTER_NAME: user.displayName || user.username,
170 GIT_COMMITTER_EMAIL: user.email,
171 };
172
173 const commitProc = Bun.spawn(
174 ["git", "commit-tree", newTreeSha, "-p", parentSha, "-m", message],
175 {
176 cwd: repoDir,
177 stdout: "pipe",
178 stderr: "pipe",
179 env: { ...process.env, ...env },
180 }
181 );
182 const commitSha = (await new Response(commitProc.stdout).text()).trim();
183 await commitProc.exited;
184
185 // Update branch ref
186 await run(
187 ["git", "update-ref", `refs/heads/${ref}`, commitSha],
188 repoDir
189 );
190
191 return c.redirect(`/${owner}/${repo}/blob/${ref}/${fullPath}`);
192});
193
194// Edit file form
04f6b7fClaude195editor.get("/:owner/:repo/edit/:ref{.+$}", requireAuth, requireRepoAccess("write"), async (c) => {
0074234Claude196 const { owner, repo } = c.req.param();
197 const user = c.get("user")!;
198 const refAndPath = c.req.param("ref");
199
200 // Parse ref/path
201 const slashIdx = refAndPath.indexOf("/");
202 if (slashIdx === -1) return c.text("Not found", 404);
203 const ref = refAndPath.slice(0, slashIdx);
204 const filePath = refAndPath.slice(slashIdx + 1);
205
206 const blob = await getBlob(owner, repo, ref, filePath);
207 if (!blob || blob.isBinary) {
208 return c.html(
209 <Layout title="Cannot edit" user={user}>
bb0f894Claude210 <EmptyState title={blob?.isBinary ? "Cannot edit binary file" : "File not found"} />
0074234Claude211 </Layout>,
212 404
213 );
214 }
215
216 return c.html(
217 <Layout title={`Editing ${filePath} — ${owner}/${repo}`} user={user}>
218 <RepoHeader owner={owner} repo={repo} />
219 <RepoNav owner={owner} repo={repo} active="code" />
220 <Breadcrumb owner={owner} repo={repo} ref={ref} path={filePath} />
0316dbbClaude221 <Container maxWidth={900}>
222 <Form method="post" action={`/${owner}/${repo}/edit/${ref}/${filePath}`}>
223 <FormGroup label="Content">
224 <TextArea
0074234Claude225 name="content"
226 rows={25}
bb0f894Claude227 value={blob.content}
228 mono
229 style="line-height: 1.5; tab-size: 2; width: 100%"
230 />
231 </FormGroup>
232 <FormGroup label="Commit message">
233 <Input
0074234Claude234 name="message"
235 placeholder={`Update ${filePath.split("/").pop()}`}
236 required
2228c49copilot-swe-agent[bot]237 aria-label="Commit message"
0074234Claude238 />
bb0f894Claude239 </FormGroup>
240 <Flex gap={8}>
241 <Button type="submit" variant="primary">
0074234Claude242 Commit changes
bb0f894Claude243 </Button>
244 <LinkButton href={`/${owner}/${repo}/blob/${ref}/${filePath}`}>
0074234Claude245 Cancel
bb0f894Claude246 </LinkButton>
247 </Flex>
248 </Form>
249 </Container>
0074234Claude250 </Layout>
251 );
252});
253
254// Save edited file
04f6b7fClaude255editor.post("/:owner/:repo/edit/:ref{.+$}", requireAuth, requireRepoAccess("write"), async (c) => {
0074234Claude256 const { owner, repo } = c.req.param();
257 const user = c.get("user")!;
258 const refAndPath = c.req.param("ref");
259
260 const slashIdx = refAndPath.indexOf("/");
261 if (slashIdx === -1) return c.redirect(`/${owner}/${repo}`);
262 const ref = refAndPath.slice(0, slashIdx);
263 const filePath = refAndPath.slice(slashIdx + 1);
264
265 const body = await c.req.parseBody();
266 const content = String(body.content || "");
267 const message = String(
268 body.message || `Update ${filePath.split("/").pop()}`
269 ).trim();
270
271 const repoDir = getRepoPath(owner, repo);
272
273 const run = async (cmd: string[], cwd: string, stdin?: string) => {
274 const proc = Bun.spawn(cmd, {
275 cwd,
276 stdout: "pipe",
277 stderr: "pipe",
278 stdin: stdin !== undefined ? "pipe" : undefined,
279 });
280 if (stdin !== undefined && proc.stdin) {
281 proc.stdin.write(new TextEncoder().encode(stdin));
282 proc.stdin.end();
283 }
284 const stdout = await new Response(proc.stdout).text();
285 await proc.exited;
286 return stdout.trim();
287 };
288
289 // Hash new content
290 const blobSha = await run(
291 ["git", "hash-object", "-w", "--stdin"],
292 repoDir,
293 content
294 );
295
296 // Read current tree, replace the file
297 const treeContent = await run(["git", "ls-tree", "-r", ref], repoDir);
298 const lines = treeContent.split("\n").filter(Boolean);
299 const updated = lines
300 .map((line) => {
301 const parts = line.match(/^(\d+) (\w+) ([0-9a-f]+)\t(.+)$/);
302 if (parts && parts[4] === filePath) {
303 return `${parts[1]} blob ${blobSha}\t${parts[4]}`;
304 }
305 return line;
306 })
307 .join("\n") + "\n";
308
309 const newTreeSha = await run(["git", "mktree"], repoDir, updated);
310 const parentSha = await run(["git", "rev-parse", ref], repoDir);
311
312 const env = {
313 GIT_AUTHOR_NAME: user.displayName || user.username,
314 GIT_AUTHOR_EMAIL: user.email,
315 GIT_COMMITTER_NAME: user.displayName || user.username,
316 GIT_COMMITTER_EMAIL: user.email,
317 };
318
319 const commitProc = Bun.spawn(
320 ["git", "commit-tree", newTreeSha, "-p", parentSha, "-m", message],
321 {
322 cwd: repoDir,
323 stdout: "pipe",
324 stderr: "pipe",
325 env: { ...process.env, ...env },
326 }
327 );
328 const commitSha = (await new Response(commitProc.stdout).text()).trim();
329 await commitProc.exited;
330
331 await run(
332 ["git", "update-ref", `refs/heads/${ref}`, commitSha],
333 repoDir
334 );
335
336 return c.redirect(`/${owner}/${repo}/blob/${ref}/${filePath}`);
337});
338
339export default editor;