|
| 1 | +import { test, expect, describe, beforeEach, afterEach } from 'bun:test'; |
| 2 | +import { resolve } from 'node:path'; |
| 3 | +import { mkdtemp, rm } from 'node:fs/promises'; |
| 4 | +import { tmpdir } from 'node:os'; |
| 5 | +import { runArgs } from '../../src/utils/shell.ts'; |
| 6 | +import { |
| 7 | + createTag, |
| 8 | + tagExists, |
| 9 | + listTags, |
| 10 | + pushWithTags, |
| 11 | + hasUncommittedChanges, |
| 12 | + getCurrentBranch, |
| 13 | + commitFiles, |
| 14 | +} from '../../src/core/git.ts'; |
| 15 | +import { writeText } from '../../src/utils/fs.ts'; |
| 16 | + |
| 17 | +function initRepo(dir: string) { |
| 18 | + runArgs(['git', 'init'], { cwd: dir }); |
| 19 | + runArgs(['git', 'commit', '--allow-empty', '-m', 'init'], { cwd: dir }); |
| 20 | +} |
| 21 | + |
| 22 | +describe('git helpers', () => { |
| 23 | + let tmpDir: string; |
| 24 | + |
| 25 | + beforeEach(async () => { |
| 26 | + tmpDir = await mkdtemp(resolve(tmpdir(), 'bumpy-git-test-')); |
| 27 | + initRepo(tmpDir); |
| 28 | + }); |
| 29 | + |
| 30 | + afterEach(async () => { |
| 31 | + await rm(tmpDir, { recursive: true }); |
| 32 | + }); |
| 33 | + |
| 34 | + // ---- createTag / tagExists ---- |
| 35 | + |
| 36 | + describe('createTag & tagExists', () => { |
| 37 | + test('creates a tag and detects it exists', () => { |
| 38 | + expect(tagExists('v1.0.0', { cwd: tmpDir })).toBe(false); |
| 39 | + createTag('v1.0.0', { cwd: tmpDir }); |
| 40 | + expect(tagExists('v1.0.0', { cwd: tmpDir })).toBe(true); |
| 41 | + }); |
| 42 | + |
| 43 | + test('tagExists returns false for non-existent tag', () => { |
| 44 | + expect(tagExists('nope', { cwd: tmpDir })).toBe(false); |
| 45 | + }); |
| 46 | + |
| 47 | + test('scoped package tag with @ and /', () => { |
| 48 | + createTag('@scope/pkg@1.2.3', { cwd: tmpDir }); |
| 49 | + expect(tagExists('@scope/pkg@1.2.3', { cwd: tmpDir })).toBe(true); |
| 50 | + expect(tagExists('@scope/pkg@1.2.4', { cwd: tmpDir })).toBe(false); |
| 51 | + }); |
| 52 | + }); |
| 53 | + |
| 54 | + // ---- listTags ---- |
| 55 | + |
| 56 | + describe('listTags', () => { |
| 57 | + test('returns empty array when no tags match', () => { |
| 58 | + expect(listTags('v*', { cwd: tmpDir })).toEqual([]); |
| 59 | + }); |
| 60 | + |
| 61 | + test('lists tags matching a pattern', () => { |
| 62 | + createTag('v1.0.0', { cwd: tmpDir }); |
| 63 | + createTag('v1.1.0', { cwd: tmpDir }); |
| 64 | + createTag('other-tag', { cwd: tmpDir }); |
| 65 | + |
| 66 | + const result = listTags('v*', { cwd: tmpDir }); |
| 67 | + expect(result).toContain('v1.0.0'); |
| 68 | + expect(result).toContain('v1.1.0'); |
| 69 | + expect(result).not.toContain('other-tag'); |
| 70 | + }); |
| 71 | + |
| 72 | + test('glob matches date-based release tags for suffix logic', () => { |
| 73 | + // This is the pattern used by createAggregateRelease |
| 74 | + createTag('release-2026-04-14', { cwd: tmpDir }); |
| 75 | + expect(listTags('release-2026-04-14*', { cwd: tmpDir })).toEqual(['release-2026-04-14']); |
| 76 | + |
| 77 | + createTag('release-2026-04-14-2', { cwd: tmpDir }); |
| 78 | + const tags = listTags('release-2026-04-14*', { cwd: tmpDir }); |
| 79 | + expect(tags).toHaveLength(2); |
| 80 | + expect(tags).toContain('release-2026-04-14'); |
| 81 | + expect(tags).toContain('release-2026-04-14-2'); |
| 82 | + |
| 83 | + // Different date should not match |
| 84 | + createTag('release-2026-04-15', { cwd: tmpDir }); |
| 85 | + expect(listTags('release-2026-04-14*', { cwd: tmpDir })).toHaveLength(2); |
| 86 | + }); |
| 87 | + }); |
| 88 | + |
| 89 | + // ---- hasUncommittedChanges ---- |
| 90 | + |
| 91 | + describe('hasUncommittedChanges', () => { |
| 92 | + test('returns false on clean repo', () => { |
| 93 | + expect(hasUncommittedChanges({ cwd: tmpDir })).toBe(false); |
| 94 | + }); |
| 95 | + |
| 96 | + test('returns true with uncommitted files', async () => { |
| 97 | + await writeText(resolve(tmpDir, 'dirty.txt'), 'hello'); |
| 98 | + expect(hasUncommittedChanges({ cwd: tmpDir })).toBe(true); |
| 99 | + }); |
| 100 | + }); |
| 101 | + |
| 102 | + // ---- getCurrentBranch ---- |
| 103 | + |
| 104 | + describe('getCurrentBranch', () => { |
| 105 | + test('returns current branch name', () => { |
| 106 | + // git init defaults to main or master depending on config |
| 107 | + const branch = getCurrentBranch({ cwd: tmpDir }); |
| 108 | + expect(typeof branch).toBe('string'); |
| 109 | + expect(branch!.length).toBeGreaterThan(0); |
| 110 | + }); |
| 111 | + }); |
| 112 | + |
| 113 | + // ---- commitFiles ---- |
| 114 | + |
| 115 | + describe('commitFiles', () => { |
| 116 | + test('stages and commits specified files', async () => { |
| 117 | + await writeText(resolve(tmpDir, 'a.txt'), 'aaa'); |
| 118 | + await writeText(resolve(tmpDir, 'b.txt'), 'bbb'); |
| 119 | + |
| 120 | + commitFiles(['a.txt', 'b.txt'], 'add files', { cwd: tmpDir }); |
| 121 | + |
| 122 | + // Verify clean working tree |
| 123 | + expect(hasUncommittedChanges({ cwd: tmpDir })).toBe(false); |
| 124 | + }); |
| 125 | + |
| 126 | + test('only stages specified files', async () => { |
| 127 | + await writeText(resolve(tmpDir, 'staged.txt'), 'yes'); |
| 128 | + await writeText(resolve(tmpDir, 'unstaged.txt'), 'no'); |
| 129 | + |
| 130 | + commitFiles(['staged.txt'], 'partial commit', { cwd: tmpDir }); |
| 131 | + |
| 132 | + // unstaged.txt should still be dirty |
| 133 | + expect(hasUncommittedChanges({ cwd: tmpDir })).toBe(true); |
| 134 | + }); |
| 135 | + }); |
| 136 | + |
| 137 | + // ---- pushWithTags ---- |
| 138 | + |
| 139 | + describe('pushWithTags', () => { |
| 140 | + test('pushes commits and tags to remote', async () => { |
| 141 | + // Set up a bare remote and clone |
| 142 | + const remoteDir = await mkdtemp(resolve(tmpdir(), 'bumpy-remote-')); |
| 143 | + runArgs(['git', 'init', '--bare'], { cwd: remoteDir }); |
| 144 | + runArgs(['git', 'remote', 'add', 'origin', remoteDir], { cwd: tmpDir }); |
| 145 | + // Push once with -u to set up tracking before testing pushWithTags |
| 146 | + runArgs(['git', 'push', '-u', 'origin', 'HEAD'], { cwd: tmpDir }); |
| 147 | + |
| 148 | + createTag('v1.0.0', { cwd: tmpDir }); |
| 149 | + pushWithTags({ cwd: tmpDir }); |
| 150 | + |
| 151 | + // Clone from remote and check the tag arrived |
| 152 | + const cloneDir = await mkdtemp(resolve(tmpdir(), 'bumpy-clone-')); |
| 153 | + runArgs(['git', 'clone', remoteDir, '.'], { cwd: cloneDir }); |
| 154 | + expect(tagExists('v1.0.0', { cwd: cloneDir })).toBe(true); |
| 155 | + |
| 156 | + await rm(remoteDir, { recursive: true }); |
| 157 | + await rm(cloneDir, { recursive: true }); |
| 158 | + }); |
| 159 | + }); |
| 160 | +}); |
0 commit comments