Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(git): make git plugin usable on browser #189

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pkg/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"dependencies": {
"@slangroom/core": "workspace:*",
"@slangroom/deps": "workspace:*",
"@slangroom/git": "workspace:*",
"@slangroom/helpers": "workspace:*",
"@slangroom/http": "workspace:*",
"@slangroom/json-schema": "workspace:*",
Expand Down Expand Up @@ -43,7 +44,7 @@
"esbuild": "^0.21.4"
},
"scripts": {
"build": "pnpm exec esbuild --bundle src/index.ts --outfile=build/slangroom.js --target=es2016 --external:fs --external:path --external:crypto && cp build/slangroom.js public"
"build": "pnpm exec esbuild --bundle src/index.ts --outfile=build/slangroom.js --target=ESNext --external:fs --external:path --external:crypto && cp build/slangroom.js public"
},
"engines": {
"node": "^18.20.0 || ^20.10.0 || ^22"
Expand Down
5 changes: 5 additions & 0 deletions pkg/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { Slangroom, version as coreVersion } from '@slangroom/core';
import { qrcode, version as qrcodeVersion } from '@slangroom/qrcode';
import { git, version as gitVersion } from '@slangroom/git';
import { http, version as httpVersion } from '@slangroom/http';
import { pocketbase, version as pocketbaseVersion } from '@slangroom/pocketbase';
import { helpers, version as helpersVersion } from '@slangroom/helpers';
Expand All @@ -14,6 +15,10 @@ import packageJson from '@slangroom/browser/package.json' with { type: 'json' };
export const version = packageJson.version;

const plugins_dict = {
git: {
plugin: git,
version: gitVersion
},
http: {
plugin: http,
version: httpVersion
Expand Down
7 changes: 6 additions & 1 deletion pkg/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"version": "1.39.0",
"dependencies": {
"@slangroom/core": "workspace:*",
"isomorphic-git": "^1.25.10"
"@zenfs/core": "^0.16.3",
"isomorphic-git": "^1.25.10",
"path-browserify": "^1.0.1"
},
"repository": "https://github.com/dyne/slangroom",
"license": "AGPL-3.0-only",
Expand Down Expand Up @@ -38,5 +40,8 @@
},
"engines": {
"node": "^18.20.0 || ^20.10.0 || >=22.0.0 <22.3.0 || ^22.4.0"
},
"devDependencies": {
"@types/path-browserify": "^1.0.2"
}
}
116 changes: 61 additions & 55 deletions pkg/git/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,23 @@

import { Plugin } from '@slangroom/core';
import gitpkg 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';
import http from 'isomorphic-git/http/web/index.js';
import { promises as fs } from '@zenfs/core';
import path from 'path-browserify';
// read the version from the package.json
import packageJson from '@slangroom/git/package.json' with { type: 'json' };

export const version = packageJson.version;

export class GitError extends Error {
constructor(e: string) {
super(e)
this.name = 'Slangroom @slangroom/git@' + packageJson.version + ' Error'
}
constructor(e: string) {
super(e);
this.name = 'Slangroom @slangroom/git@' + packageJson.version + ' Error';
}
}

type dirResponse = { ok: true; dirpath: string } | { ok: false; error: string };
type fileResponse = { ok: true; filepath: string } | { ok: false; error: string };
/**
* @internal
*/
Expand All @@ -28,7 +29,7 @@ export const sandboxDir = () => {
return process.env['FILES_DIR'];
};

const sandboxizeDir = (unsafe: string): ({ok: true, dirpath: string} | {ok: false, error: string}) => {
const sandboxizeDir = (unsafe: string): dirResponse => {
const normalized = path.normalize(unsafe);
// `/` and `..` prevent directory traversal
const doesDirectoryTraversal = normalized.startsWith('/') || normalized.startsWith('..');
Expand All @@ -39,7 +40,7 @@ const sandboxizeDir = (unsafe: string): ({ok: true, dirpath: string} | {ok: fals
return { ok: true, dirpath: path.join(sandboxdir, normalized) };
};

const sandboxizeFile = (sandboxdir: string, unsafe: string): ({ok: true, filepath: string} | {ok: false, error: string}) => {
const sandboxizeFile = (sandboxdir: string, unsafe: string): fileResponse => {
const normalized = path.normalize(unsafe);
// `/` and `..` prevent directory traversal
const doesDirectoryTraversal = normalized.startsWith('/') || normalized.startsWith('..');
Expand Down Expand Up @@ -79,7 +80,7 @@ export const cloneRepository = p.new('connect', ['path'], 'clone repository', as

const res = sandboxizeDir(unsafe);
if (!res.ok) return ctx.fail(new GitError(res.error));
try {
try {
await gitpkg.clone({ fs: fs, http: http, dir: res.dirpath, url: repoUrl });
return ctx.pass(null);
} catch (e) {
Expand All @@ -90,49 +91,54 @@ export const cloneRepository = p.new('connect', ['path'], 'clone repository', as
/*
* @internal
*/
export const createNewGitCommit = p.new('open', ['commit'], 'create new git commit', async (ctx) => {
const unsafe = ctx.fetchOpen()[0];
const res = sandboxizeDir(unsafe);
if (!res.ok) return ctx.fail(new GitError(res.error));

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

try {
commit.files.map((unsafe) => {
const r = sandboxizeFile(res.dirpath, unsafe);
if (!r.ok) throw new Error(r.error);
return r.filepath;
});

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

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

return ctx.pass(hash);
} catch (e) {
return ctx.fail(new GitError(e.message));
}
});
export const createNewGitCommit = p.new(
'open',
['commit'],
'create new git commit',
async (ctx) => {
const unsafe = ctx.fetchOpen()[0];
const res = sandboxizeDir(unsafe);
if (!res.ok) return ctx.fail(new GitError(res.error));

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

try {
commit.files.map((unsafe) => {
const r = sandboxizeFile(res.dirpath, unsafe);
if (!r.ok) throw new Error(r.error);
return r.filepath;
});

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

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

return ctx.pass(hash);
} catch (e) {
return ctx.fail(new GitError(e.message));
}
},
);

export const git = p;
52 changes: 27 additions & 25 deletions pkg/git/test/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,32 @@
import test from 'ava';
import { Slangroom } from '@slangroom/core';
import { git } from '@slangroom/git';
import * as fs from 'node:fs/promises';
import { promises as fs } from '@zenfs/core';
// read the version from the package.json
import packageJson from '@slangroom/git/package.json' with { type: 'json' };

process.env['FILES_DIR'] = "./test";
process.env['FILES_DIR'] = './test';

const stripAnsiCodes = (str: string) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
const stripAnsiCodes = (str: string) =>
str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');

test('Should fail to check non existing repository', async (t) => {
const slangroom = new Slangroom(git);
const data = {
path: 'some/dumb/path',
verified_git_repo: 'true'
verified_git_repo: 'true',
};
const zen =`Rule unknown ignore
const zen = `Rule unknown ignore
Given I open 'path' and verify git repository
Given I have a 'string' named 'verified_git_repo'
Then print the data`;
const fn = slangroom.execute(zen, {
data,
});
const error = await t.throwsAsync(fn);
t.is(stripAnsiCodes((error as Error).message),
`0 | Rule unknown ignore
t.is(
stripAnsiCodes((error as Error).message),
`0 | Rule unknown ignore
1 | Given I open 'path' and verify git repository
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | Given I have a 'string' named 'verified_git_repo'
Expand All @@ -47,8 +49,9 @@ Heap:
"path": "some/dumb/path",
"verified_git_repo": "true"
}
`);
})
`,
);
});

test('Should not clone a not exitsting repository', async (t) => {
const slangroom = new Slangroom(git);
Expand All @@ -66,8 +69,9 @@ test('Should not clone a not exitsting repository', async (t) => {
data,
});
const error = await t.throwsAsync(fn);
t.is(stripAnsiCodes((error as Error).message),
`0 | Rule unknown ignore
t.is(
stripAnsiCodes((error as Error).message),
`0 | Rule unknown ignore
1 | Given I connect to 'url' and send path 'path' and clone repository
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | Given I have a 'string' named 'cloned_repository'
Expand All @@ -87,9 +91,9 @@ Heap:
"path": "another/dumb/path",
"cloned_repository": "true"
}
`);
})

`,
);
});

test.serial('Should clone a repository', async (t) => {
const slangroom = new Slangroom(git);
Expand All @@ -113,15 +117,15 @@ test.serial('Should clone a repository', async (t) => {
},
res.logs,
);
})
});

test.serial('Should verify git repository', async (t) => {
const slangroom = new Slangroom(git);
const data = {
path: 'dumb',
verified_git_repo: 'true'
verified_git_repo: 'true',
};
const zen =`Rule unknown ignore
const zen = `Rule unknown ignore
Given I open 'path' and verify git repository
Given I have a 'string' named 'verified_git_repo'
Then print data`;
Expand All @@ -135,7 +139,7 @@ test.serial('Should verify git repository', async (t) => {
},
res.logs,
);
})
});

test.serial('Should create a new git commit', async (t) => {
const slangroom = new Slangroom(git);
Expand All @@ -145,22 +149,20 @@ test.serial('Should create a new git commit', async (t) => {
author: 'Jhon Doe',
message: 'docs: update readme',
email: '[email protected]',
files: [
'README.md'
]
files: ['README.md'],
},
};
const zen =`Rule unknown ignore
const zen = `Rule unknown ignore
Given I open 'path' and send commit 'commit' and create new git commit and output into 'commit_hash'
Given I have a 'string' named 'commit hash'
Then print data`;
await fs.appendFile('./test/dumb/README.md', '\nChanged the README\n')
await fs.appendFile('./test/dumb/README.md', '\nChanged the README\n');
const res = await slangroom.execute(zen, {
data,
});
t.truthy(typeof res.result['commit_hash'] === 'string');
})
});

test.after.always('guaranteed cleanup', async () => {
await fs.rm('./test/dumb', { recursive: true })
await fs.rm('./test/dumb', { recursive: true });
});
13 changes: 8 additions & 5 deletions pkg/git/test/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

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 { promises as fs } from '@zenfs/core';
import path from 'path-browserify';
import git from 'isomorphic-git';
import { PluginContextTest } from '@slangroom/core';
import { cloneRepository, createNewGitCommit, verifyGitRepository } from '@slangroom/git';

const test = ava as TestFn<string>;
const join = path.join;

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

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

test.serial('verifyGitRepository works', async (t) => {
const path = join('foo', 'bar');
Expand Down
Loading
Loading