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.tsxBlame335 lines · 1 contributor
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";
28import { join } from "path";
29
30const editor = new Hono<AuthEnv>();
31
32editor.use("*", softAuth);
33
34// New file form
35editor.get("/:owner/:repo/new/:ref{.+$}", requireAuth, async (c) => {
36 const { owner, repo } = c.req.param();
37 const user = c.get("user")!;
38 const refAndPath = c.req.param("ref");
39
40 // Parse ref — use first segment
41 const slashIdx = refAndPath.indexOf("/");
42 const ref = slashIdx === -1 ? refAndPath : refAndPath.slice(0, slashIdx);
43 const dirPath = slashIdx === -1 ? "" : refAndPath.slice(slashIdx + 1);
44
45 return c.html(
46 <Layout title={`New file — ${owner}/${repo}`} user={user}>
47 <RepoHeader owner={owner} repo={repo} />
48 <RepoNav owner={owner} repo={repo} active="code" />
bb0f894Claude49 <Container maxWidth={900}>
0074234Claude50 <h2 style="margin-bottom: 16px">Create new file</h2>
0316dbbClaude51 <Form method="post" action={`/${owner}/${repo}/new/${ref}`}>
0074234Claude52 <input type="hidden" name="dir_path" value={dirPath} />
bb0f894Claude53 <FormGroup label="File path">
54 <Flex align="center" gap={4}>
0074234Claude55 {dirPath && (
bb0f894Claude56 <Text muted size={14}>
0074234Claude57 {dirPath}/
bb0f894Claude58 </Text>
0074234Claude59 )}
bb0f894Claude60 <Input
0074234Claude61 name="filename"
62 required
63 placeholder="filename.ts"
64 style="flex: 1"
65 autocomplete="off"
66 />
bb0f894Claude67 </Flex>
68 </FormGroup>
69 <FormGroup label="Content">
70 <TextArea
0074234Claude71 name="content"
72 rows={20}
73 placeholder="Enter file content..."
bb0f894Claude74 mono
75 style="line-height: 1.5; tab-size: 2"
0074234Claude76 />
bb0f894Claude77 </FormGroup>
78 <FormGroup label="Commit message">
79 <Input
0074234Claude80 name="message"
81 placeholder="Create new file"
82 required
83 />
bb0f894Claude84 </FormGroup>
85 <Button type="submit" variant="primary">
0074234Claude86 Commit new file
bb0f894Claude87 </Button>
88 </Form>
89 </Container>
0074234Claude90 </Layout>
91 );
92});
93
94// Create file via commit
95editor.post("/:owner/:repo/new/:ref", requireAuth, async (c) => {
96 const { owner, repo } = c.req.param();
97 const user = c.get("user")!;
98 const ref = c.req.param("ref");
99 const body = await c.req.parseBody();
100 const dirPath = String(body.dir_path || "").trim();
101 const filename = String(body.filename || "").trim();
102 const content = String(body.content || "");
103 const message = String(body.message || `Create ${filename}`).trim();
104
105 if (!filename) return c.redirect(`/${owner}/${repo}`);
106
107 const fullPath = dirPath ? `${dirPath}/${filename}` : filename;
108
109 // Use git hash-object + update-index + write-tree + commit-tree
110 const repoDir = getRepoPath(owner, repo);
111
112 const run = async (cmd: string[], cwd: string, stdin?: string) => {
113 const proc = Bun.spawn(cmd, {
114 cwd,
115 stdout: "pipe",
116 stderr: "pipe",
117 stdin: stdin !== undefined ? "pipe" : undefined,
118 });
119 if (stdin !== undefined && proc.stdin) {
120 proc.stdin.write(new TextEncoder().encode(stdin));
121 proc.stdin.end();
122 }
123 const stdout = await new Response(proc.stdout).text();
124 await proc.exited;
125 return stdout.trim();
126 };
127
128 // Hash the new file content
129 const blobSha = await run(
130 ["git", "hash-object", "-w", "--stdin"],
131 repoDir,
132 content
133 );
134
135 // Read current tree
136 const currentTreeSha = await run(
137 ["git", "rev-parse", `${ref}^{tree}`],
138 repoDir
139 );
140
141 // Read current tree and add new entry
142 const treeContent = await run(["git", "ls-tree", "-r", ref], repoDir);
143 const entries = treeContent
144 .split("\n")
145 .filter(Boolean)
146 .map((line) => line + "\n")
147 .join("");
148 const newEntry = `100644 blob ${blobSha}\t${fullPath}\n`;
149
150 const newTreeSha = await run(
151 ["git", "mktree"],
152 repoDir,
153 entries + newEntry
154 );
155
156 // Get parent commit
157 const parentSha = await run(
158 ["git", "rev-parse", ref],
159 repoDir
160 );
161
162 // Create commit
163 const env = {
164 GIT_AUTHOR_NAME: user.displayName || user.username,
165 GIT_AUTHOR_EMAIL: user.email,
166 GIT_COMMITTER_NAME: user.displayName || user.username,
167 GIT_COMMITTER_EMAIL: user.email,
168 };
169
170 const commitProc = Bun.spawn(
171 ["git", "commit-tree", newTreeSha, "-p", parentSha, "-m", message],
172 {
173 cwd: repoDir,
174 stdout: "pipe",
175 stderr: "pipe",
176 env: { ...process.env, ...env },
177 }
178 );
179 const commitSha = (await new Response(commitProc.stdout).text()).trim();
180 await commitProc.exited;
181
182 // Update branch ref
183 await run(
184 ["git", "update-ref", `refs/heads/${ref}`, commitSha],
185 repoDir
186 );
187
188 return c.redirect(`/${owner}/${repo}/blob/${ref}/${fullPath}`);
189});
190
191// Edit file form
192editor.get("/:owner/:repo/edit/:ref{.+$}", requireAuth, async (c) => {
193 const { owner, repo } = c.req.param();
194 const user = c.get("user")!;
195 const refAndPath = c.req.param("ref");
196
197 // Parse ref/path
198 const slashIdx = refAndPath.indexOf("/");
199 if (slashIdx === -1) return c.text("Not found", 404);
200 const ref = refAndPath.slice(0, slashIdx);
201 const filePath = refAndPath.slice(slashIdx + 1);
202
203 const blob = await getBlob(owner, repo, ref, filePath);
204 if (!blob || blob.isBinary) {
205 return c.html(
206 <Layout title="Cannot edit" user={user}>
bb0f894Claude207 <EmptyState title={blob?.isBinary ? "Cannot edit binary file" : "File not found"} />
0074234Claude208 </Layout>,
209 404
210 );
211 }
212
213 return c.html(
214 <Layout title={`Editing ${filePath} — ${owner}/${repo}`} user={user}>
215 <RepoHeader owner={owner} repo={repo} />
216 <RepoNav owner={owner} repo={repo} active="code" />
217 <Breadcrumb owner={owner} repo={repo} ref={ref} path={filePath} />
0316dbbClaude218 <Container maxWidth={900}>
219 <Form method="post" action={`/${owner}/${repo}/edit/${ref}/${filePath}`}>
220 <FormGroup label="Content">
221 <TextArea
0074234Claude222 name="content"
223 rows={25}
bb0f894Claude224 value={blob.content}
225 mono
226 style="line-height: 1.5; tab-size: 2; width: 100%"
227 />
228 </FormGroup>
229 <FormGroup label="Commit message">
230 <Input
0074234Claude231 name="message"
232 placeholder={`Update ${filePath.split("/").pop()}`}
233 required
234 />
bb0f894Claude235 </FormGroup>
236 <Flex gap={8}>
237 <Button type="submit" variant="primary">
0074234Claude238 Commit changes
bb0f894Claude239 </Button>
240 <LinkButton href={`/${owner}/${repo}/blob/${ref}/${filePath}`}>
0074234Claude241 Cancel
bb0f894Claude242 </LinkButton>
243 </Flex>
244 </Form>
245 </Container>
0074234Claude246 </Layout>
247 );
248});
249
250// Save edited file
251editor.post("/:owner/:repo/edit/:ref{.+$}", requireAuth, async (c) => {
252 const { owner, repo } = c.req.param();
253 const user = c.get("user")!;
254 const refAndPath = c.req.param("ref");
255
256 const slashIdx = refAndPath.indexOf("/");
257 if (slashIdx === -1) return c.redirect(`/${owner}/${repo}`);
258 const ref = refAndPath.slice(0, slashIdx);
259 const filePath = refAndPath.slice(slashIdx + 1);
260
261 const body = await c.req.parseBody();
262 const content = String(body.content || "");
263 const message = String(
264 body.message || `Update ${filePath.split("/").pop()}`
265 ).trim();
266
267 const repoDir = getRepoPath(owner, repo);
268
269 const run = async (cmd: string[], cwd: string, stdin?: string) => {
270 const proc = Bun.spawn(cmd, {
271 cwd,
272 stdout: "pipe",
273 stderr: "pipe",
274 stdin: stdin !== undefined ? "pipe" : undefined,
275 });
276 if (stdin !== undefined && proc.stdin) {
277 proc.stdin.write(new TextEncoder().encode(stdin));
278 proc.stdin.end();
279 }
280 const stdout = await new Response(proc.stdout).text();
281 await proc.exited;
282 return stdout.trim();
283 };
284
285 // Hash new content
286 const blobSha = await run(
287 ["git", "hash-object", "-w", "--stdin"],
288 repoDir,
289 content
290 );
291
292 // Read current tree, replace the file
293 const treeContent = await run(["git", "ls-tree", "-r", ref], repoDir);
294 const lines = treeContent.split("\n").filter(Boolean);
295 const updated = lines
296 .map((line) => {
297 const parts = line.match(/^(\d+) (\w+) ([0-9a-f]+)\t(.+)$/);
298 if (parts && parts[4] === filePath) {
299 return `${parts[1]} blob ${blobSha}\t${parts[4]}`;
300 }
301 return line;
302 })
303 .join("\n") + "\n";
304
305 const newTreeSha = await run(["git", "mktree"], repoDir, updated);
306 const parentSha = await run(["git", "rev-parse", ref], repoDir);
307
308 const env = {
309 GIT_AUTHOR_NAME: user.displayName || user.username,
310 GIT_AUTHOR_EMAIL: user.email,
311 GIT_COMMITTER_NAME: user.displayName || user.username,
312 GIT_COMMITTER_EMAIL: user.email,
313 };
314
315 const commitProc = Bun.spawn(
316 ["git", "commit-tree", newTreeSha, "-p", parentSha, "-m", message],
317 {
318 cwd: repoDir,
319 stdout: "pipe",
320 stderr: "pipe",
321 env: { ...process.env, ...env },
322 }
323 );
324 const commitSha = (await new Response(commitProc.stdout).text()).trim();
325 await commitProc.exited;
326
327 await run(
328 ["git", "update-ref", `refs/heads/${ref}`, commitSha],
329 repoDir
330 );
331
332 return c.redirect(`/${owner}/${repo}/blob/${ref}/${filePath}`);
333});
334
335export default editor;