lukeonline

source for my website
git clone https://git.lukeonline.net/repos/lukeonline
Log | Files | Refs | README | LICENSE

commit ed5e62ade1dacc0d5fc6fda909289bcd2e78db25
parent 217d8f02cbc51db6a611184777efb49e2945f4a4
Author: LukeJ <luke.jones.2567@gmail.com>
Date:   Thu, 22 Oct 2026 22:07:01 +0100

refactor: remove local projects, redirect to git pages

Diffstat:
Dscripts/export-repos.py | 440-------------------------------------------------------------------------------
Msrc/pages/index.astro | 4++--
Dsrc/pages/projects/[name].astro | 70----------------------------------------------------------------------
Dsrc/pages/projects/[name]/commit/[sha].astro | 38--------------------------------------
Dsrc/pages/projects/[name]/commits/index.astro | 97-------------------------------------------------------------------------------
Dsrc/pages/projects/[name]/file/[...path].astro | 87-------------------------------------------------------------------------------
Dsrc/pages/projects/[name]/files/[...path].astro | 125-------------------------------------------------------------------------------
Dsrc/pages/projects/[name]/files/index.astro | 92-------------------------------------------------------------------------------
Dsrc/pages/projects/index.astro | 18------------------
9 files changed, 2 insertions(+), 969 deletions(-)

diff --git a/scripts/export-repos.py b/scripts/export-repos.py @@ -1,439 +0,0 @@ -#!/usr/bin/env python3 - -import base64 -import json -import shlex -import shutil -import subprocess -import sys -from pathlib import Path - -OUTPUT_ROOT = Path("src/content/repos") - -def run_ssh(remote, command): - """Run a shell command on the remote server.""" - - result = subprocess.run( - ["ssh", remote, command], - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - if result.returncode != 0: - print( - f"SSH command failed: {command}", - file=sys.stderr, - ) - print(result.stderr, file=sys.stderr) - raise RuntimeError("Remote command failed") - - return result.stdout - - -def git(remote, repo, *args): - """Run git --git-dir=<repo> remotely.""" - - command = " ".join( - [ - "git", - "--git-dir", - shlex.quote(repo), - *[shlex.quote(str(arg)) for arg in args], - ] - ) - - return run_ssh(remote, command) - - -def get_repo_metadata(remote, repo): - """Get basic repository metadata.""" - - head = git( - remote, - repo, - "rev-parse", - "HEAD", - ).strip() - - default_branch = git( - remote, - repo, - "symbolic-ref", - "HEAD", - ).strip().removeprefix("refs/heads/") - - branches = git( - remote, - repo, - "for-each-ref", - "--format=%(refname:short)", - "refs/heads/", - ).splitlines() - - tags = git( - remote, - repo, - "for-each-ref", - "--format=%(refname:short)", - "refs/tags/", - ).splitlines() - - return { - "head": head, - "defaultBranch": default_branch, - "branches": branches, - "tags": tags, - } - - -def get_commits(remote, repo): - """Get all commits in chronological order.""" - - output = git( - remote, - repo, - "log", - "--reverse", - "--format=%H%x00%P%x00%an%x00%ae%x00%aI%x00%s%x00", - ) - - commits = [] - - for record in output.split("\n"): - if not record: - continue - - fields = record.split("\x00") - - if len(fields) < 6: - continue - - sha, parents, author, email, date, message = fields[:6] - - commits.append( - { - "sha": sha, - "shortSha": sha[:7], - "parents": parents.split() if parents else [], - "author": { - "name": author, - "email": email, - }, - "date": date, - "message": message, - } - ) - - return commits - - -def get_commit_diff(remote, repo, commit): - """Get the complete diff for a commit.""" - - return git( - remote, - repo, - "show", - "--format=", - "--binary", - commit, - ) - - -def get_tree(remote, repo, commit): - """Get the complete file tree for a commit.""" - - result = subprocess.run( - [ - "ssh", - remote, - "git", - "--git-dir", - repo, - "ls-tree", - "-r", - "-z", - commit, - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - if result.returncode != 0: - print(result.stderr.decode(), file=sys.stderr) - raise RuntimeError("Could not read repository tree") - - files = [] - - for entry in result.stdout.split(b"\0"): - if not entry: - continue - - metadata, path = entry.split(b"\t", 1) - - mode, object_type, sha = metadata.decode().split() - - files.append( - { - "path": path.decode("utf-8", errors="replace"), - "mode": mode, - "type": object_type, - "sha": sha, - } - ) - - return files - - -def get_file_content(remote, repo, commit, path): - """Get a file's contents from a specific commit.""" - - command = ( - f"git --git-dir {shlex.quote(repo)} " - f"show {shlex.quote(f'{commit}:{path}')}" - ) - - result = subprocess.run( - ["ssh", remote, command], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - if result.returncode != 0: - return None - - return result.stdout - - -def write_json(path, data): - """Write formatted JSON.""" - - with open(path, "w", encoding="utf-8") as f: - json.dump( - data, - f, - ensure_ascii=False, - indent=2, - ) - f.write("\n") - - -def export_repository(remote, repo): - """Export a single repository.""" - - name = Path(repo).name - - output = OUTPUT_ROOT / name - - print(f"Exporting {name}...") - - output.mkdir(parents=True, exist_ok=True) - (output / "commits").mkdir(exist_ok=True) - (output / "files").mkdir(exist_ok=True) - - metadata = get_repo_metadata(remote, repo) - - print(f" HEAD: {metadata['head']}") - print(f" Branch: {metadata['defaultBranch']}") - print(f" Branches: {len(metadata['branches'])}") - print(f" Tags: {len(metadata['tags'])}") - - commits = get_commits(remote, repo) - - print(f" Commits: {len(commits)}") - - write_json( - output / "repo.json", - { - "name": name, - **metadata, - "commitCount": len(commits), - }, - ) - - write_json( - output / "commits.json", - commits, - ) - - for number, commit in enumerate(commits, 1): - sha = commit["sha"] - - print( - f" Commit {number}/{len(commits)}: " - f"{commit['shortSha']} {commit['message']}" - ) - - commit_data = { - **commit, - "diff": get_commit_diff( - remote, - repo, - sha, - ), - } - - write_json( - output / "commits" / f"{sha}.json", - commit_data, - ) - - print(" Exporting file tree...") - - files = get_tree( - remote, - repo, - metadata["head"], - ) - - write_json( - output / "tree.json", - files, - ) - - print(f" Exporting {len(files)} files...") - - for number, file_info in enumerate(files, 1): - path = file_info["path"] - - print( - f" File {number}/{len(files)}: {path}" - ) - - content = get_file_content( - remote, - repo, - metadata["head"], - path, - ) - - if content is None: - continue - - try: - text = content.decode("utf-8") - - file_data = { - **file_info, - "binary": False, - "content": text, - } - - except UnicodeDecodeError: - - file_data = { - **file_info, - "binary": True, - "content": base64.b64encode( - content - ).decode("ascii"), - } - - filename = ( - base64.urlsafe_b64encode( - path.encode("utf-8") - ) - .decode("ascii") - .rstrip("=") - ) - - write_json( - output / "files" / f"{filename}.json", - file_data, - ) - - print(f" Finished {name}") - - -def main(): - if len(sys.argv) != 2: - print( - f"Usage: {sys.argv[0]} user@host:/path/to/repositories", - file=sys.stderr, - ) - sys.exit(1) - - remote = sys.argv[1].split(':')[0] - remote_root = sys.argv[1].split(':')[1] - - print("Checking SSH connection...") - - run_ssh(remote, "true") - - print("Finding repositories...") - - repos = run_ssh( - remote, - ( - f"find {shlex.quote(remote_root)} " - "-mindepth 1 " - "-maxdepth 1 " - "-type d " - "-print" - ), - ).splitlines() - - repos = [ - repo.strip() - for repo in repos - if repo.strip() - ] - - if not repos: - print( - "No repositories found.", - file=sys.stderr, - ) - sys.exit(1) - - print(f"Found {len(repos)} repositories.") - - OUTPUT_ROOT.mkdir( - parents=True, - exist_ok=True, - ) - - current_names = { - Path(repo).name - for repo in repos - } - - for existing in OUTPUT_ROOT.iterdir(): - if ( - existing.is_dir() - and existing.name not in current_names - ): - print( - f"Removing old repository: " - f"{existing.name}" - ) - shutil.rmtree(existing) - - for repo in sorted(repos): - export_repository( - remote, - repo, - ) - - print() - print("Repository export complete.") - - -if __name__ == "__main__": - try: - main() - - except KeyboardInterrupt: - print( - "\nCancelled.", - file=sys.stderr, - ) - sys.exit(130) - - except Exception as e: - print( - f"\nERROR: {e}", - file=sys.stderr, - ) - sys.exit(1)- \ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro @@ -9,8 +9,8 @@ const title = 'Home' const mainButtonRow = [ { title: 'Projects', - desc: 'A list of a few of my projects', - link: '/projects', + desc: 'A few of my projects', + link: 'https://git.lukeonline.net/', }, { title: 'Blog', diff --git a/src/pages/projects/[name].astro b/src/pages/projects/[name].astro @@ -1,70 +0,0 @@ ---- -import Base from '../../layouts/Base.astro' - -import { getCollection } from 'astro:content' -import { getFileByPath, getTree } from '../../lib/repos' -import { - BUTTON_STYLING_SHARED, - DISABLED_BUTTON_STYLING_SHARED, -} from '../../consts.ts' -import CodeContainer from '../../components/CodeContainer.astro' -import { encodeFilePath } from '../../lib/repos.ts' - -export async function getStaticPaths() { - const repos = await getCollection('repos') - - return repos.map((repo) => ({ - params: { - name: repo.data.name, - }, - props: { - repo, - }, - })) -} - -const { repo } = Astro.props - -const tree = getTree(repo.data.name) - -function getReadMeContent() { - const readme = tree.find((file) => file.path.toLowerCase() === 'readme.md') - - if (!readme) { - return 'No README provided in this repository.' - } - - const file = getFileByPath(repo.data.name, readme.path) - - return file.binary ? '' : file.content -} ---- - -<Base title={repo.data.name}> - <div class="flex flex-row items-baseline gap-2"> - <code class="text-sm" - ><a href={`/projects/${repo.data.name}`}>{repo.data.defaultBranch}</a - ></code - > - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/commits`} - >{repo.data.commitCount} commits</a - > - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/files`}>{tree.length} files</a - > - <a - class={DISABLED_BUTTON_STYLING_SHARED + - ' h-8 p-1 pl-2 items-center gap-3'} - aria-disabled="true">README</a - > - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/file/${encodeFilePath('LICENSE')}`} - >LICENSE</a - > - </div> - <CodeContainer code={getReadMeContent()} /> -</Base> diff --git a/src/pages/projects/[name]/commit/[sha].astro b/src/pages/projects/[name]/commit/[sha].astro @@ -1,38 +0,0 @@ ---- -import { getCollection } from 'astro:content' -import { getCommit, getCommits } from '../../../../lib/repos' -import Base from '../../../../layouts/Base.astro' -import CodeContainer from '../../../../components/CodeContainer.astro' - -export async function getStaticPaths() { - const repos = await getCollection('repos') - - return repos.flatMap((repo) => { - const commits = getCommits(repo.data.name) - - return commits.map((commit) => ({ - params: { - name: repo.data.name, - sha: commit.sha, - }, - props: { - repo, - commit: getCommit(repo.data.name, commit.sha), - }, - })) - }) -} - -const { repo, commit } = Astro.props ---- - -<Base title={`${repo.data.name} - commit ${commit.shortSha}`}> - <div class="flex flex-col text-sm"> - <code>{commit.message}</code> - <code>{commit.sha}</code> - <code>{commit.author.name}</code> - <code>&lt;{commit.author.email}&gt;</code> - <code>{commit.date}</code> - </div> - <CodeContainer code={commit.diff} /> -</Base> diff --git a/src/pages/projects/[name]/commits/index.astro b/src/pages/projects/[name]/commits/index.astro @@ -1,97 +0,0 @@ ---- -import Base from '../../../../layouts/Base.astro' - -import { getCollection } from 'astro:content' -import { getCommits, getTree } from '../../../../lib/repos' -import { - BUTTON_STYLING_SHARED, - DISABLED_BUTTON_STYLING_SHARED, -} from '../../../../consts.ts' -import { encodeFilePath } from '../../../../lib/repos.ts' - -export async function getStaticPaths() { - const repos = await getCollection('repos') - - return repos.map((repo) => ({ - params: { - name: repo.data.name, - }, - props: { - repo, - }, - })) -} - -const { repo } = Astro.props -const { name } = Astro.params - -const commits = getCommits(name!) -const tree = getTree(name!) ---- - -<Base title={`${name}`}> - <div class="flex flex-row items-baseline gap-2"> - <code class="text-sm" - ><a href={`/projects/${name}`}>{repo.data.defaultBranch}</a></code - > - - <a - class={DISABLED_BUTTON_STYLING_SHARED + - ' h-8 p-1 pl-2 items-center gap-3'} - aria-disabled="true" - > - {repo.data.commitCount} commits - </a> - - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${name}/files`} - > - {tree.length} files - </a> - - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${name}/file/${encodeFilePath('README.md')}`} - > - README - </a> - - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${name}/file/${encodeFilePath('LICENSE')}`} - > - LICENSE - </a> - </div> - - <table class="w-full"> - <thead> - <tr> - <th class="text-left">sha</th> - <th class="text-left">Message</th> - <th class="text-left">Date</th> - </tr> - </thead> - - <tbody> - { - [...commits].reverse().map((commit) => ( - <tr> - <td class="max-w-12 min-w-12"> - <a href={`/projects/${name}/commit/${commit.sha}`}> - <code class="text-sm">{commit.shortSha}</code> - </a> - </td> - - <td class="max-w-72 overflow-hidden text-ellipsis whitespace-nowrap"> - <code class="text-sm">{commit.message}</code> - </td> - - <td>{new Date(commit.date).toLocaleDateString()}</td> - </tr> - )) - } - </tbody> - </table> -</Base> diff --git a/src/pages/projects/[name]/file/[...path].astro b/src/pages/projects/[name]/file/[...path].astro @@ -1,87 +0,0 @@ ---- -import Base from '../../../../layouts/Base.astro' - -import { getCollection } from 'astro:content' -import { - decodeFilePath, - encodeFilePath, - getFileByPath, - getTree, -} from '../../../../lib/repos' -import CodeContainer from '../../../../components/CodeContainer.astro' -import { BUTTON_STYLING_SHARED } from '../../../../consts.ts' - -export async function getStaticPaths() { - const repos = await getCollection('repos') - - return repos.flatMap((repo) => { - const tree = getTree(repo.data.name) - - return tree - .filter((file) => file.path) - .map((file) => ({ - params: { - name: repo.data.name, - path: encodeFilePath(file.path), - }, - props: { - repo, - file, - tree, - }, - })) - }) -} - -const { repo, file } = Astro.props - -const tree = getTree(repo.data.name) -const filePath = decodeFilePath(Astro.params.path!) -const fileData = getFileByPath(repo.data.name, filePath) ---- - -<Base title={`${repo.data.name} - ${file.path}`}> - <div class="flex flex-row items-baseline gap-2"> - <code class="text-sm"> - <a href={`/projects/${repo.data.name}`}> - {repo.data.defaultBranch} - </a> - </code> - - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/commits`} - > - {repo.data.commitCount} commits - </a> - - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/files`} - > - {tree.length} files - </a> - - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/file/${encodeFilePath('README.md')}`} - > - README - </a> - - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/file/${encodeFilePath('LICENSE')}`} - > - LICENSE - </a> - </div> - - { - fileData.binary ? ( - <CodeContainer code={'Binary File'} /> - ) : ( - <CodeContainer code={fileData.content} /> - ) - } -</Base> diff --git a/src/pages/projects/[name]/files/[...path].astro b/src/pages/projects/[name]/files/[...path].astro @@ -1,125 +0,0 @@ ---- -import { getCollection } from 'astro:content' -import { getTree, getDirectoryChildren } from '../../../../lib/repos' -import Base from '../../../../layouts/Base.astro' -import { BUTTON_STYLING_SHARED } from '../../../../consts.ts' -import { encodeFilePath } from '../../../../lib/repos.ts' - -export async function getStaticPaths() { - const repos = await getCollection('repos') - - return repos.flatMap((repo) => { - const tree = getTree(repo.data.name) - const directories = new Set<string>() - - for (const file of tree) { - const parts = file.path.split('/') - - for (let i = 1; i < parts.length; i++) { - directories.add(parts.slice(0, i).join('/')) - } - } - - return [...directories].map((path) => ({ - params: { - name: repo.data.name, - path, - }, - props: { - repo, - tree, - }, - })) - }) -} - -const { repo, tree } = Astro.props -const directory = Astro.params.path ?? '' - -const { directories, files } = getDirectoryChildren(tree, directory) - -const parent = directory.includes('/') - ? directory.slice(0, directory.lastIndexOf('/')) - : '' ---- - -<Base title={`${repo.data.name} - ${directory}`}> - <div class="flex flex-row items-baseline gap-2"> - <code class="text-sm"> - <a href={`/projects/${repo.data.name}`}> - {repo.data.defaultBranch} - </a> - </code> - - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/commits`} - > - {repo.data.commitCount} commits - </a> - - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/files`} - > - {tree.length} files - </a> - - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/file/${encodeFilePath('README.md')}`} - > - README - </a> - - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/file/${encodeFilePath('LICENSE')}`} - > - LICENSE - </a> - </div> - - <div - class="flex flex-col rounded-sm p-2 font-mono text-sm whitespace-pre-line ring inset-shadow-sm ring-gray-600 inset-shadow-gray-400" - > - <a - class="p-0.5 hover:text-blue-800 focus:underline" - href={parent - ? `/projects/${repo.data.name}/files/${parent}` - : `/projects/${repo.data.name}/files`} - > - ../ - </a> - - { - directories.map((child) => { - const childPath = directory ? `${directory}/${child}` : child - - return ( - <a - class="p-0.5 hover:text-blue-800 focus:underline" - href={`/projects/${repo.data.name}/files/${childPath}`} - > - {child}/ - </a> - ) - }) - } - - { - files.map((file) => { - const filePath = directory ? `${directory}/${file}` : file - - return ( - <a - class="p-0.5 hover:text-blue-800 focus:underline" - href={`/projects/${repo.data.name}/file/${encodeFilePath(filePath)}`} - > - {file} - </a> - ) - }) - } - </div> -</Base> diff --git a/src/pages/projects/[name]/files/index.astro b/src/pages/projects/[name]/files/index.astro @@ -1,92 +0,0 @@ ---- -import { getCollection } from 'astro:content' -import { getTree } from '../../../../lib/repos' -import Base from '../../../../layouts/Base.astro' -import { - BUTTON_STYLING_SHARED, - DISABLED_BUTTON_STYLING_SHARED, -} from '../../../../consts.ts' -import { encodeFilePath } from '../../../../lib/repos.ts' - -export async function getStaticPaths() { - const repos = await getCollection('repos') - - return repos.map((repo) => ({ - params: { - name: repo.data.name, - }, - props: { - repo, - }, - })) -} - -const { repo } = Astro.props - -const tree = getTree(repo.data.name) - -const files = tree - .filter((file) => !file.path.includes('/')) - .sort((a, b) => a.path.localeCompare(b.path)) - -const directories = [ - ...new Set( - tree - .filter((file) => file.path.includes('/')) - .map((file) => file.path.split('/')[0]) - ), -].sort((a, b) => a.localeCompare(b)) ---- - -<Base title={repo.data.name}> - <div class="flex flex-row items-baseline gap-2"> - <code class="text-sm" - ><a href={`/projects/${repo.data.name}`}>{repo.data.defaultBranch}</a - ></code - > - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/commits`} - >{repo.data.commitCount} commits</a - > - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/files`}>{tree.length} files</a - > - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/file/${encodeFilePath('README.md')}`} - >README</a - > - <a - class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'} - href={`/projects/${repo.data.name}/file/${encodeFilePath('LICENSE')}`} - >LICENSE</a - > - </div> - <div - class="flex flex-col rounded-sm p-2 font-mono text-sm whitespace-pre-line ring inset-shadow-sm ring-gray-600 inset-shadow-gray-400" - > - { - directories.map((directory) => ( - <a - class="p-0.5 hover:text-blue-800 focus:underline" - href={`/projects/${repo.data.name}/files/${directory}`} - > - {directory}/ - </a> - )) - } - - { - files.map((file) => ( - <a - class="p-0.5 hover:text-blue-800 focus:underline" - href={`/projects/${repo.data.name}/file/${encodeFilePath(file.path)}`} - > - {file.path} - </a> - )) - } - </div> -</Base> diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro @@ -1,18 +0,0 @@ ---- -import ContentList from '../../layouts/ContentList.astro' - -import { getCollection, getEntry } from 'astro:content' - -const title = 'Projects' - -const repos = await getCollection('repos') - -const repoUrl = '/projects/' - -const contentData = repos.map((repo) => ({ - title: `${repo.data.name}`, - link: `${repoUrl}${repo.data.name}`, -})) ---- - -<ContentList title={title} content={contentData} />