lib/git.js - git - Webernet Explorer _X
git 🏠 Home 👤 Sign up Self hosted git, retro shell
★ otgit, code hosting from the year 2000 ★
📄 octuna / lib / git.js
222 lines · 7.1 KB · Raw
1const { spawn } = require('child_process');
2const fs = require('fs');
3const path = require('path');
4const config = require('./config');
5const validate = require('./validate');
6 
7const ROOT = path.join(__dirname, '..');
8 
9function reposRoot() {
10 const r = config.get('repos.rootDir');
11 return path.isAbsolute(r) ? r : path.join(ROOT, r);
12}
13 
14function repoPath(owner, name) {
15 return validate.safeJoin(reposRoot(), owner, name + '.git');
16}
17 
18function exists(gitDir) {
19 try { return fs.statSync(path.join(gitDir, 'HEAD')).isFile(); }
20 catch { return false; }
21}
22 
23function bin() { return config.get('git.binPath') || 'git'; }
24 
25function run(args, gitDir) {
26 return new Promise((resolve, reject) => {
27 const proc = spawn(bin(), ['--git-dir=' + gitDir, ...args]);
28 let out = '';
29 let err = '';
30 proc.stdout.on('data', d => { out += d; });
31 proc.stderr.on('data', d => { err += d; });
32 proc.on('error', reject);
33 proc.on('close', code => {
34 if (code !== 0) reject(new Error(err.trim() || 'git exit ' + code));
35 else resolve(out);
36 });
37 });
38}
39 
40function init(gitDir) {
41 const branch = config.get('repos.defaultBranch') || 'main';
42 return new Promise((resolve, reject) => {
43 const proc = spawn(bin(), ['init', '--bare', '--initial-branch=' + branch, gitDir]);
44 proc.on('error', reject);
45 proc.on('close', code => {
46 if (code !== 0) return reject(new Error('git init --bare failed'));
47 // Enable HTTP receive-pack so smart HTTP push works.
48 const cfg = spawn(bin(), ['--git-dir=' + gitDir, 'config', 'http.receivepack', 'true']);
49 cfg.on('close', () => resolve());
50 cfg.on('error', () => resolve());
51 });
52 });
53}
54 
55async function refs(gitDir) {
56 let headRef = null;
57 try {
58 const head = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
59 const m = head.match(/^ref: refs\/heads\/(.+)$/);
60 if (m) headRef = m[1];
61 } catch {}
62 let branches = [];
63 try {
64 const out = await run(['branch', '--format=%(refname:short)'], gitDir);
65 branches = out.split('\n').map(s => s.trim()).filter(Boolean);
66 } catch {}
67 // HEAD may point to a branch that does not exist yet (bare repo created
68 // before any push). Prefer an actual existing branch.
69 if (!headRef || !branches.includes(headRef)) {
70 if (branches.length) headRef = branches[0];
71 else headRef = config.get('repos.defaultBranch') || 'main';
72 }
73 return { default: headRef, branches };
74}
75 
76async function commitCount(gitDir, ref) {
77 try {
78 const out = await run(['rev-list', '--count', ref], gitDir);
79 return parseInt(out.trim(), 10) || 0;
80 } catch { return 0; }
81}
82 
83const LOG_FORMAT = '%H%x09%an%x09%ae%x09%ai%x09%s';
84 
85async function log(gitDir, ref, limit, skip) {
86 const args = ['log', '--format=' + LOG_FORMAT];
87 if (limit) args.push('-' + limit);
88 if (skip) args.push('--skip=' + skip);
89 args.push(ref);
90 const out = await run(args, gitDir);
91 return out.split('\n').filter(Boolean).map(line => {
92 const parts = line.split('\t');
93 return {
94 hash: parts[0],
95 author: parts[1],
96 email: parts[2],
97 date: parts[3],
98 message: parts.slice(4).join('\t')
99 };
100 });
101}
102 
103async function tree(gitDir, ref, treePath) {
104 const spec = treePath ? `${ref}:${treePath}` : `${ref}:`;
105 const out = await run(['ls-tree', spec], gitDir);
106 return out.split('\n').filter(Boolean).map(line => {
107 const m = line.match(/^(\d+) (blob|tree) ([0-9a-f]+)\t(.+)$/);
108 return m ? { mode: m[1], type: m[2], hash: m[3], name: m[4] } : null;
109 }).filter(Boolean);
110}
111 
112function blobStream(gitDir, ref, filePath) {
113 return spawn(bin(), ['--git-dir=' + gitDir, 'cat-file', 'blob', `${ref}:${filePath}`]);
114}
115 
116function archiveStream(gitDir, ref, prefix) {
117 const args = ['--git-dir=' + gitDir, 'archive', '--format=zip'];
118 if (prefix) args.push('--prefix=' + prefix + '/');
119 args.push(ref);
120 return spawn(bin(), args);
121}
122 
123function blobContent(gitDir, ref, filePath) {
124 return new Promise((resolve, reject) => {
125 const proc = spawn(bin(), ['--git-dir=' + gitDir, 'cat-file', 'blob', `${ref}:${filePath}`]);
126 const chunks = [];
127 proc.stdout.on('data', d => chunks.push(d));
128 proc.on('error', reject);
129 proc.on('close', code => {
130 if (code !== 0) reject(new Error('object not found'));
131 else resolve(Buffer.concat(chunks));
132 });
133 });
134}
135 
136async function objectSize(gitDir, objId) {
137 try {
138 const out = await run(['cat-file', '-s', objId], gitDir);
139 return parseInt(out.trim(), 10) || 0;
140 } catch { return 0; }
141}
142 
143async function commitInfo(gitDir, sha) {
144 const fmt = '%H%x09%an%x09%ae%x09%ai%x09%P%x09%s';
145 const out = await run(['show', '--no-patch', '--format=' + fmt, sha], gitDir);
146 const line = out.split('\n')[0];
147 const parts = line.split('\t');
148 return {
149 hash: parts[0],
150 author: parts[1],
151 email: parts[2],
152 date: parts[3],
153 parents: parts[4] ? parts[4].split(' ').filter(Boolean) : [],
154 message: parts.slice(5).join('\t')
155 };
156}
157 
158async function diff(gitDir, sha) {
159 return run(['diff-tree', '--no-commit-id', '-p', sha], gitDir);
160}
161 
162async function fileCount(gitDir, ref) {
163 try {
164 const out = await run(['ls-tree', '-r', '--name-only', ref], gitDir);
165 return out.split('\n').filter(Boolean).length;
166 } catch { return 0; }
167}
168 
169const LANG_EXT = {
170 js: 'JavaScript', mjs: 'JavaScript', cjs: 'JavaScript', jsx: 'JavaScript',
171 ts: 'TypeScript', tsx: 'TypeScript',
172 py: 'Python', rb: 'Ruby', go: 'Go', rs: 'Rust', java: 'Java',
173 c: 'C', h: 'C', cpp: 'C++', cc: 'C++', hpp: 'C++', hh: 'C++',
174 cs: 'C#', php: 'PHP', sh: 'Shell', bash: 'Shell', zsh: 'Shell',
175 ps1: 'PowerShell', bat: 'Batch', cmd: 'Batch',
176 html: 'HTML', htm: 'HTML', css: 'CSS', scss: 'SCSS', sass: 'Sass', less: 'Less',
177 md: 'Markdown', markdown: 'Markdown',
178 json: 'JSON', yml: 'YAML', yaml: 'YAML',
179 xml: 'XML', toml: 'TOML', ini: 'INI', conf: 'Config',
180 sql: 'SQL',
181 vue: 'Vue', svelte: 'Svelte',
182 swift: 'Swift', kt: 'Kotlin', dart: 'Dart',
183 ex: 'Elixir', exs: 'Elixir', erl: 'Erlang',
184 scala: 'Scala', clj: 'Clojure', elm: 'Elm',
185 lua: 'Lua', pl: 'Perl', r: 'R',
186 txt: 'Text', tex: 'TeX'
187};
188 
189async function langStats(gitDir, ref) {
190 try {
191 const out = await run(['ls-tree', '-r', '-l', ref], gitDir);
192 const totals = {};
193 let total = 0;
194 for (const line of out.split('\n')) {
195 const m = line.match(/^\d+ blob [0-9a-f]+\s+(\d+)\t(.+)$/);
196 if (!m) continue;
197 const size = parseInt(m[1], 10) || 0;
198 if (size === 0) continue;
199 const name = m[2];
200 let ext;
201 if (/^Dockerfile/i.test(name.split('/').pop())) { ext = 'dockerfile'; }
202 else if (name.includes('.')) { ext = name.slice(name.lastIndexOf('.') + 1).toLowerCase(); }
203 else continue;
204 const lang = LANG_EXT[ext];
205 if (!lang) continue;
206 totals[lang] = (totals[lang] || 0) + size;
207 total += size;
208 }
209 if (total === 0) return [];
210 return Object.entries(totals)
211 .map(([lang, size]) => ({ lang, size, pct: (size / total) * 100 }))
212 .sort((a, b) => b.size - a.size);
213 } catch { return []; }
214}
215 
216module.exports = {
217 reposRoot, repoPath, exists,
218 init, refs, commitCount, log, tree,
219 blobStream, blobContent, objectSize, archiveStream,
220 commitInfo, diff, fileCount, langStats
221};
222 
Done 🔒 Internet