gitmaps 1.0.0 β 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -11
- package/app/[owner]/[repo]/page.client.tsx +5 -0
- package/app/[owner]/[repo]/page.tsx +6 -0
- package/app/[slug]/page.client.tsx +5 -0
- package/app/[slug]/page.tsx +6 -0
- package/app/api/manifest.json/route.ts +20 -0
- package/app/api/pwa-icon/route.ts +14 -0
- package/app/api/repo/clone-stream/route.ts +20 -12
- package/app/api/repo/imports/route.ts +21 -3
- package/app/api/repo/list/route.ts +30 -0
- package/app/api/repo/upload/route.ts +6 -9
- package/app/api/sw.js/route.ts +70 -0
- package/app/galaxy-canvas/page.client.tsx +2 -0
- package/app/galaxy-canvas/page.tsx +5 -0
- package/app/globals.css +477 -95
- package/app/icon.png +0 -0
- package/app/layout.tsx +30 -2
- package/app/lib/canvas-text.ts +4 -72
- package/app/lib/canvas.ts +1 -1
- package/app/lib/card-arrangement.ts +21 -7
- package/app/lib/card-context-menu.tsx +2 -2
- package/app/lib/card-groups.ts +9 -2
- package/app/lib/cards.tsx +3 -1
- package/app/lib/connections.tsx +34 -43
- package/app/lib/events.tsx +25 -0
- package/app/lib/file-card-plugin.ts +14 -0
- package/app/lib/file-preview.ts +68 -41
- package/app/lib/galaxydraw-bridge.ts +5 -0
- package/app/lib/global-search.ts +48 -27
- package/app/lib/layers.tsx +17 -18
- package/app/lib/perf-overlay.ts +78 -0
- package/app/lib/positions.ts +1 -1
- package/app/lib/repo.tsx +18 -8
- package/app/lib/shortcuts-panel.ts +2 -0
- package/app/lib/viewport-culling.ts +7 -0
- package/app/page.client.tsx +72 -18
- package/app/page.tsx +22 -86
- package/banner.png +0 -0
- package/package.json +2 -2
- package/packages/galaxydraw/README.md +2 -2
- package/packages/galaxydraw/package.json +1 -1
- package/server.ts +1 -1
- package/app/api/connections/route.ts +0 -72
- package/app/api/positions/route.ts +0 -80
- package/app/api/repo/browse/route.ts +0 -55
- package/app/lib/pr-review.ts +0 -374
package/README.md
CHANGED
|
@@ -11,19 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
# πͺ GitMaps
|
|
13
13
|
|
|
14
|
-
**
|
|
14
|
+
**Transcend the file tree.** GitMaps renders knowledge on an infinite canvas β with layers, time-travel, and a minimap to never lose context.
|
|
15
15
|
|
|
16
|
-
π **Try it now:** [gitmaps.xyz](https://gitmaps.xyz) β no install required
|
|
16
|
+
π **Try it now:** [gitmaps.xyz](https://gitmaps.xyz) β no install required
|
|
17
|
+
π¬ **Explore React:** [gitmaps.xyz/facebook/react](https://gitmaps.xyz/facebook/react)
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
20
|
-
## The Problem
|
|
21
|
-
|
|
22
|
-
Traditional code review: Open file β read β close β open next file β forget what you just saw β repeat.
|
|
23
|
-
|
|
24
|
-
**Git on Canvas:**
|
|
25
|
-
All changed files laid out on an infinite canvas. Drag them next to each other. Draw connections between related lines. Switch commits with arrow keys. Your spatial layout persists across sessions.
|
|
26
|
-
|
|
27
21
|
## β¨ Features
|
|
28
22
|
|
|
29
23
|
| Feature | Description |
|
|
@@ -45,7 +39,7 @@ All changed files laid out on an infinite canvas. Drag them next to each other.
|
|
|
45
39
|
| βοΈ **Full Editor** | CodeMirror 6 with syntax highlighting, multi-tab, auto-save, git commit, and code minimap. |
|
|
46
40
|
| β **Tab Diff** | Compare two open tabs side-by-side with LCS diff, change markers, synced scrolling. |
|
|
47
41
|
| π§ **Symbol Outline** | Side panel showing all functions/classes/types in a file. Click to scroll. |
|
|
48
|
-
|
|
|
42
|
+
| πΊοΈ **Minimap** | Bird's-eye overview with click-to-navigate and viewport indicator. |
|
|
49
43
|
| π **PR Review** | Comment threads on any line, stored in localStorage. |
|
|
50
44
|
| π **Card Grouping** | Collapse entire directories into compact summary cards to reduce clutter. |
|
|
51
45
|
|
|
@@ -60,7 +54,7 @@ npx gitmaps
|
|
|
60
54
|
Or clone for development:
|
|
61
55
|
|
|
62
56
|
```sh
|
|
63
|
-
git clone https://github.com/7flash/
|
|
57
|
+
git clone https://github.com/7flash/gitmaps.git
|
|
64
58
|
cd galaxy-canvas
|
|
65
59
|
bun install
|
|
66
60
|
bun run dev
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// GET /api/manifest.json β PWA Web App Manifest
|
|
2
|
+
export function GET() {
|
|
3
|
+
return Response.json({
|
|
4
|
+
name: 'GitMaps β Spatial Code Explorer',
|
|
5
|
+
short_name: 'GitMaps',
|
|
6
|
+
description: 'Transcend the file tree. Explore code on an infinite canvas with layers, time-travel, and a minimap.',
|
|
7
|
+
start_url: '/',
|
|
8
|
+
display: 'standalone',
|
|
9
|
+
background_color: '#0a0a0f',
|
|
10
|
+
theme_color: '#7c3aed',
|
|
11
|
+
orientation: 'any',
|
|
12
|
+
categories: ['developer-tools', 'productivity'],
|
|
13
|
+
icons: [
|
|
14
|
+
{ src: '/api/pwa-icon?size=192', sizes: '192x192', type: 'image/png' },
|
|
15
|
+
{ src: '/api/pwa-icon?size=512', sizes: '512x512', type: 'image/png' },
|
|
16
|
+
],
|
|
17
|
+
}, {
|
|
18
|
+
headers: { 'Content-Type': 'application/manifest+json' },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const iconBuffer = readFileSync(join(import.meta.dir, '..', '..', 'icon.png'));
|
|
5
|
+
|
|
6
|
+
// GET /api/pwa-icon β serves the app icon (any size param ignored, returns PNG)
|
|
7
|
+
export function GET() {
|
|
8
|
+
return new Response(iconBuffer, {
|
|
9
|
+
headers: {
|
|
10
|
+
'Content-Type': 'image/png',
|
|
11
|
+
'Cache-Control': 'public, max-age=86400',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { measure } from 'measure-fn';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
|
|
6
6
|
const CLONES_DIR = path.join(process.cwd(), 'git-canvas', 'repos');
|
|
7
7
|
|
|
@@ -46,8 +46,8 @@ export async function POST(req: Request) {
|
|
|
46
46
|
if (fs.existsSync(path.join(targetPath, '.git'))) {
|
|
47
47
|
return measure('api:repo:clone-stream:cached', async () => {
|
|
48
48
|
try {
|
|
49
|
-
const pull = spawn('git',
|
|
50
|
-
await
|
|
49
|
+
const pull = Bun.spawn(['git', 'pull'], { cwd: targetPath, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
50
|
+
await pull.exited;
|
|
51
51
|
console.log(`[clone-stream] Updated existing repo: ${repoName}`);
|
|
52
52
|
} catch {
|
|
53
53
|
console.log(`[clone-stream] Using existing repo (pull skipped): ${repoName}`);
|
|
@@ -69,7 +69,7 @@ export async function POST(req: Request) {
|
|
|
69
69
|
|
|
70
70
|
sendSSE('progress', { message: `Starting clone of ${repoName}...`, percent: 0 });
|
|
71
71
|
|
|
72
|
-
const gitProc = spawn('git',
|
|
72
|
+
const gitProc = Bun.spawn(['git', 'clone', '--depth', '100', '--progress', url, targetPath], {
|
|
73
73
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
74
74
|
});
|
|
75
75
|
|
|
@@ -99,10 +99,20 @@ export async function POST(req: Request) {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
async function consumeStream(stream: ReadableStream) {
|
|
103
|
+
try {
|
|
104
|
+
for await (const chunk of stream) {
|
|
105
|
+
parseProgress(Buffer.from(chunk));
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error('[clone-stream] stream parse error:', e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
104
111
|
|
|
105
|
-
gitProc.
|
|
112
|
+
if (gitProc.stderr) consumeStream(gitProc.stderr);
|
|
113
|
+
if (gitProc.stdout) consumeStream(gitProc.stdout);
|
|
114
|
+
|
|
115
|
+
gitProc.exited.then(code => {
|
|
106
116
|
if (code === 0) {
|
|
107
117
|
console.log(`[clone-stream] β
Cloned ${repoName}`);
|
|
108
118
|
sendSSE('done', { ok: true, path: targetPath, cached: false });
|
|
@@ -110,13 +120,11 @@ export async function POST(req: Request) {
|
|
|
110
120
|
console.error(`[clone-stream] β git clone exited with code ${code}`);
|
|
111
121
|
sendSSE('error', { error: `git clone failed (exit code ${code})` });
|
|
112
122
|
}
|
|
113
|
-
try { controller.close(); } catch {
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
gitProc.on('error', (err) => {
|
|
123
|
+
try { controller.close(); } catch { }
|
|
124
|
+
}).catch(err => {
|
|
117
125
|
console.error('[clone-stream] spawn error:', err);
|
|
118
126
|
sendSSE('error', { error: err.message || 'Failed to start git' });
|
|
119
|
-
try { controller.close(); } catch {
|
|
127
|
+
try { controller.close(); } catch { }
|
|
120
128
|
});
|
|
121
129
|
}
|
|
122
130
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { measure } from 'measure-fn';
|
|
2
2
|
import simpleGit from 'simple-git';
|
|
3
3
|
import { validateRepoPath } from '../validate-path';
|
|
4
|
+
import { join } from 'path';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* POST /api/repo/imports
|
|
@@ -96,13 +97,30 @@ export async function POST(req: Request) {
|
|
|
96
97
|
});
|
|
97
98
|
|
|
98
99
|
// Scan each source file for imports (limit to first 200 files for perf)
|
|
99
|
-
const filesToScan = sourceFiles.slice(0,
|
|
100
|
+
const filesToScan = sourceFiles.slice(0, 300);
|
|
100
101
|
const edges: { source: string; target: string; line: number }[] = [];
|
|
101
102
|
|
|
103
|
+
const isWorkingTree = !commit || commit === 'allfiles' || commit === 'HEAD' || commit === '';
|
|
104
|
+
|
|
102
105
|
await Promise.allSettled(filesToScan.map(async (filePath) => {
|
|
103
106
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
let text = '';
|
|
108
|
+
if (isWorkingTree) {
|
|
109
|
+
try {
|
|
110
|
+
const file = Bun.file(join(repoPath, filePath));
|
|
111
|
+
if (await file.exists()) {
|
|
112
|
+
text = await file.text();
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Fallback if failed
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!text) {
|
|
119
|
+
text = await git.show([`${commit === 'allfiles' ? 'HEAD' : commit}:${filePath}`]);
|
|
120
|
+
}
|
|
121
|
+
if (!text) return;
|
|
122
|
+
|
|
123
|
+
const lines = text.split('\n');
|
|
106
124
|
|
|
107
125
|
for (let i = 0; i < Math.min(lines.length, 100); i++) {
|
|
108
126
|
// Only scan first 100 lines (imports are at the top)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// app/api/repo/list/route.ts β Lists repos from the git-canvas/repos directory
|
|
2
|
+
import { readdirSync, statSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
const reposDir = join(process.cwd(), 'git-canvas', 'repos');
|
|
7
|
+
const repos: { name: string; path: string }[] = [];
|
|
8
|
+
|
|
9
|
+
if (existsSync(reposDir)) {
|
|
10
|
+
try {
|
|
11
|
+
const entries = readdirSync(reposDir);
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const fullPath = join(reposDir, entry);
|
|
14
|
+
try {
|
|
15
|
+
const stat = statSync(fullPath);
|
|
16
|
+
if (stat.isDirectory()) {
|
|
17
|
+
// Check if it's a git repo (has .git)
|
|
18
|
+
const isGit = existsSync(join(fullPath, '.git'));
|
|
19
|
+
repos.push({
|
|
20
|
+
name: entry,
|
|
21
|
+
path: fullPath.replace(/\\/g, '/'),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
} catch { }
|
|
25
|
+
}
|
|
26
|
+
} catch { }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Response.json({ repos });
|
|
30
|
+
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import {
|
|
4
|
-
import { promisify } from "util";
|
|
5
|
-
|
|
6
|
-
const execAsync = promisify(exec);
|
|
3
|
+
import { $ } from "bun";
|
|
7
4
|
|
|
8
5
|
export async function POST(req: Request) {
|
|
9
6
|
try {
|
|
@@ -35,15 +32,15 @@ export async function POST(req: Request) {
|
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
// Initialize a Git repository so galaxy-canvas can read it
|
|
38
|
-
await
|
|
35
|
+
await $`git init`.cwd(repoPath);
|
|
39
36
|
|
|
40
37
|
// Setup dummy user info, otherwise git commits will fail if not globally set
|
|
41
|
-
await
|
|
42
|
-
await
|
|
38
|
+
await $`git config user.name "Galaxy Canvas"`.cwd(repoPath);
|
|
39
|
+
await $`git config user.email "bot@galaxycanvas.local"`.cwd(repoPath);
|
|
43
40
|
|
|
44
41
|
// Add all files and commit
|
|
45
|
-
await
|
|
46
|
-
await
|
|
42
|
+
await $`git add .`.cwd(repoPath);
|
|
43
|
+
await $`git commit -m "Initial drop imported by drag-and-drop"`.cwd(repoPath);
|
|
47
44
|
|
|
48
45
|
return Response.json({ path: repoPath, success: true });
|
|
49
46
|
} catch (e: any) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// GET /api/sw.js β Service Worker for offline caching
|
|
2
|
+
export function GET() {
|
|
3
|
+
const sw = `
|
|
4
|
+
const CACHE_NAME = 'gitmaps-v1';
|
|
5
|
+
const PRECACHE = [
|
|
6
|
+
'/',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
self.addEventListener('install', (e) => {
|
|
10
|
+
e.waitUntil(
|
|
11
|
+
caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE))
|
|
12
|
+
);
|
|
13
|
+
self.skipWaiting();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
self.addEventListener('activate', (e) => {
|
|
17
|
+
e.waitUntil(
|
|
18
|
+
caches.keys().then(keys =>
|
|
19
|
+
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
self.clients.claim();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
self.addEventListener('fetch', (e) => {
|
|
26
|
+
const url = new URL(e.request.url);
|
|
27
|
+
|
|
28
|
+
// Skip non-GET, WebSocket, and API requests (except manifest)
|
|
29
|
+
if (e.request.method !== 'GET') return;
|
|
30
|
+
if (url.protocol === 'ws:' || url.protocol === 'wss:') return;
|
|
31
|
+
if (url.pathname.startsWith('/api/') && !url.pathname.includes('manifest')) return;
|
|
32
|
+
|
|
33
|
+
// Network-first for HTML pages (always get fresh content)
|
|
34
|
+
if (e.request.headers.get('accept')?.includes('text/html')) {
|
|
35
|
+
e.respondWith(
|
|
36
|
+
fetch(e.request)
|
|
37
|
+
.then(res => {
|
|
38
|
+
const clone = res.clone();
|
|
39
|
+
caches.open(CACHE_NAME).then(c => c.put(e.request, clone));
|
|
40
|
+
return res;
|
|
41
|
+
})
|
|
42
|
+
.catch(() => caches.match(e.request))
|
|
43
|
+
);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Cache-first for static assets (CSS, JS, fonts, images)
|
|
48
|
+
e.respondWith(
|
|
49
|
+
caches.match(e.request).then(cached => {
|
|
50
|
+
if (cached) return cached;
|
|
51
|
+
return fetch(e.request).then(res => {
|
|
52
|
+
if (res.ok && res.status === 200) {
|
|
53
|
+
const clone = res.clone();
|
|
54
|
+
caches.open(CACHE_NAME).then(c => c.put(e.request, clone));
|
|
55
|
+
}
|
|
56
|
+
return res;
|
|
57
|
+
});
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
return new Response(sw, {
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/javascript',
|
|
66
|
+
'Service-Worker-Allowed': '/',
|
|
67
|
+
'Cache-Control': 'no-cache',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|