Skip to content

Commit 417eabf

Browse files
fix issue with empty paths and add some UTs
1 parent 718a26a commit 417eabf

File tree

2 files changed

+207
-7
lines changed

2 files changed

+207
-7
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { getBrowseParamsFromPathParam } from './utils';
3+
4+
describe('getBrowseParamsFromPathParam', () => {
5+
describe('tree paths', () => {
6+
it('should parse tree path with trailing slash', () => {
7+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/tree/');
8+
expect(result).toEqual({
9+
repoName: 'github.com/sourcebot-dev/zoekt',
10+
revisionName: 'HEAD',
11+
path: '',
12+
pathType: 'tree',
13+
});
14+
});
15+
16+
it('should parse tree path without trailing slash', () => {
17+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/tree');
18+
expect(result).toEqual({
19+
repoName: 'github.com/sourcebot-dev/zoekt',
20+
revisionName: 'HEAD',
21+
path: '',
22+
pathType: 'tree',
23+
});
24+
});
25+
26+
it('should parse tree path with nested directory', () => {
27+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/tree/packages/web/src');
28+
expect(result).toEqual({
29+
repoName: 'github.com/sourcebot-dev/zoekt',
30+
revisionName: 'HEAD',
31+
path: 'packages/web/src',
32+
pathType: 'tree',
33+
});
34+
});
35+
36+
it('should parse tree path without revision', () => {
37+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt/-/tree/docs');
38+
expect(result).toEqual({
39+
repoName: 'github.com/sourcebot-dev/zoekt',
40+
revisionName: undefined,
41+
path: 'docs',
42+
pathType: 'tree',
43+
});
44+
});
45+
});
46+
47+
describe('blob paths', () => {
48+
49+
50+
it('should parse blob path with file', () => {
51+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/blob/README.md');
52+
expect(result).toEqual({
53+
repoName: 'github.com/sourcebot-dev/zoekt',
54+
revisionName: 'HEAD',
55+
path: 'README.md',
56+
pathType: 'blob',
57+
});
58+
});
59+
60+
it('should parse blob path with nested file', () => {
61+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/blob/packages/web/src/app/page.tsx');
62+
expect(result).toEqual({
63+
repoName: 'github.com/sourcebot-dev/zoekt',
64+
revisionName: 'HEAD',
65+
path: 'packages/web/src/app/page.tsx',
66+
pathType: 'blob',
67+
});
68+
});
69+
70+
it('should parse blob path without revision', () => {
71+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt/-/blob/main.go');
72+
expect(result).toEqual({
73+
repoName: 'github.com/sourcebot-dev/zoekt',
74+
revisionName: undefined,
75+
path: 'main.go',
76+
pathType: 'blob',
77+
});
78+
});
79+
});
80+
81+
describe('URL decoding', () => {
82+
it('should decode URL-encoded spaces in path', () => {
83+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/tree/folder%20with%20spaces');
84+
expect(result).toEqual({
85+
repoName: 'github.com/sourcebot-dev/zoekt',
86+
revisionName: 'HEAD',
87+
path: 'folder with spaces',
88+
pathType: 'tree',
89+
});
90+
});
91+
92+
it('should decode URL-encoded special characters in path', () => {
93+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/blob/file%20with%20%26%20symbols.txt');
94+
expect(result).toEqual({
95+
repoName: 'github.com/sourcebot-dev/zoekt',
96+
revisionName: 'HEAD',
97+
path: 'file with & symbols.txt',
98+
pathType: 'blob',
99+
});
100+
});
101+
});
102+
103+
describe('different revision formats', () => {
104+
it('should parse with branch name', () => {
105+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@main/-/tree/');
106+
expect(result).toEqual({
107+
repoName: 'github.com/sourcebot-dev/zoekt',
108+
revisionName: 'main',
109+
path: '',
110+
pathType: 'tree',
111+
});
112+
});
113+
114+
it('should parse with commit hash', () => {
115+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@a1b2c3d/-/tree/');
116+
expect(result).toEqual({
117+
repoName: 'github.com/sourcebot-dev/zoekt',
118+
revisionName: 'a1b2c3d',
119+
path: '',
120+
pathType: 'tree',
121+
});
122+
});
123+
124+
it('should parse with tag', () => {
125+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/[email protected]/-/tree/');
126+
expect(result).toEqual({
127+
repoName: 'github.com/sourcebot-dev/zoekt',
128+
revisionName: 'v1.0.0',
129+
path: '',
130+
pathType: 'tree',
131+
});
132+
});
133+
});
134+
135+
describe('edge cases', () => {
136+
it('should handle repo name with multiple @ symbols', () => {
137+
const result = getBrowseParamsFromPathParam('gitlab.com/user@domain/repo@main/-/tree/');
138+
expect(result).toEqual({
139+
repoName: 'gitlab.com/user@domain/repo',
140+
revisionName: 'main',
141+
path: '',
142+
pathType: 'tree',
143+
});
144+
});
145+
146+
it('should handle paths with @ symbols', () => {
147+
const result = getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/blob/[email protected]');
148+
expect(result).toEqual({
149+
repoName: 'github.com/sourcebot-dev/zoekt',
150+
revisionName: 'HEAD',
151+
152+
pathType: 'blob',
153+
});
154+
});
155+
});
156+
157+
describe('error cases', () => {
158+
it('should throw error for blob path with trailing slash and no path', () => {
159+
expect(() => {
160+
getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/blob/');
161+
}).toThrow();
162+
});
163+
164+
it('should throw error for blob path without trailing slash and no path', () => {
165+
expect(() => {
166+
getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/blob');
167+
}).toThrow();
168+
});
169+
170+
it('should throw error for invalid pattern - missing /-/', () => {
171+
expect(() => {
172+
getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/tree/');
173+
}).toThrow();
174+
});
175+
176+
it('should throw error for invalid pattern - missing tree/blob', () => {
177+
expect(() => {
178+
getBrowseParamsFromPathParam('github.com/sourcebot-dev/zoekt@HEAD/-/invalid/');
179+
}).toThrow();
180+
});
181+
182+
it('should throw error for completely invalid format', () => {
183+
expect(() => {
184+
getBrowseParamsFromPathParam('invalid-path');
185+
}).toThrow();
186+
});
187+
188+
it('should throw error for empty string', () => {
189+
expect(() => {
190+
getBrowseParamsFromPathParam('');
191+
}).toThrow();
192+
});
193+
});
194+
});

packages/web/src/app/[domain]/browse/hooks/utils.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,39 @@
11

22
export const getBrowseParamsFromPathParam = (pathParam: string) => {
3-
const sentinalIndex = pathParam.search(/\/-\/(tree|blob)\//);
3+
const sentinalIndex = pathParam.search(/\/-\/(tree|blob)/);
44
if (sentinalIndex === -1) {
55
throw new Error(`Invalid browse pathname: "${pathParam}" - expected to contain "/-/(tree|blob)/" pattern`);
66
}
77

8-
const repoAndRevisionName = pathParam.substring(0, sentinalIndex).split('@');
9-
const repoName = repoAndRevisionName[0];
10-
const revisionName = repoAndRevisionName.length > 1 ? repoAndRevisionName[1] : undefined;
8+
const repoAndRevisionPart = pathParam.substring(0, sentinalIndex);
9+
const lastAtIndex = repoAndRevisionPart.lastIndexOf('@');
10+
11+
const repoName = lastAtIndex === -1 ? repoAndRevisionPart : repoAndRevisionPart.substring(0, lastAtIndex);
12+
const revisionName = lastAtIndex === -1 ? undefined : repoAndRevisionPart.substring(lastAtIndex + 1);
1113

1214
const { path, pathType } = ((): { path: string, pathType: 'tree' | 'blob' } => {
1315
const path = pathParam.substring(sentinalIndex + '/-/'.length);
14-
const pathType = path.startsWith('tree/') ? 'tree' : 'blob';
16+
const pathType = path.startsWith('tree') ? 'tree' : 'blob';
1517

1618
// @note: decodedURIComponent is needed here incase the path contains a space.
1719
switch (pathType) {
1820
case 'tree':
1921
return {
20-
path: decodeURIComponent(path.substring('tree/'.length)),
22+
path: decodeURIComponent(path.startsWith('tree/') ? path.substring('tree/'.length) : path.substring('tree'.length)),
2123
pathType,
2224
};
2325
case 'blob':
2426
return {
25-
path: decodeURIComponent(path.substring('blob/'.length)),
27+
path: decodeURIComponent(path.startsWith('blob/') ? path.substring('blob/'.length) : path.substring('blob'.length)),
2628
pathType,
2729
};
2830
}
2931
})();
3032

33+
if (pathType === 'blob' && path === '') {
34+
throw new Error(`Invalid browse pathname: "${pathParam}" - expected to contain a path for blob type`);
35+
}
36+
3137
return {
3238
repoName,
3339
revisionName,

0 commit comments

Comments
 (0)