Skip to content

Commit 3cbd9ad

Browse files
authored
Merge branch 'main' into index-default-branch
2 parents 17cc16d + 7d516b1 commit 3cbd9ad

File tree

29 files changed

+962
-212
lines changed

29 files changed

+962
-212
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Fixed issue where we crash on startup if the install / upgrade PostHog event fails to send. ([#159](https://github.com/sourcebot-dev/sourcebot/pull/159))
13+
- Fixed issue with broken file links. ([#161](https://github.com/sourcebot-dev/sourcebot/pull/161))
14+
15+
## [2.7.0] - 2025-01-10
16+
17+
### Added
18+
19+
- Added support for creating share links to snippets of code. ([#149](https://github.com/sourcebot-dev/sourcebot/pull/149))
20+
- Added support for indexing raw remote git repository. ([#152](https://github.com/sourcebot-dev/sourcebot/pull/152))
21+
1022
## [2.6.3] - 2024-12-18
1123

1224
### Added

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
FROM node:20-alpine3.19 AS node-alpine
2-
FROM golang:1.22.2-alpine3.19 AS go-alpine
2+
FROM golang:1.23.4-alpine3.19 AS go-alpine
33

44
# ------ Build Zoekt ------
55
FROM go-alpine AS zoekt-builder

entrypoint.sh

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ if [ ! -f "$FIRST_RUN_FILE" ]; then
3030
# If this is our first run, send a `install` event to PostHog
3131
# (if telemetry is enabled)
3232
if [ -z "$SOURCEBOT_TELEMETRY_DISABLED" ]; then
33-
curl -L -s --header "Content-Type: application/json" -d '{
33+
if ! ( curl -L --output /dev/null --silent --fail --header "Content-Type: application/json" -d '{
3434
"api_key": "'"$POSTHOG_PAPIK"'",
3535
"event": "install",
3636
"distinct_id": "'"$SOURCEBOT_INSTALL_ID"'",
3737
"properties": {
3838
"sourcebot_version": "'"$SOURCEBOT_VERSION"'"
3939
}
40-
}' https://us.i.posthog.com/capture/ > /dev/null
40+
}' https://us.i.posthog.com/capture/ ) then
41+
echo -e "\e[33m[Warning] Failed to send install event.\e[0m"
42+
fi
4143
fi
4244
else
4345
export SOURCEBOT_INSTALL_ID=$(cat "$FIRST_RUN_FILE" | jq -r '.install_id')
@@ -48,15 +50,17 @@ else
4850
echo -e "\e[34m[Info] Upgraded from version $PREVIOUS_VERSION to $SOURCEBOT_VERSION\e[0m"
4951

5052
if [ -z "$SOURCEBOT_TELEMETRY_DISABLED" ]; then
51-
curl -L -s --header "Content-Type: application/json" -d '{
53+
if ! ( curl -L --output /dev/null --silent --fail --header "Content-Type: application/json" -d '{
5254
"api_key": "'"$POSTHOG_PAPIK"'",
5355
"event": "upgrade",
5456
"distinct_id": "'"$SOURCEBOT_INSTALL_ID"'",
5557
"properties": {
5658
"from_version": "'"$PREVIOUS_VERSION"'",
5759
"to_version": "'"$SOURCEBOT_VERSION"'"
5860
}
59-
}' https://us.i.posthog.com/capture/ > /dev/null
61+
}' https://us.i.posthog.com/capture/ ) then
62+
echo -e "\e[33m[Warning] Failed to send upgrade event.\e[0m"
63+
fi
6064
fi
6165
fi
6266
fi

packages/backend/src/git.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { GitRepository } from './types.js';
1+
import { GitRepository, AppContext } from './types.js';
22
import { simpleGit, SimpleGitProgressEvent } from 'simple-git';
33
import { existsSync } from 'fs';
44
import { createLogger } from './logger.js';
5+
import { GitConfig } from './schemas/v2.js';
6+
import path from 'path';
57

68
const logger = createLogger('git');
79

@@ -48,4 +50,81 @@ export const fetchRepository = async (repo: GitRepository, onProgress?: (event:
4850
"--progress"
4951
]
5052
);
53+
}
54+
55+
const isValidGitRepo = async (url: string): Promise<boolean> => {
56+
const git = simpleGit();
57+
try {
58+
await git.listRemote([url]);
59+
return true;
60+
} catch (error) {
61+
logger.debug(`Error checking if ${url} is a valid git repo: ${error}`);
62+
return false;
63+
}
64+
}
65+
66+
const stripProtocolAndGitSuffix = (url: string): string => {
67+
return url.replace(/^[a-zA-Z]+:\/\//, '').replace(/\.git$/, '');
68+
}
69+
70+
const getRepoNameFromUrl = (url: string): string => {
71+
const strippedUrl = stripProtocolAndGitSuffix(url);
72+
return strippedUrl.split('/').slice(-2).join('/');
73+
}
74+
75+
export const getGitRepoFromConfig = async (config: GitConfig, ctx: AppContext) => {
76+
const repoValid = await isValidGitRepo(config.url);
77+
if (!repoValid) {
78+
logger.error(`Git repo provided in config with url ${config.url} is not valid`);
79+
return null;
80+
}
81+
82+
const cloneUrl = config.url;
83+
const repoId = stripProtocolAndGitSuffix(cloneUrl);
84+
const repoName = getRepoNameFromUrl(config.url);
85+
const repoPath = path.resolve(path.join(ctx.reposPath, `${repoId}.git`));
86+
const repo: GitRepository = {
87+
vcs: 'git',
88+
id: repoId,
89+
name: repoName,
90+
path: repoPath,
91+
isStale: false,
92+
cloneUrl: cloneUrl,
93+
branches: [],
94+
tags: [],
95+
}
96+
97+
if (config.revisions) {
98+
if (config.revisions.branches) {
99+
const branchGlobs = config.revisions.branches;
100+
const git = simpleGit();
101+
const branchList = await git.listRemote(['--heads', cloneUrl]);
102+
const branches = branchList
103+
.split('\n')
104+
.map(line => line.split('\t')[1])
105+
.filter(Boolean)
106+
.map(branch => branch.replace('refs/heads/', ''));
107+
108+
repo.branches = branches.filter(branch =>
109+
branchGlobs.some(glob => new RegExp(glob).test(branch))
110+
);
111+
}
112+
113+
if (config.revisions.tags) {
114+
const tagGlobs = config.revisions.tags;
115+
const git = simpleGit();
116+
const tagList = await git.listRemote(['--tags', cloneUrl]);
117+
const tags = tagList
118+
.split('\n')
119+
.map(line => line.split('\t')[1])
120+
.filter(Boolean)
121+
.map(tag => tag.replace('refs/tags/', ''));
122+
123+
repo.tags = tags.filter(tag =>
124+
tagGlobs.some(glob => new RegExp(glob).test(tag))
125+
);
126+
}
127+
}
128+
129+
return repo;
51130
}

packages/backend/src/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { getGitLabReposFromConfig } from "./gitlab.js";
66
import { getGiteaReposFromConfig } from "./gitea.js";
77
import { getGerritReposFromConfig } from "./gerrit.js";
88
import { AppContext, LocalRepository, GitRepository, Repository, Settings } from "./types.js";
9-
import { cloneRepository, fetchRepository } from "./git.js";
9+
import { cloneRepository, fetchRepository, getGitRepoFromConfig } from "./git.js";
1010
import { createLogger } from "./logger.js";
1111
import { createRepository, Database, loadDB, updateRepository, updateSettings } from './db.js';
1212
import { arraysEqualShallow, stringsEqualFalseySafe, isRemotePath, measure } from "./utils.js";
@@ -246,6 +246,11 @@ const syncConfig = async (configPath: string, db: Database, signal: AbortSignal,
246246
configRepos.push(repo);
247247
break;
248248
}
249+
case 'git': {
250+
const gitRepo = await getGitRepoFromConfig(repoConfig, ctx);
251+
gitRepo && configRepos.push(gitRepo);
252+
break;
253+
}
249254
}
250255
}
251256

packages/backend/src/schemas/v2.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
22

3-
export type Repos = GitHubConfig | GitLabConfig | GiteaConfig | GerritConfig | LocalConfig;
3+
export type Repos = GitHubConfig | GitLabConfig | GiteaConfig | GerritConfig | LocalConfig | GitConfig;
44

55
/**
66
* A Sourcebot configuration file outlines which repositories Sourcebot should sync and index.
@@ -268,3 +268,14 @@ export interface LocalConfig {
268268
paths?: string[];
269269
};
270270
}
271+
export interface GitConfig {
272+
/**
273+
* Git Configuration
274+
*/
275+
type: "git";
276+
/**
277+
* The URL to the git repository.
278+
*/
279+
url: string;
280+
revisions?: GitRevisions;
281+
}

packages/web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@codemirror/search": "^6.5.6",
3636
"@codemirror/state": "^6.4.1",
3737
"@codemirror/view": "^6.33.0",
38+
"@floating-ui/react": "^0.27.2",
3839
"@hookform/resolvers": "^3.9.0",
3940
"@iconify/react": "^5.1.0",
4041
"@iizukak/codemirror-lang-wgsl": "^0.3.0",
@@ -87,7 +88,7 @@
8788
"graphql": "^16.9.0",
8889
"http-status-codes": "^2.3.0",
8990
"lucide-react": "^0.435.0",
90-
"next": "14.2.15",
91+
"next": "14.2.21",
9192
"next-themes": "^0.3.0",
9293
"posthog-js": "^1.161.5",
9394
"pretty-bytes": "^6.1.1",

packages/web/src/app/api/(client)/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { NEXT_PUBLIC_DOMAIN_SUB_PATH } from "@/lib/environment.client";
44
import { fileSourceResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas";
55
import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types";
6+
import assert from "assert";
67

78
export const search = async (body: SearchRequest): Promise<SearchResponse> => {
89
const path = resolveServerPath("/api/search");
@@ -48,5 +49,6 @@ export const getRepos = async (): Promise<ListRepositoriesResponse> => {
4849
* the base path (if any).
4950
*/
5051
export const resolveServerPath = (path: string) => {
52+
assert(path.startsWith("/"));
5153
return `${NEXT_PUBLIC_DOMAIN_SUB_PATH}${path}`;
5254
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
'use client';
2+
3+
import { ScrollArea } from "@/components/ui/scroll-area";
4+
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
5+
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
6+
import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
7+
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
8+
import { search } from "@codemirror/search";
9+
import CodeMirror, { Decoration, DecorationSet, EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, StateField, ViewUpdate } from "@uiw/react-codemirror";
10+
import { useEffect, useMemo, useRef, useState } from "react";
11+
import { EditorContextMenu } from "../../components/editorContextMenu";
12+
13+
interface CodePreviewProps {
14+
path: string;
15+
repoName: string;
16+
revisionName: string;
17+
source: string;
18+
language: string;
19+
}
20+
21+
export const CodePreview = ({
22+
source,
23+
language,
24+
path,
25+
repoName,
26+
revisionName,
27+
}: CodePreviewProps) => {
28+
const editorRef = useRef<ReactCodeMirrorRef>(null);
29+
const syntaxHighlighting = useSyntaxHighlightingExtension(language, editorRef.current?.view);
30+
const [currentSelection, setCurrentSelection] = useState<SelectionRange>();
31+
const keymapExtension = useKeymapExtension(editorRef.current?.view);
32+
const [isEditorCreated, setIsEditorCreated] = useState(false);
33+
34+
const highlightRangeQuery = useNonEmptyQueryParam('highlightRange');
35+
const highlightRange = useMemo(() => {
36+
if (!highlightRangeQuery) {
37+
return;
38+
}
39+
40+
const rangeRegex = /^\d+:\d+,\d+:\d+$/;
41+
if (!rangeRegex.test(highlightRangeQuery)) {
42+
return;
43+
}
44+
45+
const [start, end] = highlightRangeQuery.split(',').map((range) => {
46+
return range.split(':').map((val) => parseInt(val, 10));
47+
});
48+
49+
return {
50+
start: {
51+
line: start[0],
52+
character: start[1],
53+
},
54+
end: {
55+
line: end[0],
56+
character: end[1],
57+
}
58+
}
59+
}, [highlightRangeQuery]);
60+
61+
const extensions = useMemo(() => {
62+
const highlightDecoration = Decoration.mark({
63+
class: "cm-searchMatch-selected",
64+
});
65+
66+
return [
67+
syntaxHighlighting,
68+
EditorView.lineWrapping,
69+
keymapExtension,
70+
search({
71+
top: true,
72+
}),
73+
EditorView.updateListener.of((update: ViewUpdate) => {
74+
if (update.selectionSet) {
75+
setCurrentSelection(update.state.selection.main);
76+
}
77+
}),
78+
StateField.define<DecorationSet>({
79+
create(state) {
80+
if (!highlightRange) {
81+
return Decoration.none;
82+
}
83+
84+
const { start, end } = highlightRange;
85+
const from = state.doc.line(start.line).from + start.character - 1;
86+
const to = state.doc.line(end.line).from + end.character - 1;
87+
88+
return Decoration.set([
89+
highlightDecoration.range(from, to),
90+
]);
91+
},
92+
update(deco, tr) {
93+
return deco.map(tr.changes);
94+
},
95+
provide: (field) => EditorView.decorations.from(field),
96+
}),
97+
];
98+
}, [keymapExtension, syntaxHighlighting, highlightRange]);
99+
100+
useEffect(() => {
101+
if (!highlightRange || !editorRef.current || !editorRef.current.state) {
102+
return;
103+
}
104+
105+
const doc = editorRef.current.state.doc;
106+
const { start, end } = highlightRange;
107+
const from = doc.line(start.line).from + start.character - 1;
108+
const to = doc.line(end.line).from + end.character - 1;
109+
const selection = EditorSelection.range(from, to);
110+
111+
editorRef.current.view?.dispatch({
112+
effects: [
113+
EditorView.scrollIntoView(selection, { y: "center" }),
114+
]
115+
});
116+
// @note: we need to include `isEditorCreated` in the dependency array since
117+
// a race-condition can happen if the `highlightRange` is resolved before the
118+
// editor is created.
119+
// eslint-disable-next-line react-hooks/exhaustive-deps
120+
}, [highlightRange, isEditorCreated]);
121+
122+
const { theme } = useThemeNormalized();
123+
124+
return (
125+
<ScrollArea className="h-full overflow-auto flex-1">
126+
<CodeMirror
127+
className="relative"
128+
ref={editorRef}
129+
onCreateEditor={() => {
130+
setIsEditorCreated(true);
131+
}}
132+
value={source}
133+
extensions={extensions}
134+
readOnly={true}
135+
theme={theme === "dark" ? "dark" : "light"}
136+
>
137+
{editorRef.current && editorRef.current.view && currentSelection && (
138+
<EditorContextMenu
139+
view={editorRef.current.view}
140+
selection={currentSelection}
141+
repoName={repoName}
142+
path={path}
143+
revisionName={revisionName}
144+
/>
145+
)}
146+
</CodeMirror>
147+
</ScrollArea>
148+
)
149+
}
150+

0 commit comments

Comments
 (0)