Skip to content

Instantly share code, notes, and snippets.

@iczero
Last active January 16, 2024 07:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iczero/2bc8aed789d9232c0b14d220ee5cf8b6 to your computer and use it in GitHub Desktop.
Save iczero/2bc8aed789d9232c0b14d220ee5cf8b6 to your computer and use it in GitHub Desktop.
an approximately terrible idea involving the invocation of git plumbing commands from typescript
import path from 'node:path';
import stream from 'node:stream';
import { git } from './spawn.js';
import { Repository } from './repository.ts';
import { FastifyPluginCallback } from 'fastify';
export const api: FastifyPluginCallback = (fastify, _opts, done) => {
const gitSmartSchema = {
type: 'object',
properties: {
service: { type: 'string' },
},
required: ['service'], // will reject dumb clients
}
fastify.addContentTypeParser(
[
'application/x-git-upload-pack-request',
'application/x-git-receive-pack-request'
],
(_req, _payload, done) => {
// do not parse, stream instead
done(null);
}
);
fastify.get<{
Params: { repo: string },
Querystring: { service: string }
}>(
'/git/:repo/info/refs',
{ schema: { querystring: gitSmartSchema } },
(req, res) => {
if (req.params.repo !== 'test') {
// quality code :100:
res.code(404).send('?');
return;
}
// ensure no cache
res.headers({
'Pragma': 'no-cache',
'Cache-Control': 'no-cache, max-age=0, must-revalidate'
});
let protocol = null;
if (typeof req.headers['git-protocol'] === 'string') {
protocol = req.headers['git-protocol'];
}
let service = req.query.service;
switch (service) {
case 'git-upload-pack': {
res.type('application/x-git-upload-pack-advertisement');
res.code(200);
let repo = path.resolve('../../data', 'test.git');
res.send(gitUploadPack(protocol, repo, req.raw, true));
break;
}
case 'git-receive-pack': {
res.type('application/x-git-receive-pack-advertisement');
res.code(200);
let repo = path.resolve('../../data', 'test.git');
res.send(gitReceivePack(protocol, repo, req.raw, true));
break;
}
default: {
res.code(403).send('unknown service name');
return;
}
}
}
);
fastify.post<{ Params: { repo: string } }>(
'/git/:repo/git-upload-pack',
(req, res) => {
if (req.params.repo !== 'test') {
res.code(404).send('?');
return;
}
let protocol = null;
if (typeof req.headers['git-protocol'] === 'string') {
protocol = req.headers['git-protocol'];
}
res.status(200);
res.type('application/x-git-upload-pack-result');
let repo = path.resolve('../../data', 'test.git');
res.send(gitUploadPack(protocol, repo, req.raw, false));
}
);
fastify.post<{ Params: { repo: string } }>(
'/git/:repo/git-receive-pack',
(req, res) => {
if (req.params.repo !== 'test') {
res.code(404).send('?');
return;
}
let protocol = null;
if (typeof req.headers['git-protocol'] === 'string') {
protocol = req.headers['git-protocol'];
}
res.status(200);
res.type('application/x-git-receive-pack-result');
let repo = path.resolve('../../data', 'test.git');
res.send(gitReceivePack(protocol, repo, req.raw, false));
}
);
function writePktLine(data: string | Buffer): Buffer {
let len = data.length + 4;
if (len > 65535) throw new Error('line too long');
return Buffer.concat([
Buffer.from(len.toString(16).padStart(4, '0')),
typeof data === 'string' ? Buffer.from(data) : data
]);
}
function gitUploadPack(
gitProtocol: string | null, repo: string, inStream: stream.Readable,
isGet: boolean
): stream.Readable {
let argv = ['upload-pack', '--stateless-rpc'];
if (isGet) argv.push('--http-backend-info-refs');
argv.push(repo);
let env = {};
if (gitProtocol !== null) {
env['GIT_PROTOCOL'] = gitProtocol;
}
let proc = git(argv, { stdin: !isGet, env }).collectStderr();
if (!isGet) inStream.pipe(proc.stdin!);
(async () => {
await proc.wait();
if (!proc.succeeded) {
console.log('upload-pack error:', proc.collectedStderr);
}
})();
if (isGet && gitProtocol !== 'version=2') {
let through = new stream.PassThrough();
through.write(writePktLine('# service=git-upload-pack\n'));
through.write('0000'); // flush packet
proc.stdout.pipe(through);
return through;
}
return proc.stdout;
}
function gitReceivePack(
gitProtocol: string | null, repo: string, inStream: stream.Readable,
isGet: boolean
): stream.Readable {
let argv = ['receive-pack', '--stateless-rpc'];
if (isGet) argv.push('--http-backend-info-refs');
argv.push(repo);
let env = {};
if (gitProtocol !== null) {
env['GIT_PROTOCOL'] = gitProtocol;
}
let proc = git(argv, { stdin: !isGet, env }).collectStderr();
if (!isGet) inStream.pipe(proc.stdin!);
(async () => {
await proc.wait();
if (!proc.succeeded) {
console.log('receive-pack error:', proc.collectedStderr);
}
})();
if (isGet && gitProtocol !== 'version=2') {
let through = new stream.PassThrough();
through.write(writePktLine('# service=git-receive-pack\n'));
through.write('0000'); // flush packet
proc.stdout.pipe(through);
return through;
}
return proc.stdout;
}
done();
}
import path from 'path';
import { promises as fsP } from 'fs';
import stream from 'stream';
import EventEmitter from 'events';
import { GitExecOptions, GitProcess, LineReader, git } from './spawn.js';
import createDebug from 'debug';
const debug = createDebug('git:repository');
/** null object id */
export const NULL_ID = '0000000000000000000000000000000000000000';
/** file modes used by git */
export enum FileMode {
/** regular file, not executable */
Regular = 100644,
/** regular file, executable */
RegularExec = 100755,
/** symbolic link */
Symlink = 120000,
/** not sure */
Gitlink = 160000,
/** fake mode used to delete entries from index */
Delete = 0
}
export interface GitAuthorInfo {
authorName: string;
authorEmail: string;
committerName?: string;
committerEmail?: string;
}
export async function streamWrite(stream: stream.Writable, chunk: Buffer | string) {
let canContinue = stream.write(chunk);
if (!canContinue) {
await EventEmitter.once(stream, 'drain');
}
}
export class BaseRepository {
constructor(public gitdir: string) {}
git(argv: string[], opts?: GitExecOptions): GitProcess {
return git(argv, Object.assign({ gitdir: this.gitdir }, opts));
}
/** parse a revision name to an id */
async revParse(rev: string): Promise<string | null> {
let proc = await this.git(['rev-parse', '--verify', '--end-of-options', rev])
.collectStdout()
.collectStderr()
.wait();
if (proc.succeeded) {
return proc.collectedStdout!.trim();
} else {
return null;
}
}
/** perform various cleanup tasks on repository */
async gc(auto = true, prune = '8.hours.ago') {
let argv = ['gc', '--prune=' + prune];
if (auto) argv.push('--auto');
let proc = await this.git(argv)
.collectStdout()
.collectStderr()
.wait();
proc.assertSucceeded('repository gc');
}
/** read object from object database */
readObject(id: string, type = 'blob'): stream.Readable {
let proc = this.git(['cat-file', type, id])
.collectStderr();
(async () => {
// ensure errors go to the stream
await proc.wait();
try {
proc.assertSucceeded('reading object');
} catch (err) {
proc.stdout.emit('error', err);
}
})();
return proc.stdout;
}
/** write object to object database, returning id */
writeObject(type = 'blob'): [stream.Writable, Promise<string>] {
let proc = this.git(['hash-object', '-t', type, '-w', '--stdin'], { stdin: true })
.collectStdout()
.collectStderr();
return [
proc.stdin!,
(async () => {
await proc.wait();
proc.assertSucceeded('writing object');
return proc.collectedStdout!.trim();
})()
];
}
/** create an empty tree object */
async createEmptyTree(): Promise<string> {
let tmpIndex = path.join(this.gitdir, 'index.tmp-' + Math.random().toString(36).slice(2));
try {
// create empty tree object
let proc = await this.git(['write-tree'], { env: { GIT_INDEX_FILE: tmpIndex } })
.collectStdout()
.collectStderr()
.wait();
proc.assertSucceeded('write empty tree');
return proc.collectedStdout!.trim();
} finally {
await fsP.unlink(tmpIndex);
}
}
/** create a commit from a tree object */
async commitTree(tree: string, parents: string[], message: string, author: GitAuthorInfo): Promise<string> {
let proc = this.git(['commit-tree', ...parents.flatMap(p => ['-p', p]), tree], {
config: {
'author.name': author.authorName,
'author.email': author.authorEmail,
'committer.name': author.committerName ?? author.authorName,
'committer.email': author.committerEmail ?? author.authorEmail
},
stdin: true
})
.collectStdout()
.collectStderr();
proc.stdin!.write(message);
proc.stdin!.end();
await proc.wait();
proc.assertSucceeded('commit tree');
return proc.collectedStdout!.trim();
}
/** update a ref */
async updateRef(ref: string, newValue: string, oldValue?: string) {
let argv = ['update-ref', ref, newValue];
if (oldValue) argv.push(oldValue);
let proc = await this.git(argv)
.collectStdout()
.collectStderr()
.wait();
proc.assertSucceeded('updating ref');
}
/** delete a ref */
async deleteRef(ref: string, oldValue?: string) {
let argv = ['update-ref', '-d', ref];
if (oldValue) argv.push(oldValue);
let proc = await this.git(argv)
.collectStdout()
.collectStderr()
.wait();
proc.assertSucceeded('deleting ref');
}
/** update multiple refs */
updateRefs(): UpdateRef {
let proc = this.git(['update-ref', '--stdin', '-z'], { stdin: true });
return new UpdateRef(proc);
}
/** list all refs */
async *listRefs(): AsyncGenerator<{ ref: string, id: string }, void, never> {
let proc = this.git(['show-ref'])
.collectStderr();
let lineReader = new LineReader('\n');
proc.stdout.pipe(lineReader);
while (true) {
let line = await lineReader.nextLine();
if (!line) break;
let [id, ref] = line.split(' ');
yield { ref, id };
}
await proc.wait();
proc.assertSucceeded('listing refs');
}
}
function expect(result: string, expected: string, what: string) {
if (result !== expected) {
throw new Error(`${what}: unexpected output "${result}"`);
}
}
export class UpdateRef {
stdoutReader: LineReader;
stderrReader: LineReader;
errorWait: Promise<string | null>;
stdin: stream.Writable;
constructor(public proc: GitProcess) {
this.stdoutReader = new LineReader('\n');
proc.stdout.pipe(this.stdoutReader);
this.stderrReader = new LineReader('\n');
proc.stderr.pipe(this.stderrReader);
this.errorWait = this.stderrReader.nextLine();
this.stdin = proc.stdin!;
}
async command(command: string, expectOutput?: false): Promise<null>;
async command(command: string, expectOutput: true): Promise<string>;
async command(command: string, expectOutput?: boolean): Promise<string | null> {
if (this.proc.exited) throw new Error('already ended');
await streamWrite(this.stdin, command + '\x00');
if (expectOutput) {
let [which, output] = await Promise.race([
this.stdoutReader.nextLine().then(r => ['out', r]),
this.errorWait.then(r => ['err', r])
]);
if (which === 'out') {
return output;
} else {
throw new Error('command failed: ' + output);
}
} else {
return null;
}
}
/** start transaction */
async start() {
let output = await this.command('start', true);
expect(output, 'start: ok', 'update-ref start transaction');
}
/** rollback transaction */
async abort() {
let output = await this.command('abort', true);
expect(output, 'abort: ok', 'update-ref abort transaction');
}
/** prepare for commit */
async prepare() {
let output = await this.command('prepare', true);
expect(output, 'prepare: ok', 'update-ref prepare transaction');
}
/** commit transaction */
async commit() {
let output = await this.command('commit', true);
expect(output, 'commit: ok', 'update-ref commit transaction');
}
/** update a ref, with optional old value */
async update(ref: string, newValue: string, oldValue?: string) {
await this.command(`update ${ref}\x00${newValue}\x00${oldValue ?? ''}`);
}
/** create a new ref */
async create(ref: string, value: string) {
await this.command(`create ${ref}\x00${value}`);
}
/** verify current value of ref */
async verify(ref: string, value: string | null) {
await this.command(`verify ${ref}\x00${value ?? ''}`);
}
/** set option for next operation */
async option(option: string) {
await this.command(`option ${option}`);
}
/**
* end session
*
* Note: commit must have been run before, or nothing will happen.
*/
async end() {
if (this.proc.exited) throw new Error('already ended');
this.stdin.end();
await EventEmitter.once(this.stdin, 'finish');
await this.proc.wait();
this.proc.assertSucceeded('updating ref');
}
}
export class Repository extends BaseRepository {
/** open a repository at given path */
static async open(repoPath: string) {
let proc = await git(['rev-parse', '--absolute-git-dir'], { cwd: repoPath })
.collectStdout()
.collectStderr()
.wait();
if (!proc.succeeded) {
throw new Error('failed to open repository: ' + proc.collectedStderr!.trim());
}
let gitdir = proc.collectedStdout!.trim();
// ensure config exists, might be needed later
try {
await fsP.stat(path.join(gitdir, 'config'));
} catch (err) {
throw new Error('failed to open repository: could not find config');
}
debug(`open repository: ${repoPath} -> ${gitdir}`);
return new Repository(gitdir);
}
/** initialize a repository at given path */
static async create(repoPath: string) {
let proc = await git(['init', '--bare'], { gitdir: repoPath })
.collectStdout()
.collectStderr()
.wait();
proc.assertSucceeded('create repository');
// clean up sample hooks
let hooksDir = path.join(repoPath, 'hooks');
let hooks = await fsP.readdir(hooksDir);
for (let hook of hooks) {
if (hook.endsWith('.sample')) {
fsP.unlink(path.join(hooksDir, hook));
}
}
// reopen
return this.open(repoPath);
}
/** create a worktree */
async createWorktree(name: string, from: string): Promise<Worktree> {
if (name.includes('/')) throw new Error('worktree name cannot contain slash');
let fromId = await this.revParse(from);
if (!fromId) throw new Error('invalid rev passed as from');
let worktreePath = path.join(this.gitdir, 'worktrees', name);
await fsP.mkdir(worktreePath, { recursive: true });
try {
// ensure new worktree is locked to prevent gc
await fsP.writeFile(
path.join(worktreePath, 'locked'),
'externally managed'
);
// set up common directory
await fsP.writeFile(
path.join(worktreePath, 'commondir'),
'../..',
{ flag: 'wx' }
);
} catch (err: any) {
if (err?.code === 'EEXIST') {
throw new Error('worktree already exists');
} else {
throw err;
}
}
// point gitdir somewhere
// needed so prune doesn't remove objects used by the worktree
await fsP.writeFile(path.join(worktreePath, 'gitdir'), `[managed: ${name}]`);
// set up HEAD
await fsP.writeFile(path.join(worktreePath, 'HEAD'), fromId);
// set up index
let worktree = new Worktree(worktreePath);
await worktree.readTree(fromId);
return worktree;
}
/** remove a worktree */
async removeWorktree(name: string) {
let worktreePath = path.join(this.gitdir, 'worktrees', name);
await fsP.rm(worktreePath, { recursive: true });
}
/**
* open a worktree
*
* If `worktreePath` contains a slash, it is interpreted as a path. Otherwise,
* it is interpreted as a name under .git/worktrees.
*/
async openWorktree(worktreePath: string | null): Promise<Worktree> {
if (worktreePath === null) {
// open "main" worktree
worktreePath = this.gitdir;
} else if (!worktreePath.includes('/')) {
// open path as name
worktreePath = path.join(this.gitdir, 'worktrees', worktreePath);
}
let proc = await git(['rev-parse', '--absolute-git-dir'], { cwd: worktreePath })
.collectStdout()
.collectStderr()
.wait();
if (!proc.succeeded) {
throw new Error('failed to open repository: ' + proc.collectedStderr!.trim());
}
let gitdir = proc.collectedStdout!.trim();
return new Worktree(gitdir);
}
}
export interface IndexEntry {
mode: FileMode,
id: string,
stage: number,
path: string
}
export interface CheckoutIndexOpts {
/** paths to checkout, or 'all' for all paths */
paths: string[] | 'all',
/** whether to overwrite existing files if changed in index */
overwrite?: boolean,
/** destination path */
destination: string
}
export class Worktree extends BaseRepository {
/** read tree object to index */
async readTree(tree: string) {
let proc = await this.git(['read-tree', tree])
.collectStdout()
.collectStderr()
.wait();
proc.assertSucceeded('read tree to index');
}
/** write index to tree object */
async writeTree(): Promise<string> {
let proc = await this.git(['write-tree'])
.collectStdout()
.collectStderr()
.wait();
proc.assertSucceeded('write tree to object database');
return proc.collectedStdout!.trim();
}
/** list entries in index */
async listIndex(): Promise<IndexEntry[]> {
// mode '\x20' id '\x20' stage '\t' path '\x00'
let proc = this.git([
'ls-files',
'-z', // use null-separated paths instead of quoting
'-c', // list cache
'--full-name', // list all entries with full path
'--stage', // use format described above
]);
proc.collectStderr();
let lineReader = new LineReader('\x00');
proc.stdout.pipe(lineReader);
let entries: IndexEntry[] = [];
while (true) {
let line = await lineReader.nextLine();
if (line === null) break;
let [status, path] = line.split('\t');
let [mode, id, stage] = status.split(' ');
entries.push({
mode: +mode,
id,
stage: +stage,
path
});
}
// wait for exit
await proc.wait();
proc.assertSucceeded('read index file');
return entries;
}
/** update the index */
async updateIndex(entries: IndexEntry[]) {
// same format as listIndex
let proc = this.git(['update-index', '--index-info', '-z'], { stdin: true })
.collectStdout()
.collectStderr();
for (let entry of entries) {
let line = `${entry.mode} ${entry.id} ${entry.stage}\t${entry.path}\x00`;
await streamWrite(proc.stdin!, line);
}
proc.stdin!.end();
await proc.wait();
proc.assertSucceeded('update index entries');
}
/** checkout files from index to filesystem */
async checkoutIndex(opts: CheckoutIndexOpts) {
if (typeof opts.overwrite === 'undefined') {
opts.overwrite = true;
}
if (opts.paths instanceof Array) {
let argv = ['checkout-index', '--stdin', '-z']
if (opts.overwrite) argv.push('-f');
let proc = this.git(argv, {
stdin: true,
worktree: opts.destination
})
.collectStdout()
.collectStderr();
for (let path of opts.paths) {
await streamWrite(proc.stdin!, path);
}
proc.stdin!.end();
await proc.wait();
proc.assertSucceeded('checking out files');
} else {
let argv = ['checkout-index', '-a'];
if (opts.overwrite) argv.push('-f');
let proc = await this.git(argv, { worktree: opts.destination })
.collectStdout()
.collectStderr()
.wait();
proc.assertSucceeded('checking out files');
}
}
}
import { ChildProcess, SpawnOptions, spawn } from 'child_process';
import EventEmitter from 'events';
import stream from 'stream';
import createDebug from 'debug';
const debug = createDebug('git:spawn');
export class Deferred<T> {
promise: Promise<T>;
resolve!: (value: T | PromiseLike<T>) => void;
reject!: (reason?: any) => void;
constructor() {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
export class Semaphore {
current = 0;
// wait queue for things currently trying to up()
waitUp: (() => void)[] = [];
// wait queue for things currently trying to down()
waitDown: (() => void)[] = [];
constructor(public max: number) {}
async up(): Promise<void> {
while (true) {
if (this.current < this.max) {
this.current++;
while (this.current > 0) {
let handler = this.waitDown.shift();
if (!handler) break;
handler();
}
return;
} else {
// wait for down() to happen
let deferred = new Deferred<void>();
this.waitUp.push(deferred.resolve);
await deferred.promise;
}
};
}
async down(): Promise<void> {
while (true) {
if (this.current > 0) {
this.current--;
while (this.current < this.max) {
let handler = this.waitUp.shift();
if (!handler) break;
handler();
}
return;
} else {
// wait for up() to happen
let deferred = new Deferred<void>();
this.waitDown.push(deferred.resolve);
await deferred.promise;
}
};
}
}
export class LineReader extends stream.Writable {
delimiter: number;
buffered: Buffer[] = [];
lines: string[] = [];
semaphore: Semaphore;
eof = false;
constructor(delimiter = '\n', maxBufferedLines = 3) {
super();
let delimBuf = Buffer.from(delimiter);
if (delimBuf.length !== 1) throw new Error('invalid delimiter');
this.delimiter = delimBuf[0];
this.semaphore = new Semaphore(maxBufferedLines);
}
_write(chunk_: any, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
if (!(chunk_ instanceof Buffer)) throw new Error('cannot handle non-Buffers');
let chunk: Buffer = chunk_;
(async () => {
let delimIndex = chunk.indexOf(this.delimiter);
while (delimIndex > -1) {
let line = Buffer.concat([
...this.buffered,
chunk.subarray(0, delimIndex)
]);
this.lines.unshift(line.toString());
chunk = chunk.subarray(delimIndex + 1);
delimIndex = chunk.indexOf(this.delimiter);
// wait for space in array
await this.semaphore.up();
}
// no more delimiters in chunk
if (chunk.length > 0) {
this.buffered.push(chunk);
}
callback(null);
})();
}
_final(callback: (error?: Error | null) => void) {
this.eof = true;
// ensure eof notified
this.semaphore.up();
callback(null);
}
async nextLine(): Promise<string | null> {
while (true) {
await this.semaphore.down();
let line = this.lines.pop();
if (typeof line === 'undefined') {
if (this.eof) {
// ensure others do not block on eof
this.semaphore.up();
return null;
}
// wait again
continue;
} else {
return line;
}
}
}
}
export interface GitExecOptions {
// directory of git repository
gitdir?: string | null,
// the GIT_WORK_TREE option, not to be confused with git-worktree
worktree?: string | null,
// configuration overrides
config?: Record<string, string>,
// environment overrides
env?: Record<string, string>,
// working directory for git
cwd?: string | null,
// whether stdin is needed
stdin?: boolean,
// additional fds to open
additionalFds?: Exclude<SpawnOptions['stdio'], string>
}
export class GitProcess {
stdin: stream.Writable | null = null;
stdout: stream.Readable;
stderr: stream.Readable;
exited = false;
exitStatus: number | string | null = null;
collectedStdout: string | null = null;
collectedStderr: string | null = null;
constructor(public process: ChildProcess) {
this.stdout = process.stdout!;
this.stderr = process.stderr!;
if (process.stdin) this.stdin = process.stdin;
process.once('exit', (status, code) => {
if (status !== null) this.exitStatus = status;
else if (code !== null) this.exitStatus = code;
else throw new Error(); // at least one must not be null
this.exited = true;
debug('git exited with', this.exitStatus);
});
}
get succeeded(): boolean {
if (!this.exited) throw new Error('process did not exit yet');
return this.exitStatus === 0;
}
collectStdout() {
if (this.collectedStdout !== null) return this;
this.collectedStdout = '';
this.stdout.on('data', d => this.collectedStdout += d.toString());
return this;
}
collectStderr() {
if (this.collectedStderr !== null) return this;
this.collectedStderr = '';
this.stderr.on('data', d => this.collectedStderr += d.toString());
return this;
}
async wait(): Promise<this> {
if (this.collectedStdout !== null && !this.stdout.readableEnded) {
await EventEmitter.once(this.process.stdout!, 'end');
}
if (this.collectedStderr !== null && !this.stderr.readableEnded) {
await EventEmitter.once(this.process.stderr!, 'end');
}
if (this.exited) return this;
await EventEmitter.once(this.process, 'exit');
return this;
}
assertSucceeded(what?: string) {
if (!this.exited) throw new Error('process did not exit yet');
if (!this.succeeded) {
let message = 'git command failed';
if (what) message += ` (${what})`;
if (this.collectedStderr) {
message += '\n\nstderr:\n' + this.collectStderr;
}
throw new Error(message);
}
}
}
export function git(argv: string[], opts: GitExecOptions): GitProcess {
let fullOpts: Required<GitExecOptions> = Object.assign({
gitdir: null,
config: {},
env: {},
worktree: null,
cwd: null,
stdin: false,
additionalFds: [],
}, opts);
let baseArgs = [];
if (fullOpts.gitdir) baseArgs.push('--git-dir', fullOpts.gitdir);
if (fullOpts.worktree) baseArgs.push('--work-tree', fullOpts.worktree);
for (let [key, value] of Object.entries(fullOpts.config)) {
baseArgs.push('-c', `${key}=${value}`);
}
let spawnOpts: SpawnOptions = {
argv0: 'git',
stdio: [fullOpts.stdin ? 'pipe' : 'ignore', 'pipe', 'pipe', ...fullOpts.additionalFds],
};
if (fullOpts.cwd) spawnOpts.cwd = fullOpts.cwd;
if (Object.entries(fullOpts.env).length) {
spawnOpts.env = Object.assign({}, process.env, fullOpts.env);
}
let spawnArgv = [...baseArgs, ...argv];
let proc = spawn('git', spawnArgv, spawnOpts);
debug('command: git', spawnArgv);
return new GitProcess(proc);
}

git screwery

begin

git init --bare <repo>

Creates bare repository. No worktree attached by default. <repo> will be referred to as .git for brevity.

worktrees

Worktrees exist in .git/worktrees/<name> and have a few files:

  • HEAD: ref where the worktree is currently "based on"
    • usually contains a commit id or a symbolic ref (such as ref: refs/heads/master)
  • commondir: pointing to parent .git directory
    • usually just ../..
  • index: index file for the worktree, contains git's view of the worktree, including staged files
    • after creating HEAD and commondir, create index with git --git-dir .git/worktrees/<name> read-tree HEAD
  • gitdir: supposed to point to the "real" .git of the worktree
    • must point to an existing file or git gc will remove the entire worktree
    • unless locked exists, in which case the referenced file does not need to exist
    • must exist, or prune may remove objects still in use by the worktree
  • locked: marks worktree as "locked" to prevent git gc from removing it
    • must exist if gitdir does not exist or points to a nonexistent file
    • existence is enough, but it can also contain a locked reason

After worktree creation, simply set GIT_DIR=.git/worktrees/<name> to use its index and HEAD for operations.

misc

The -z argument can be given for \x00 separated output in many cases, bypassing quoting.

The "stage number" is usually only relevant while merging.

  • 0: normal operation
  • 1: merge-base file
  • 2: "ours" version
  • 3: "theirs" version
# list all objects and their types
git cat-file --batch-check --batch-all-objects

# manual commit from index
# -p: specifies parent commit
tree="$(git write-tree)"
git commit-tree -c user.name=iczero -c user.email=iczero@hellomouse.net commit-tree -m "message" -p HEAD "$tree"

# diff from rev to index
# -p to generate patch, --cached to use index only
git diff-index -p --cached "$rev"

# remove all unreachable reflog entries
git reflog expire --expire=now

# gc loose objects
git gc --prune=now

# write blob to object store
# -t: object type
# -w: write object to store
cat blob | git hash-object -t blob -w --stdin
# read blob from object store
# "blob" can be another type, but they will be returned raw
git cat-file blob "$id"

# read tree or commit from object store
# --full-tree: do not consider working directory
# -r: recurse children trees
# -t: show trees as they are recursed
# can use -z
git ls-tree --full-tree -r -t "$id"

# list files in index
# can use -z
git ls-files -c --full-name --stage

# copy tree to index
git read-tree "$rev"

# update index from stdin
# --index-info: read index information from stdin
# --add, --remove, --replace not needed when using --index-info
# to remove entry, set mode to 0
# can use -z
# can use same format as ls-files --stage
echo "100755 $id 0"$'\t'"$path" | git update-index --index-info

# update ref
# with --stdin, supports transactions; see man page
git update-ref "$ref" "$id"

# update symbolic ref
git symbolic-ref HEAD refs/heads/master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment