Skip to content

Commit 4353d20

Browse files
Add autoDeleteStaleRepos config option (#128)
1 parent 4d358f9 commit 4353d20

File tree

13 files changed

+292
-17
lines changed

13 files changed

+292
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Changed
1515

1616
- Made language suggestions case insensitive. ([#124](https://github.com/sourcebot-dev/sourcebot/pull/124))
17+
- Stale repositories are now automatically deleted from the index. This can be configured via `settings.autoDeleteStaleRepos` in the config. ([#128](https://github.com/sourcebot-dev/sourcebot/pull/128))
1718

1819
## [2.6.1] - 2024-12-09
1920

packages/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"cross-fetch": "^4.0.0",
2929
"dotenv": "^16.4.5",
3030
"gitea-js": "^1.22.0",
31+
"glob": "^11.0.0",
3132
"lowdb": "^7.0.1",
3233
"micromatch": "^4.0.8",
3334
"posthog-node": "^4.2.1",

packages/backend/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ export const RESYNC_CONFIG_INTERVAL_MS = 1000 * 60 * 60 * 24;
1515
*/
1616
export const DEFAULT_SETTINGS: Settings = {
1717
maxFileSize: 2 * 1024 * 1024, // 2MB in bytes
18+
autoDeleteStaleRepos: true,
1819
}

packages/backend/src/db.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
import { expect, test } from 'vitest';
2-
import { migration_addMaxFileSize, migration_addSettings, Schema } from './db';
2+
import { DEFAULT_DB_DATA, migration_addDeleteStaleRepos, migration_addMaxFileSize, migration_addSettings, Schema } from './db';
33
import { DEFAULT_SETTINGS } from './constants';
44
import { DeepPartial } from './types';
5+
import { Low } from 'lowdb';
56

7+
class InMemoryAdapter<T> {
8+
private data: T;
9+
async read() {
10+
return this.data;
11+
}
12+
async write(data: T) {
13+
this.data = data;
14+
}
15+
}
16+
17+
export const createMockDB = (defaultData: Schema = DEFAULT_DB_DATA) => {
18+
const db = new Low(new InMemoryAdapter<Schema>(), defaultData);
19+
return db;
20+
}
621

722
test('migration_addSettings adds the `settings` field with defaults if it does not exist', () => {
823
const schema: DeepPartial<Schema> = {};
@@ -29,4 +44,20 @@ test('migration_addMaxFileSize adds the `maxFileSize` field with the default val
2944
test('migration_addMaxFileSize will throw if `settings` is not defined', () => {
3045
const schema: DeepPartial<Schema> = {};
3146
expect(() => migration_addMaxFileSize(schema as Schema)).toThrow();
47+
});
48+
49+
test('migration_addDeleteStaleRepos adds the `autoDeleteStaleRepos` field with the default value if it does not exist', () => {
50+
const schema: DeepPartial<Schema> = {
51+
settings: {
52+
maxFileSize: DEFAULT_SETTINGS.maxFileSize,
53+
},
54+
}
55+
56+
const migratedSchema = migration_addDeleteStaleRepos(schema as Schema);
57+
expect(migratedSchema).toStrictEqual({
58+
settings: {
59+
maxFileSize: DEFAULT_SETTINGS.maxFileSize,
60+
autoDeleteStaleRepos: DEFAULT_SETTINGS.autoDeleteStaleRepos,
61+
}
62+
});
3263
});

packages/backend/src/db.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ export type Schema = {
1313
}
1414
}
1515

16+
export const DEFAULT_DB_DATA: Schema = {
17+
repos: {},
18+
settings: DEFAULT_SETTINGS,
19+
}
20+
1621
export type Database = Low<Schema>;
1722

1823
export const loadDB = async (ctx: AppContext): Promise<Database> => {
19-
const db = await JSONFilePreset<Schema>(`${ctx.cachePath}/db.json`, {
20-
repos: {},
21-
settings: DEFAULT_SETTINGS,
22-
});
24+
const db = await JSONFilePreset<Schema>(`${ctx.cachePath}/db.json`, DEFAULT_DB_DATA);
2325

2426
await applyMigrations(db);
2527

@@ -53,6 +55,7 @@ export const applyMigrations = async (db: Database) => {
5355
// @NOTE: please ensure new migrations are added after older ones!
5456
schema = migration_addSettings(schema, log);
5557
schema = migration_addMaxFileSize(schema, log);
58+
schema = migration_addDeleteStaleRepos(schema, log);
5659
return schema;
5760
});
5861
}
@@ -78,5 +81,17 @@ export const migration_addMaxFileSize = (schema: Schema, log?: (name: string) =>
7881
schema.settings.maxFileSize = DEFAULT_SETTINGS.maxFileSize;
7982
}
8083

84+
return schema;
85+
}
86+
87+
/**
88+
* @see: https://github.com/sourcebot-dev/sourcebot/pull/128
89+
*/
90+
export const migration_addDeleteStaleRepos = (schema: Schema, log?: (name: string) => void) => {
91+
if (schema.settings.autoDeleteStaleRepos === undefined) {
92+
log?.("deleteStaleRepos");
93+
schema.settings.autoDeleteStaleRepos = DEFAULT_SETTINGS.autoDeleteStaleRepos;
94+
}
95+
8196
return schema;
8297
}

packages/backend/src/github.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ export const getGitHubReposFromConfig = async (config: GitHubConfig, signal: Abo
100100
});
101101

102102
if (config.topics) {
103-
repos = includeReposByTopic(repos, config.topics, logger);
103+
const topics = config.topics.map(topic => topic.toLowerCase());
104+
repos = includeReposByTopic(repos, topics, logger);
104105
}
105106

106107
if (config.exclude) {
@@ -117,7 +118,8 @@ export const getGitHubReposFromConfig = async (config: GitHubConfig, signal: Abo
117118
}
118119

119120
if (config.exclude.topics) {
120-
repos = excludeReposByTopic(repos, config.exclude.topics, logger);
121+
const topics = config.exclude.topics.map(topic => topic.toLowerCase());
122+
repos = excludeReposByTopic(repos, topics, logger);
121123
}
122124
}
123125

packages/backend/src/gitlab.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ export const getGitLabReposFromConfig = async (config: GitLabConfig, ctx: AppCon
115115
});
116116

117117
if (config.topics) {
118-
repos = includeReposByTopic(repos, config.topics, logger);
118+
const topics = config.topics.map(topic => topic.toLowerCase());
119+
repos = includeReposByTopic(repos, topics, logger);
119120
}
120121

121122
if (config.exclude) {
@@ -132,7 +133,8 @@ export const getGitLabReposFromConfig = async (config: GitLabConfig, ctx: AppCon
132133
}
133134

134135
if (config.exclude.topics) {
135-
repos = excludeReposByTopic(repos, config.exclude.topics, logger);
136+
const topics = config.exclude.topics.map(topic => topic.toLowerCase());
137+
repos = excludeReposByTopic(repos, topics, logger);
136138
}
137139
}
138140

packages/backend/src/main.test.ts

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
1-
import { expect, test } from 'vitest';
2-
import { isAllRepoReindexingRequired, isRepoReindexingRequired } from './main';
3-
import { Repository, Settings } from './types';
1+
import { expect, test, vi } from 'vitest';
2+
import { deleteStaleRepository, isAllRepoReindexingRequired, isRepoReindexingRequired } from './main';
3+
import { AppContext, GitRepository, LocalRepository, Repository, Settings } from './types';
4+
import { DEFAULT_DB_DATA } from './db';
5+
import { createMockDB } from './db.test';
6+
import { rm } from 'fs/promises';
7+
import path from 'path';
8+
import { glob } from 'glob';
9+
10+
vi.mock('fs/promises', () => ({
11+
rm: vi.fn(),
12+
}));
13+
14+
vi.mock('glob', () => ({
15+
glob: vi.fn().mockReturnValue(['fake_index.zoekt']),
16+
}));
17+
18+
const createMockContext = (rootPath: string = '/app') => {
19+
return {
20+
configPath: path.join(rootPath, 'config.json'),
21+
cachePath: path.join(rootPath, '.sourcebot'),
22+
indexPath: path.join(rootPath, '.sourcebot/index'),
23+
reposPath: path.join(rootPath, '.sourcebot/repos'),
24+
} satisfies AppContext;
25+
}
26+
427

528
test('isRepoReindexingRequired should return false when no changes are made', () => {
629
const previous: Repository = {
@@ -80,6 +103,7 @@ test('isRepoReindexingRequired should return true when local excludedPaths chang
80103
test('isAllRepoReindexingRequired should return false when fileLimitSize has not changed', () => {
81104
const previous: Settings = {
82105
maxFileSize: 1000,
106+
autoDeleteStaleRepos: true,
83107
}
84108
const current: Settings = {
85109
...previous,
@@ -90,10 +114,89 @@ test('isAllRepoReindexingRequired should return false when fileLimitSize has not
90114
test('isAllRepoReindexingRequired should return true when fileLimitSize has changed', () => {
91115
const previous: Settings = {
92116
maxFileSize: 1000,
117+
autoDeleteStaleRepos: true,
93118
}
94119
const current: Settings = {
95120
...previous,
96121
maxFileSize: 2000,
97122
}
98123
expect(isAllRepoReindexingRequired(previous, current)).toBe(true);
124+
});
125+
126+
test('isAllRepoReindexingRequired should return false when autoDeleteStaleRepos has changed', () => {
127+
const previous: Settings = {
128+
maxFileSize: 1000,
129+
autoDeleteStaleRepos: true,
130+
}
131+
const current: Settings = {
132+
...previous,
133+
autoDeleteStaleRepos: false,
134+
}
135+
expect(isAllRepoReindexingRequired(previous, current)).toBe(false);
136+
});
137+
138+
test('deleteStaleRepository can delete a git repository', async () => {
139+
const ctx = createMockContext();
140+
141+
const repo: GitRepository = {
142+
id: 'github.com/sourcebot-dev/sourcebot',
143+
vcs: 'git',
144+
name: 'sourcebot',
145+
cloneUrl: 'https://github.com/sourcebot-dev/sourcebot',
146+
path: `${ctx.reposPath}/github.com/sourcebot-dev/sourcebot`,
147+
branches: ['main'],
148+
tags: [''],
149+
isStale: true,
150+
}
151+
152+
const db = createMockDB({
153+
...DEFAULT_DB_DATA,
154+
repos: {
155+
'github.com/sourcebot-dev/sourcebot': repo,
156+
}
157+
});
158+
159+
160+
await deleteStaleRepository(repo, db, ctx);
161+
162+
expect(db.data.repos['github.com/sourcebot-dev/sourcebot']).toBeUndefined();;
163+
expect(rm).toHaveBeenCalledWith(`${ctx.reposPath}/github.com/sourcebot-dev/sourcebot`, {
164+
recursive: true,
165+
});
166+
expect(glob).toHaveBeenCalledWith(`github.com%2Fsourcebot-dev%2Fsourcebot*.zoekt`, {
167+
cwd: ctx.indexPath,
168+
absolute: true
169+
});
170+
expect(rm).toHaveBeenCalledWith(`fake_index.zoekt`);
171+
});
172+
173+
test('deleteStaleRepository can delete a local repository', async () => {
174+
const ctx = createMockContext();
175+
176+
const repo: LocalRepository = {
177+
vcs: 'local',
178+
name: 'UnrealEngine',
179+
id: '/path/to/UnrealEngine',
180+
path: '/path/to/UnrealEngine',
181+
watch: false,
182+
excludedPaths: [],
183+
isStale: true,
184+
}
185+
186+
const db = createMockDB({
187+
...DEFAULT_DB_DATA,
188+
repos: {
189+
'/path/to/UnrealEngine': repo,
190+
}
191+
});
192+
193+
await deleteStaleRepository(repo, db, ctx);
194+
195+
expect(db.data.repos['/path/to/UnrealEngine']).toBeUndefined();
196+
expect(rm).not.toHaveBeenCalledWith('/path/to/UnrealEngine');
197+
expect(glob).toHaveBeenCalledWith(`UnrealEngine*.zoekt`, {
198+
cwd: ctx.indexPath,
199+
absolute: true
200+
});
201+
expect(rm).toHaveBeenCalledWith('fake_index.zoekt');
99202
});

0 commit comments

Comments
 (0)