mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
Merge branch 'main' into feat/pdg-impact-mode
This commit is contained in:
commit
f4d4034994
5 changed files with 159 additions and 17 deletions
|
|
@ -1,15 +1,19 @@
|
|||
/**
|
||||
* Server Mapping Configuration
|
||||
*
|
||||
* Reads ~/.gitnexus/server-mapping.json to map repo names to service names.
|
||||
* Reads getGlobalDir()/server-mapping.json to map repo names to service names.
|
||||
* Used in embedding text to enrich metadata with microservice context.
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { getGlobalDir } from '../../storage/repo-manager.js';
|
||||
|
||||
const MAPPING_FILE = path.join(os.homedir(), '.gitnexus', 'server-mapping.json');
|
||||
// Sourced from getGlobalDir() so it honors GITNEXUS_HOME (the Docker image sets
|
||||
// GITNEXUS_HOME=/data/gitnexus); falls back to ~/.gitnexus when unset. Wrapped in
|
||||
// path.resolve() for parity with the clone/upload roots (git-clone.ts CLONE_ROOT,
|
||||
// upload-paths.ts UPLOAD_ROOT) so a relative GITNEXUS_HOME still yields an absolute path.
|
||||
const MAPPING_FILE = path.resolve(path.join(getGlobalDir(), 'server-mapping.json'));
|
||||
|
||||
let cachedMapping: Record<string, string> | null = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,29 @@
|
|||
/**
|
||||
* Git Clone Utility
|
||||
*
|
||||
* Shallow-clones repositories into ~/.gitnexus/repos/{name}/.
|
||||
* Shallow-clones repositories into the clone root (getGlobalDir()/repos/{name}/).
|
||||
* If already cloned, does git pull instead.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import fs from 'fs/promises';
|
||||
import { isIP } from 'net';
|
||||
import { logger } from '../core/logger.js';
|
||||
import { parseRepoNameFromUrl } from '../storage/git.js';
|
||||
import { getGlobalDir } from '../storage/repo-manager.js';
|
||||
|
||||
/** Root directory for all cloned repositories. Targets must resolve inside this. */
|
||||
const CLONE_ROOT = path.resolve(path.join(os.homedir(), '.gitnexus', 'repos'));
|
||||
/**
|
||||
* Root directory for all cloned repositories. Targets must resolve inside this.
|
||||
*
|
||||
* Sourced from getGlobalDir() so it honors GITNEXUS_HOME — the Docker image sets
|
||||
* GITNEXUS_HOME=/data/gitnexus, the persistent volume that also holds the
|
||||
* registry and indexes. Without this, clones landed in the container's
|
||||
* ephemeral ~/.gitnexus/repos and were lost on container recreation while the
|
||||
* registry still pointed at the dead path. Falls back to ~/.gitnexus when the
|
||||
* env var is unset (CLI / local installs), matching the prior behavior exactly.
|
||||
*/
|
||||
const CLONE_ROOT = path.resolve(path.join(getGlobalDir(), 'repos'));
|
||||
|
||||
// A valid git repository name is filesystem-safe: alphanumerics plus `. _ -`.
|
||||
// Rejecting anything else (including `..`, `/`, `\`, shell metacharacters)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Upload working-directory paths.
|
||||
*
|
||||
* Browser folder uploads are written into ~/.gitnexus/uploads/{name}/ — a
|
||||
* Browser folder uploads are written into getGlobalDir()/uploads/{name}/ — a
|
||||
* sibling of the clone root (git-clone.ts CLONE_ROOT) — so an uploaded repo
|
||||
* persists and behaves like a cloned one (the graph UI's /api/file reads its
|
||||
* files after analysis, and DELETE /api/repo removes it). Staging happens in
|
||||
|
|
@ -12,12 +12,18 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { sanitizeRepoName } from '../storage/git.js';
|
||||
import { REPO_NAME_PATTERN } from './git-clone.js';
|
||||
import { getGlobalDir } from '../storage/repo-manager.js';
|
||||
|
||||
/** Root directory for all uploaded repositories. Targets must resolve inside this. */
|
||||
export const UPLOAD_ROOT = path.resolve(path.join(os.homedir(), '.gitnexus', 'uploads'));
|
||||
/**
|
||||
* Root directory for all uploaded repositories. Targets must resolve inside this.
|
||||
*
|
||||
* Sourced from getGlobalDir() so it honors GITNEXUS_HOME and stays a sibling of
|
||||
* the clone root on the same (in Docker, persistent) volume. Falls back to
|
||||
* ~/.gitnexus when the env var is unset.
|
||||
*/
|
||||
export const UPLOAD_ROOT = path.resolve(path.join(getGlobalDir(), 'uploads'));
|
||||
|
||||
/** Prefix for per-upload staging directories created under UPLOAD_ROOT. */
|
||||
export const STAGING_PREFIX = '.staging-';
|
||||
|
|
|
|||
|
|
@ -31,6 +31,16 @@ import os from 'node:os';
|
|||
import fs from 'node:fs/promises';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { getRemoteOriginUrl } from '../../src/storage/git.js';
|
||||
import { getGlobalDir } from '../../src/storage/repo-manager.js';
|
||||
|
||||
// CLONE_ROOT now derives from getGlobalDir() (GITNEXUS_HOME || ~/.gitnexus), so
|
||||
// assertions must mirror that derivation rather than hardcoding ~/.gitnexus —
|
||||
// otherwise an ambient GITNEXUS_HOME (e.g. a CI runner that sets it) makes the
|
||||
// "direct child of the clone root" assertions fail. We call getGlobalDir()
|
||||
// directly (the same function production CLONE_ROOT uses) so the test cannot
|
||||
// drift from production if that derivation ever changes. Computed at module
|
||||
// load, the same point CLONE_ROOT is frozen, so the two always agree.
|
||||
const EXPECTED_CLONE_ROOT = path.resolve(path.join(getGlobalDir(), 'repos'));
|
||||
|
||||
describe('git-clone', () => {
|
||||
describe('extractRepoName', () => {
|
||||
|
|
@ -112,11 +122,9 @@ describe('git-clone', () => {
|
|||
});
|
||||
|
||||
describe('getCloneDir', () => {
|
||||
it('returns path under ~/.gitnexus/repos/', () => {
|
||||
it('returns path under the clone root (getGlobalDir()/repos/)', () => {
|
||||
const dir = getCloneDir('my-repo');
|
||||
expect(dir).toContain('.gitnexus');
|
||||
expect(dir).toMatch(/repos/);
|
||||
expect(dir).toContain('my-repo');
|
||||
expect(dir).toBe(path.join(EXPECTED_CLONE_ROOT, 'my-repo'));
|
||||
});
|
||||
|
||||
it('rejects ".." to prevent path-traversal escape from the clone root', () => {
|
||||
|
|
@ -131,7 +139,7 @@ describe('git-clone', () => {
|
|||
});
|
||||
|
||||
it('returned path is always a direct child of the clone root', () => {
|
||||
const cloneRoot = path.resolve(path.join(os.homedir(), '.gitnexus', 'repos'));
|
||||
const cloneRoot = EXPECTED_CLONE_ROOT;
|
||||
const dir = getCloneDir('my-repo');
|
||||
const rel = path.relative(cloneRoot, path.resolve(dir));
|
||||
// path.relative from the parent to the child must be just the child name —
|
||||
|
|
@ -476,7 +484,7 @@ describe('git-clone', () => {
|
|||
//
|
||||
// These tests do NOT mock spawn — the barrier throws synchronously
|
||||
// before git is invoked, so the rejection is observable directly.
|
||||
const cloneRoot = path.resolve(path.join(os.homedir(), '.gitnexus', 'repos'));
|
||||
const cloneRoot = EXPECTED_CLONE_ROOT;
|
||||
|
||||
it('rejects an absolute target outside CLONE_ROOT', async () => {
|
||||
await expect(cloneOrPull('https://github.com/a/b.git', '/etc/passwd')).rejects.toThrow(
|
||||
|
|
|
|||
115
gitnexus/test/unit/gitnexus-home-roots.test.ts
Normal file
115
gitnexus/test/unit/gitnexus-home-roots.test.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
/**
|
||||
* Regression guard: the clone root (git-clone.ts), upload root (upload-paths.ts),
|
||||
* and server-mapping file (embeddings/server-mapping.ts) must follow
|
||||
* GITNEXUS_HOME, not the bare home directory.
|
||||
*
|
||||
* The Docker image sets GITNEXUS_HOME=/data/gitnexus — the persistent volume
|
||||
* that also holds the registry and indexes. Before this fix these three roots
|
||||
* used os.homedir() directly, so clones/uploads landed in the container's
|
||||
* ephemeral ~/.gitnexus and were lost on container recreation while the
|
||||
* registry (which honors GITNEXUS_HOME) still pointed at the dead path.
|
||||
*
|
||||
* The roots are module-level constants resolved at import time from
|
||||
* getGlobalDir(), so each case sets the env var, resets the module registry,
|
||||
* and re-imports under the new value.
|
||||
*/
|
||||
describe('GITNEXUS_HOME path roots', () => {
|
||||
const savedHome = process.env.GITNEXUS_HOME;
|
||||
|
||||
afterEach(() => {
|
||||
if (savedHome === undefined) delete process.env.GITNEXUS_HOME;
|
||||
else process.env.GITNEXUS_HOME = savedHome;
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
// customHome is only needed by the GITNEXUS_HOME-set cases below; the two
|
||||
// fallback cases manage their own (or no) temp dir, so its lifecycle lives in
|
||||
// this nested block rather than a shared beforeEach.
|
||||
describe('with GITNEXUS_HOME set', () => {
|
||||
// mkdtemp (not a fixed os.tmpdir() name) so a co-located process cannot
|
||||
// pre-create or symlink the path before our writes land
|
||||
// (CodeQL js/insecure-temporary-file).
|
||||
let customHome: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
customHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-home-roots-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(customHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('clone root follows GITNEXUS_HOME', async () => {
|
||||
process.env.GITNEXUS_HOME = customHome;
|
||||
vi.resetModules();
|
||||
const { getCloneDir } = await import('../../src/server/git-clone.js');
|
||||
expect(getCloneDir('my-repo')).toBe(path.resolve(customHome, 'repos', 'my-repo'));
|
||||
});
|
||||
|
||||
it('upload root follows GITNEXUS_HOME', async () => {
|
||||
process.env.GITNEXUS_HOME = customHome;
|
||||
vi.resetModules();
|
||||
const { UPLOAD_ROOT, getUploadDir } = await import('../../src/server/upload-paths.js');
|
||||
expect(UPLOAD_ROOT).toBe(path.resolve(customHome, 'uploads'));
|
||||
expect(getUploadDir('my-repo')).toBe(path.resolve(customHome, 'uploads', 'my-repo'));
|
||||
});
|
||||
|
||||
it('server-mapping file is read from GITNEXUS_HOME', async () => {
|
||||
process.env.GITNEXUS_HOME = customHome;
|
||||
await fs.writeFile(
|
||||
path.join(customHome, 'server-mapping.json'),
|
||||
JSON.stringify({ 'my-repo': 'payments-service' }),
|
||||
'utf-8',
|
||||
);
|
||||
vi.resetModules();
|
||||
const { readServerMapping } = await import('../../src/core/embeddings/server-mapping.js');
|
||||
expect(await readServerMapping('my-repo')).toBe('payments-service');
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to ~/.gitnexus when GITNEXUS_HOME is unset', async () => {
|
||||
delete process.env.GITNEXUS_HOME;
|
||||
vi.resetModules();
|
||||
const { getCloneDir } = await import('../../src/server/git-clone.js');
|
||||
const { UPLOAD_ROOT } = await import('../../src/server/upload-paths.js');
|
||||
expect(getCloneDir('my-repo')).toBe(
|
||||
path.resolve(os.homedir(), '.gitnexus', 'repos', 'my-repo'),
|
||||
);
|
||||
expect(UPLOAD_ROOT).toBe(path.resolve(os.homedir(), '.gitnexus', 'uploads'));
|
||||
});
|
||||
|
||||
it('server-mapping falls back to ~/.gitnexus when GITNEXUS_HOME is unset', async () => {
|
||||
// os.homedir() honors $HOME (and $USERPROFILE on Windows), so redirect the
|
||||
// home dir to a tmp path to exercise the real fallback (getGlobalDir() ->
|
||||
// ~/.gitnexus) without writing into the developer's actual
|
||||
// ~/.gitnexus/server-mapping.json.
|
||||
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-fallback-home-'));
|
||||
const savedProcessHome = process.env.HOME;
|
||||
const savedUserProfile = process.env.USERPROFILE;
|
||||
delete process.env.GITNEXUS_HOME;
|
||||
process.env.HOME = fakeHome;
|
||||
process.env.USERPROFILE = fakeHome;
|
||||
try {
|
||||
await fs.mkdir(path.join(fakeHome, '.gitnexus'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(fakeHome, '.gitnexus', 'server-mapping.json'),
|
||||
JSON.stringify({ 'my-repo': 'fallback-service' }),
|
||||
'utf-8',
|
||||
);
|
||||
vi.resetModules();
|
||||
const { readServerMapping } = await import('../../src/core/embeddings/server-mapping.js');
|
||||
expect(await readServerMapping('my-repo')).toBe('fallback-service');
|
||||
} finally {
|
||||
if (savedProcessHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = savedProcessHome;
|
||||
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = savedUserProfile;
|
||||
await fs.rm(fakeHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue