|
1 | 1 | import assert from 'node:assert' |
2 | 2 | import * as Path from 'path' |
3 | 3 | import { describe, it } from 'node:test' |
| 4 | +import { rm } from 'fs/promises' |
4 | 5 | import { exec } from 'dugite' |
5 | 6 | import { setupEmptyRepository } from '../../helpers/repositories' |
6 | 7 | import { makeCommit } from '../../helpers/repository-scaffolding' |
7 | 8 | import { |
8 | 9 | parseWorktreePorcelainOutput, |
9 | 10 | listWorktrees, |
| 11 | + getMainWorktreePath, |
| 12 | + getRepositoryType, |
10 | 13 | } from '../../../src/lib/git' |
| 14 | +import { Repository } from '../../../src/models/repository' |
11 | 15 |
|
12 | 16 | describe('git/worktree', () => { |
13 | 17 | describe('parseWorktreePorcelainOutput', () => { |
@@ -293,4 +297,109 @@ describe('git/worktree', () => { |
293 | 297 | assert(branches.has('refs/heads/main')) |
294 | 298 | }) |
295 | 299 | }) |
| 300 | + |
| 301 | + describe('getMainWorktreePath', () => { |
| 302 | + /** Build a Repository pointing at `path`, populating its real `gitDir`. */ |
| 303 | + async function repositoryAt(path: string): Promise<Repository> { |
| 304 | + const type = await getRepositoryType(path) |
| 305 | + const gitDir = type.kind === 'regular' ? type.gitDir : undefined |
| 306 | + return new Repository( |
| 307 | + path, |
| 308 | + -1, |
| 309 | + null, |
| 310 | + false, |
| 311 | + null, |
| 312 | + null, |
| 313 | + null, |
| 314 | + {}, |
| 315 | + null, |
| 316 | + false, |
| 317 | + null, |
| 318 | + gitDir |
| 319 | + ) |
| 320 | + } |
| 321 | + |
| 322 | + it('returns the main worktree path for a removed linked worktree', async t => { |
| 323 | + const repo = await setupEmptyRepository(t, 'main') |
| 324 | + await makeCommit(repo, { |
| 325 | + entries: [{ path: 'README', contents: 'hello' }], |
| 326 | + }) |
| 327 | + await exec(['branch', 'feature-a'], repo.path) |
| 328 | + |
| 329 | + const worktreePath = repo.path + '-wt-a' |
| 330 | + await exec(['worktree', 'add', worktreePath, 'feature-a'], repo.path) |
| 331 | + |
| 332 | + const linkedRepo = await repositoryAt(worktreePath) |
| 333 | + |
| 334 | + // rm leaves the worktree's admin files (and `commondir`) intact. |
| 335 | + await rm(worktreePath, { recursive: true, force: true }) |
| 336 | + |
| 337 | + assert.strictEqual( |
| 338 | + await getMainWorktreePath(linkedRepo), |
| 339 | + Path.normalize(repo.path) |
| 340 | + ) |
| 341 | + }) |
| 342 | + |
| 343 | + it('returns the main worktree path when the linked worktree was fully removed with `git worktree remove`', async t => { |
| 344 | + const repo = await setupEmptyRepository(t, 'main') |
| 345 | + await makeCommit(repo, { |
| 346 | + entries: [{ path: 'README', contents: 'hello' }], |
| 347 | + }) |
| 348 | + await exec(['branch', 'feature-a'], repo.path) |
| 349 | + await exec(['branch', 'feature-b'], repo.path) |
| 350 | + |
| 351 | + const worktreeA = repo.path + '-wt-a' |
| 352 | + const worktreeB = repo.path + '-wt-b' |
| 353 | + await exec(['worktree', 'add', worktreeA, 'feature-a'], repo.path) |
| 354 | + await exec(['worktree', 'add', worktreeB, 'feature-b'], repo.path) |
| 355 | + |
| 356 | + const linkedRepo = await repositoryAt(worktreeA) |
| 357 | + |
| 358 | + // `git worktree remove` also deletes the admin files (so `commondir` is |
| 359 | + // unreadable); worktree B keeps `.git/worktrees` itself on disk. |
| 360 | + await exec(['worktree', 'remove', '--force', worktreeA], repo.path) |
| 361 | + |
| 362 | + assert.strictEqual( |
| 363 | + await getMainWorktreePath(linkedRepo), |
| 364 | + Path.normalize(repo.path) |
| 365 | + ) |
| 366 | + }) |
| 367 | + |
| 368 | + it('returns its own path when called on the main worktree', async t => { |
| 369 | + const repo = await setupEmptyRepository(t, 'main') |
| 370 | + await makeCommit(repo, { |
| 371 | + entries: [{ path: 'README', contents: 'hello' }], |
| 372 | + }) |
| 373 | + |
| 374 | + const mainRepo = await repositoryAt(repo.path) |
| 375 | + assert.strictEqual( |
| 376 | + await getMainWorktreePath(mainRepo), |
| 377 | + Path.normalize(repo.path) |
| 378 | + ) |
| 379 | + }) |
| 380 | + |
| 381 | + it('returns null when the repository gitDir is unknown', async t => { |
| 382 | + const repo = await setupEmptyRepository(t, 'main') |
| 383 | + assert.strictEqual(await getMainWorktreePath(repo), null) |
| 384 | + }) |
| 385 | + |
| 386 | + it('returns null when the main worktree no longer exists on disk', async () => { |
| 387 | + const missingRepo = new Repository( |
| 388 | + Path.normalize('/this/path/does/not/exist'), |
| 389 | + -1, |
| 390 | + null, |
| 391 | + false, |
| 392 | + null, |
| 393 | + null, |
| 394 | + null, |
| 395 | + {}, |
| 396 | + null, |
| 397 | + false, |
| 398 | + null, |
| 399 | + Path.normalize('/this/path/does/not/exist/.git/worktrees/foo') |
| 400 | + ) |
| 401 | + |
| 402 | + assert.strictEqual(await getMainWorktreePath(missingRepo), null) |
| 403 | + }) |
| 404 | + }) |
296 | 405 | }) |
0 commit comments