Skip to content

Commit

Permalink
feat(git): introduce the git module
Browse files Browse the repository at this point in the history
  • Loading branch information
denizenging committed Oct 15, 2023
1 parent b822da6 commit 2375b25
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 10 deletions.
1 change: 1 addition & 0 deletions pkg/git/.npmignore
38 changes: 38 additions & 0 deletions pkg/git/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@slangroom/git",
"version": "1.0.0",
"dependencies": {
"@slangroom/core": "workspace:*",
"isomorphic-git": "^1.24.5"
},
"repository": "https://github.com/dyne/slangroom",
"license": "AGPL-3.0-only",
"type": "module",
"main": "./build/cjs/src/index.js",
"types": "./build/cjs/src/index.d.ts",
"exports": {
".": {
"import": {
"types": "./build/esm/src/index.d.ts",
"default": "./build/esm/src/index.js"
},
"require": {
"types": "./build/cjs/src/index.d.ts",
"default": "./build/cjs/src/index.js"
}
},
"./*": {
"import": {
"types": "./build/esm/src/*.d.ts",
"default": "./build/esm/src/*.js"
},
"require": {
"types": "./build/cjs/src/*.d.ts",
"default": "./build/cjs/src/*.js"
}
}
},
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions pkg/git/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '@slangroom/git/plugins';
131 changes: 131 additions & 0 deletions pkg/git/src/plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { PluginContext, PluginResult } from '@slangroom/core';
import git from 'isomorphic-git';
// TODO: why does this require index.js?
import http from 'isomorphic-git/http/node/index.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';

/**
* @internal
*/
export const sandboxDir = () => {
// TODO: sanitize sandboxDir
const ret = process.env['FILES_DIR'];
if (!ret) throw new Error('$FILES_DIR must be provided');
return ret;
};

const sandboxizeDir = (unsafe: string) => {
const normalized = path.normalize(unsafe);
// `/` and `..` prevent directory traversal
const doesDirectoryTraversal = normalized.startsWith('/') || normalized.startsWith('..');
// Unlike `resolveFilepath`, we allow `.` to be used here, obviously.
if (doesDirectoryTraversal) return { error: `dirpath is unsafe: ${unsafe}` };
return { dirpath: path.join(sandboxDir(), normalized) };
};

const sandboxizeFile = (sandboxdir: string, unsafe: string) => {
const normalized = path.normalize(unsafe);
// `/` and `..` prevent directory traversal
const doesDirectoryTraversal = normalized.startsWith('/') || normalized.startsWith('..');
// `.` ensures that `foo/bar` or `./foo` is valid, while `.` isn't
// (that is, no "real" filepath is provided)
const DoesntProvideFile = normalized.startsWith('.');
if (doesDirectoryTraversal || DoesntProvideFile)
return { error: `filepath is unsafe: ${unsafe}` };
return { filepath: path.join(sandboxdir, normalized) };
};

/**
* @internal
*/
export const executeVerifyGitRepository = async (ctx: PluginContext): Promise<PluginResult> => {
const unsafe = ctx.fetchOpen()[0];
const { dirpath, error } = sandboxizeDir(unsafe);
if (!dirpath) return ctx.fail(error);

try {
await git.findRoot({ fs: fs, filepath: dirpath });
return ctx.pass(null);
} catch (e) {
return ctx.fail(e);
}

Check warning on line 52 in pkg/git/src/plugins.ts

View check run for this annotation

Codecov / codecov/patch

pkg/git/src/plugins.ts#L51-L52

Added lines #L51 - L52 were not covered by tests
};

/*
* @internal
*/
export const executeCloneRepository = async (ctx: PluginContext): Promise<PluginResult> => {
const repoUrl = ctx.fetchConnect()[0];
const unsafe = ctx.fetch('path');
if (typeof unsafe !== 'string') return ctx.fail('path must be string');

const { dirpath, error } = sandboxizeDir(unsafe);
if (!dirpath) return ctx.fail(error);

await git.clone({ fs: fs, http: http, dir: dirpath, url: repoUrl });
return ctx.pass(null);
};

/*
* @internal
*/
export const executeCreateNewGitCommit = async (ctx: PluginContext): Promise<PluginResult> => {
const unsafe = ctx.fetchOpen()[0];
const { dirpath, error } = sandboxizeDir(unsafe);
if (!dirpath) return ctx.fail(error);

const commit = ctx.fetch('commit') as {
message: string;
author: string;
email: string;
files: string[];
};

try {
commit.files.map((unsafe) => {
const { filepath, error: ferror } = sandboxizeFile(dirpath, unsafe);
if (!filepath) throw ferror;
return filepath;
});
} catch (e) {
return ctx.fail(e);
}

Check warning on line 93 in pkg/git/src/plugins.ts

View check run for this annotation

Codecov / codecov/patch

pkg/git/src/plugins.ts#L92-L93

Added lines #L92 - L93 were not covered by tests

await Promise.all(
commit.files.map((safe) => {
return git.add({
fs: fs,
dir: dirpath,
filepath: safe,
});
})
);

const hash = await git.commit({
fs: fs,
dir: dirpath,
message: commit.message,
author: {
name: commit.author,
email: commit.email,
},
});

return ctx.pass(hash);
};

const gitPlugin = async (ctx: PluginContext): Promise<PluginResult> => {
switch (ctx.phrase) {
case 'verify git repository':
return await executeVerifyGitRepository(ctx);
case 'clone repository':
return await executeCloneRepository(ctx);
case 'create new git commit':
return await executeCreateNewGitCommit(ctx);
default:
return ctx.fail('no match');
}
};

Check warning on line 129 in pkg/git/src/plugins.ts

View check run for this annotation

Codecov / codecov/patch

pkg/git/src/plugins.ts#L119-L129

Added lines #L119 - L129 were not covered by tests

export const gitPlugins = new Set([gitPlugin]);
66 changes: 66 additions & 0 deletions pkg/git/test/plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import ava, { type TestFn } from 'ava';
import * as fs from 'node:fs/promises';
import { join } from 'node:path';
import * as os from 'node:os';
import git from 'isomorphic-git';
import { PluginContextTest } from '@slangroom/core';
import {
executeCloneRepository,
executeCreateNewGitCommit,
executeVerifyGitRepository,
} from '@slangroom/git';

const test = ava as TestFn<string>;

test.beforeEach(async (t) => {
const tmpdir = await fs.mkdtemp(join(os.tmpdir(), 'slangroom-test-'));
process.env['FILES_DIR'] = tmpdir;
t.context = tmpdir;
});

test.afterEach(async (t) => await fs.rm(t.context, { recursive: true }));

test.serial('verifyGitRepository works', async (t) => {
const path = join('foo', 'bar');
const dir = join(t.context, path);
await git.init({ fs: fs, dir: dir });
const ctx = PluginContextTest.openconnect(path);
const res = await executeVerifyGitRepository(ctx);
t.deepEqual(res, { ok: true, value: null });
});

// TODO: somehow make this work with nock using dumb http
test.serial('cloneRepository works', async (t) => {
const path = join('foo', 'bar');
const dir = join(t.context, path);
const ctx = new PluginContextTest('https://github.com/srfsh/dumb', {
path: path,
});
const res = await executeCloneRepository(ctx);
t.deepEqual(res, { ok: true, value: null });
const content = await fs.readFile(join(dir, 'README.md'));
t.is(content.toString(), '# dumb\nA repo only for testing. It shall never change.\n');
});

test.serial('createNewCommit works', async (t) => {
const path = join('foo', 'bar');
const dir = join(t.context, path);
await git.init({ fs: fs, dir: dir });
const files = ['file0.txt', 'file1.txt'];
files.forEach(async (f) => await fs.appendFile(join(dir, f), `test data ${f}`));
const commitParams = {
message: 'my message',
author: 'my author',
email: '[email protected]',
files: files,
};
const ctx = new PluginContextTest(path, { commit: commitParams });
const res = await executeCreateNewGitCommit(ctx);
const hash = await git.resolveRef({ fs: fs, dir: dir, ref: 'HEAD' });
t.deepEqual(res, { ok: true, value: hash });
const { commit } = await git.readCommit({ fs: fs, dir: dir, oid: hash });
t.is(commit.author.name, commitParams.author);
t.is(commit.author.email, commitParams.email);
// newline is required
t.is(commit.message, `${commitParams.message}\n`);
});
1 change: 1 addition & 0 deletions pkg/git/tsconfig.json
Loading

0 comments on commit 2375b25

Please sign in to comment.