routes/repo.js - git - Webernet Explorer _X
git 🏠 Home 👤 Sign up Self hosted git, retro shell
★ otgit, code hosting from the year 2000 ★
📄 octuna / routes / repo.js
269 lines · 10.9 KB · Raw
1const auth = require('../lib/auth');
2const data = require('../lib/data');
3const git = require('../lib/git');
4const render = require('../lib/render');
5const renderMd = require('../lib/render-md');
6const renderDiff = require('../lib/render-diff');
7const respond = require('../lib/respond');
8const validate = require('../lib/validate');
9const config = require('../lib/config');
10 
11function checkAccess(owner, name, session) {
12 if (!validate.isValidName(owner) || !validate.isValidName(name)) return null;
13 const repo = data.repos().get(`${owner}/${name}`);
14 if (!repo) return null;
15 if (repo.visibility === 'private') {
16 if (!session || session.username !== owner) return null;
17 }
18 return repo;
19}
20 
21async function repoHome(req, res, params) {
22 const session = auth.getSession(req);
23 const repo = checkAccess(params.owner, params.repo, session);
24 if (!repo) return respond.notFound(res);
25 
26 const gitDir = git.repoPath(params.owner, params.repo);
27 if (!git.exists(gitDir)) return respond.notFound(res);
28 
29 let refsData, count, entries, readmeHtml, fileCount = 0, langs = [];
30 try {
31 refsData = await git.refs(gitDir);
32 const ref = refsData.default;
33 count = await git.commitCount(gitDir, ref);
34 entries = await git.tree(gitDir, ref, '');
35 fileCount = await git.fileCount(gitDir, ref);
36 langs = await git.langStats(gitDir, ref);
37 const readmeEntry = entries.find(e => e.type === 'blob' && /^readme(\.|$)/i.test(e.name));
38 if (readmeEntry) {
39 const buf = await git.blobContent(gitDir, ref, readmeEntry.name);
40 readmeHtml = renderMd.render(buf.toString('utf8'));
41 }
42 } catch {
43 refsData = { default: config.get('repos.defaultBranch'), branches: [] };
44 count = 0; entries = []; fileCount = 0; langs = [];
45 }
46 
47 const ref = refsData.default;
48 const branchOpts = (refsData.branches.length ? refsData.branches : [ref]).map(b =>
49 `<option value="${render.escape(b)}"${b === ref ? ' selected' : ''}>${render.escape(b)}</option>`
50 ).join('');
51 
52 const createdDate = repo.createdAt
53 ? new Date(repo.createdAt).toISOString().slice(0, 10)
54 : '';
55 
56 const isOwner = session && session.username === params.owner;
57 const content = render.fill(render.loadView('repo-home.html'), {
58 OWNER: render.escape(params.owner),
59 REPO: render.escape(params.repo),
60 REF: render.escape(ref),
61 DESCRIPTION: render.escape(repo.description || ''),
62 VISIBILITY: render.escape(repo.visibility),
63 COMMIT_COUNT: count,
64 FILE_COUNT: fileCount,
65 CREATED_DATE: render.escape(createdDate),
66 BRANCH_OPTIONS: branchOpts,
67 TREE_ROWS: render.treeRows(entries, params.owner, params.repo, ref, ''),
68 LANG_BAR: render.langBar(langs),
69 README_HTML: readmeHtml ? `<div class="readme-body">${readmeHtml}</div>` : '',
70 SETTINGS_LINK: isOwner ? `<a class="btn" href="/${render.escape(params.owner)}/${render.escape(params.repo)}/settings">Settings</a>` : ''
71 });
72 
73 res.end(render.page({ title: `${params.owner}/${params.repo}`, content, session, addressUrl: req.url }));
74}
75 
76async function repoTree(req, res, params) {
77 const session = auth.getSession(req);
78 const repo = checkAccess(params.owner, params.repo, session);
79 if (!repo) return respond.notFound(res);
80 
81 const gitDir = git.repoPath(params.owner, params.repo);
82 const ref = params.ref;
83 const treePath = params.rest || '';
84 
85 let entries;
86 try { entries = await git.tree(gitDir, ref, treePath); }
87 catch { return respond.notFound(res); }
88 
89 const sidebarHtml = render.fileTreeSidebar(entries, params.owner, params.repo, ref, '');
90 
91 const content = render.fill(render.loadView('repo-tree.html'), {
92 OWNER: render.escape(params.owner),
93 REPO: render.escape(params.repo),
94 REF: render.escape(ref),
95 BREADCRUMB: render.breadcrumb(params.owner, params.repo, ref, treePath),
96 TREE_ROWS: render.treeRows(entries, params.owner, params.repo, ref, treePath)
97 });
98 
99 res.end(render.page({ title: `${params.owner}/${params.repo}`, content, session, addressUrl: req.url, extraSidebar: sidebarHtml }));
100}
101 
102async function repoArchive(req, res, params) {
103 const session = auth.getSession(req);
104 const repo = checkAccess(params.owner, params.repo, session);
105 if (!repo) return respond.notFound(res);
106 
107 const gitDir = git.repoPath(params.owner, params.repo);
108 const proc = git.archiveStream(gitDir, params.ref, `${params.repo}-${params.ref}`);
109 const filename = `${params.repo}-${params.ref}.zip`;
110 
111 let started = false;
112 proc.stdout.on('data', chunk => {
113 if (!started) {
114 started = true;
115 res.writeHead(200, {
116 'Content-Type': 'application/zip',
117 'Content-Disposition': `attachment; filename="${filename}"`,
118 ...respond.securityHeaders()
119 });
120 }
121 res.write(chunk);
122 });
123 proc.on('close', code => {
124 if (started) res.end();
125 else if (!res.headersSent) respond.notFound(res);
126 else res.end();
127 });
128 proc.on('error', () => { if (!res.headersSent) respond.serverError(res); });
129}
130 
131async function repoBlob(req, res, params) {
132 const session = auth.getSession(req);
133 const repo = checkAccess(params.owner, params.repo, session);
134 if (!repo) return respond.notFound(res);
135 
136 const gitDir = git.repoPath(params.owner, params.repo);
137 const ref = params.ref;
138 const filePath = params.rest;
139 const maxBytes = config.get('repos.maxRenderBlobMB') * 1024 * 1024;
140 
141 let buf, tooLarge = false;
142 try {
143 buf = await git.blobContent(gitDir, ref, filePath);
144 if (buf.length > maxBytes) { tooLarge = true; }
145 } catch { return respond.notFound(res); }
146 
147 const rawUrl = `/${render.escape(params.owner)}/${render.escape(params.repo)}/raw/${render.escape(ref)}/${render.escape(filePath)}`;
148 const lineCount = buf ? buf.toString('utf8').split('\n').length : 0;
149 const blobHtml = tooLarge
150 ? `<p>File too large to display (${(buf.length / 1024 / 1024).toFixed(1)} MB). <a href="${rawUrl}">Download raw</a></p>`
151 : render.blobLines(buf.toString('utf8'));
152 
153 let sidebarHtml = '';
154 try {
155 const dirPath = filePath.includes('/') ? filePath.slice(0, filePath.lastIndexOf('/')) : '';
156 const sidebarEntries = await git.tree(gitDir, ref, dirPath);
157 const currentName = filePath.includes('/') ? filePath.slice(filePath.lastIndexOf('/') + 1) : filePath;
158 sidebarHtml = render.fileTreeSidebar(sidebarEntries, params.owner, params.repo, ref, currentName);
159 } catch {}
160 
161 const content = render.fill(render.loadView('repo-blob.html'), {
162 OWNER: render.escape(params.owner),
163 REPO: render.escape(params.repo),
164 REF: render.escape(ref),
165 FILE_PATH: render.escape(filePath),
166 BREADCRUMB: render.breadcrumb(params.owner, params.repo, ref, filePath),
167 LINE_COUNT: tooLarge ? 'too large' : lineCount + ' lines',
168 FILE_SIZE: (buf.length / 1024).toFixed(1) + ' KB',
169 RAW_URL: rawUrl,
170 BLOB_HTML: blobHtml
171 });
172 
173 res.end(render.page({ title: filePath, content, session, addressUrl: req.url, extraSidebar: sidebarHtml }));
174}
175 
176async function rawBlob(req, res, params) {
177 const session = auth.getSession(req);
178 const repo = checkAccess(params.owner, params.repo, session);
179 if (!repo) return respond.notFound(res);
180 
181 const gitDir = git.repoPath(params.owner, params.repo);
182 const proc = git.blobStream(gitDir, params.ref, params.rest);
183 let started = false;
184 proc.stdout.on('data', chunk => {
185 if (!started) {
186 started = true;
187 res.writeHead(200, {
188 'Content-Type': 'application/octet-stream',
189 'Content-Disposition': `inline; filename="${params.rest.split('/').pop()}"`,
190 ...respond.securityHeaders()
191 });
192 }
193 res.write(chunk);
194 });
195 proc.on('close', code => {
196 if (!started) respond.notFound(res);
197 else res.end();
198 if (code !== 0 && !started) respond.notFound(res);
199 });
200}
201 
202async function repoHistory(req, res, params) {
203 const session = auth.getSession(req);
204 const repo = checkAccess(params.owner, params.repo, session);
205 if (!repo) return respond.notFound(res);
206 
207 const gitDir = git.repoPath(params.owner, params.repo);
208 const ref = params.ref || (await git.refs(gitDir)).default;
209 const page = Math.max(1, parseInt(new URL('http://x' + req.url).searchParams.get('page') || '1', 10));
210 const perPage = 30;
211 
212 let commits;
213 try { commits = await git.log(gitDir, ref, perPage + 1, (page - 1) * perPage); }
214 catch { return respond.notFound(res); }
215 
216 const hasNext = commits.length > perPage;
217 if (hasNext) commits.pop();
218 
219 const content = render.fill(render.loadView('repo-history.html'), {
220 OWNER: render.escape(params.owner),
221 REPO: render.escape(params.repo),
222 REF: render.escape(ref),
223 COMMITS_HTML: render.commitRows(commits, params.owner, params.repo),
224 PREV_LINK: page > 1 ? `<a href="?page=${page - 1}">&laquo; Newer</a>` : '',
225 NEXT_LINK: hasNext ? `<a href="?page=${page + 1}">Older &raquo;</a>` : ''
226 });
227 
228 res.end(render.page({ title: `History - ${params.owner}/${params.repo}`, content, session, addressUrl: req.url }));
229}
230 
231async function repoCommit(req, res, params) {
232 const session = auth.getSession(req);
233 const repo = checkAccess(params.owner, params.repo, session);
234 if (!repo) return respond.notFound(res);
235 
236 const gitDir = git.repoPath(params.owner, params.repo);
237 let info, diffHtml;
238 try {
239 info = await git.commitInfo(gitDir, params.sha);
240 const rawDiff = await git.diff(gitDir, params.sha);
241 diffHtml = renderDiff.render(rawDiff);
242 } catch { return respond.notFound(res); }
243 
244 const content = render.fill(render.loadView('repo-commit.html'), {
245 OWNER: render.escape(params.owner),
246 REPO: render.escape(params.repo),
247 SHA: render.escape(params.sha),
248 SHA7: render.escape(params.sha.slice(0, 7)),
249 COMMIT_MESSAGE: render.escape(info.message),
250 COMMIT_AUTHOR: render.escape(info.author),
251 COMMIT_DATE: render.escape(info.date ? info.date.slice(0, 10) : ''),
252 DIFF_HTML: diffHtml
253 });
254 
255 res.end(render.page({ title: params.sha.slice(0, 7), content, session, addressUrl: req.url }));
256}
257 
258module.exports = function register(router) {
259 router.get('/:owner/:repo', (q, s, p) => repoHome(q, s, p).catch(e => respond.serverError(s, e.message)));
260 router.get('/:owner/:repo/tree/:ref', (q, s, p) => repoHome(q, s, p).catch(e => respond.serverError(s, e.message)));
261 router.get('/:owner/:repo/tree/:ref/*', (q, s, p) => repoTree(q, s, p).catch(e => respond.serverError(s, e.message)));
262 router.get('/:owner/:repo/blob/:ref/*', (q, s, p) => repoBlob(q, s, p).catch(e => respond.serverError(s, e.message)));
263 router.get('/:owner/:repo/raw/:ref/*', (q, s, p) => rawBlob(q, s, p).catch(e => respond.serverError(s, e.message)));
264 router.get('/:owner/:repo/archive/:ref.zip', (q, s, p) => repoArchive(q, s, p).catch(e => respond.serverError(s, e.message)));
265 router.get('/:owner/:repo/history', (q, s, p) => repoHistory(q, s, p).catch(e => respond.serverError(s, e.message)));
266 router.get('/:owner/:repo/history/:ref', (q, s, p) => repoHistory(q, s, p).catch(e => respond.serverError(s, e.message)));
267 router.get('/:owner/:repo/commit/:sha', (q, s, p) => repoCommit(q, s, p).catch(e => respond.serverError(s, e.message)));
268};
269 
Done 🔒 Internet