open source is social v-it.org
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'hopper-xshfel6d-git-beacon'

+141
+87
src/lib/beacon.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-only 2 + // Copyright (c) 2026 sol pbc 3 + 4 + export function parseGitUrl(input) { 5 + if (typeof input !== 'string' || input.trim() === '') { 6 + throw new Error('Invalid git URL: ' + String(input)); 7 + } 8 + 9 + const raw = input.trim(); 10 + const normalizedError = () => new Error('Invalid git URL: ' + raw); 11 + 12 + let host = ''; 13 + let path = ''; 14 + 15 + if (raw.includes('@') && !raw.includes('://')) { 16 + const atIndex = raw.lastIndexOf('@'); 17 + const hostAndPath = raw.slice(atIndex + 1); 18 + const colonIndex = hostAndPath.indexOf(':'); 19 + if (colonIndex === -1) { 20 + throw normalizedError(); 21 + } 22 + host = hostAndPath.slice(0, colonIndex); 23 + path = hostAndPath.slice(colonIndex + 1); 24 + if (!host || !path) { 25 + throw normalizedError(); 26 + } 27 + } else if (raw.includes('://')) { 28 + let url; 29 + try { 30 + url = new URL(raw); 31 + } catch { 32 + throw normalizedError(); 33 + } 34 + host = url.hostname; 35 + path = url.pathname; 36 + if (!host) { 37 + throw normalizedError(); 38 + } 39 + } else { 40 + const slashIndex = raw.indexOf('/'); 41 + if (slashIndex !== -1) { 42 + const maybeHost = raw.slice(0, slashIndex); 43 + if (maybeHost.includes('.')) { 44 + host = maybeHost; 45 + path = raw.slice(slashIndex + 1); 46 + if (!host) { 47 + throw normalizedError(); 48 + } 49 + } else { 50 + throw normalizedError(); 51 + } 52 + } else { 53 + throw normalizedError(); 54 + } 55 + } 56 + 57 + const cleanedPath = path.replace(/^\/+|\/+$/g, '').replace(/\.git$/, ''); 58 + const segments = cleanedPath.split('/').filter(Boolean); 59 + 60 + if (segments.length === 0 || segments.length >= 3) { 61 + throw normalizedError(); 62 + } 63 + 64 + let org = ''; 65 + let repo = ''; 66 + if (segments.length === 1) { 67 + repo = segments[0]; 68 + } else { 69 + org = segments[0]; 70 + repo = segments[1]; 71 + } 72 + 73 + host = host.toLowerCase(); 74 + org = org.toLowerCase(); 75 + repo = repo.toLowerCase(); 76 + 77 + if (!repo) { 78 + throw normalizedError(); 79 + } 80 + 81 + return { host, org, repo }; 82 + } 83 + 84 + export function toBeacon(input) { 85 + const { host, org, repo } = parseGitUrl(input); 86 + return org ? `${host}/${org}/${repo}` : `${host}//${repo}`; 87 + }
+54
test/beacon.test.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-only 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { describe, test, expect } from 'bun:test'; 5 + import { toBeacon, parseGitUrl } from '../src/lib/beacon.js'; 6 + 7 + describe('toBeacon', () => { 8 + test('SCP SSH with .git', () => expect(toBeacon('git@github.com:org/repo.git')).toBe('github.com/org/repo')); 9 + test('SCP SSH without .git', () => expect(toBeacon('git@github.com:org/repo')).toBe('github.com/org/repo')); 10 + 11 + test('SSH URL', () => expect(toBeacon('ssh://git@github.com/org/repo.git')).toBe('github.com/org/repo')); 12 + test('SSH URL with port', () => expect(toBeacon('ssh://git@github.com:22/org/repo.git')).toBe('github.com/org/repo')); 13 + 14 + test('HTTPS with .git', () => expect(toBeacon('https://github.com/org/repo.git')).toBe('github.com/org/repo')); 15 + test('HTTPS without .git', () => expect(toBeacon('https://github.com/org/repo')).toBe('github.com/org/repo')); 16 + test('HTTPS trailing slash', () => expect(toBeacon('https://github.com/org/repo/')).toBe('github.com/org/repo')); 17 + 18 + test('git protocol', () => expect(toBeacon('git://github.com/org/repo.git')).toBe('github.com/org/repo')); 19 + 20 + test('bare slug', () => expect(toBeacon('github.com/org/repo')).toBe('github.com/org/repo')); 21 + test('bare slug with .git', () => expect(toBeacon('github.com/org/repo.git')).toBe('github.com/org/repo')); 22 + 23 + test('case normalization', () => expect(toBeacon('GitHub.Com/Org/Repo.git')).toBe('github.com/org/repo')); 24 + test('SCP case normalization', () => 25 + expect(toBeacon('git@GitHub.Com:Org/Repo.git')).toBe('github.com/org/repo')); 26 + test('HTTPS case normalization', () => 27 + expect(toBeacon('https://GitHub.Com/Org/Repo')).toBe('github.com/org/repo')); 28 + 29 + test('no-org SCP (tilde)', () => expect(toBeacon('git@sr.ht:~user/repo.git')).toBe('sr.ht/~user/repo')); 30 + test('no-org SCP single segment', () => expect(toBeacon('git@myhost.com:repo.git')).toBe('myhost.com//repo')); 31 + test('no-org HTTPS', () => expect(toBeacon('https://myhost.com/repo.git')).toBe('myhost.com//repo')); 32 + test('no-org bare slug', () => expect(toBeacon('myhost.com/repo')).toBe('myhost.com//repo')); 33 + 34 + test('this repo (solpbc/vit)', () => expect(toBeacon('git@github.com:solpbc/vit.git')).toBe('github.com/solpbc/vit')); 35 + 36 + test('empty string throws', () => expect(() => toBeacon('')).toThrow('Invalid git URL')); 37 + test('null throws', () => expect(() => toBeacon(null)).toThrow('Invalid git URL')); 38 + test('undefined throws', () => expect(() => toBeacon(undefined)).toThrow('Invalid git URL')); 39 + test('number throws', () => expect(() => toBeacon(123)).toThrow('Invalid git URL')); 40 + test('bare word throws', () => expect(() => toBeacon('repo')).toThrow('Invalid git URL')); 41 + test('too many segments throws', () => expect(() => toBeacon('github.com/a/b/c')).toThrow('Invalid git URL')); 42 + }); 43 + 44 + describe('parseGitUrl', () => { 45 + test('returns { host, org, repo } for two-segment path', () => { 46 + const r = parseGitUrl('https://github.com/org/repo.git'); 47 + expect(r).toEqual({ host: 'github.com', org: 'org', repo: 'repo' }); 48 + }); 49 + 50 + test('returns empty org for single-segment path', () => { 51 + const r = parseGitUrl('git@myhost.com:repo.git'); 52 + expect(r).toEqual({ host: 'myhost.com', org: '', repo: 'repo' }); 53 + }); 54 + });