• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

apowers313 / aiforge / 21570337701

01 Feb 2026 09:11PM UTC coverage: 81.026% (-2.9%) from 83.954%
21570337701

push

github

apowers313
test: increase coverage to 80%+

2049 of 2382 branches covered (86.02%)

Branch coverage included in aggregate %.

1849 of 2529 new or added lines in 25 files covered. (73.11%)

681 existing lines in 21 files now uncovered.

9861 of 12317 relevant lines covered (80.06%)

26.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.92
/src/server/services/project/WorktreeService.ts
1
/**
2
 * WorktreeService - Business logic for git worktree operations
3
 *
4
 * This service provides worktree operations for projects.
5
 * Phase 2 implements: getWorktrees, isGitRepository
6
 * Phase 3 adds: createWorktree, getMainBranch
7
 */
8
import { join } from 'node:path';
1✔
9
import type { WorktreeWithStatus } from '@shared/types/index.js';
10
import type { ProjectStore } from '../../storage/stores/ProjectStore.js';
11
import { GitService } from '../git/GitService.js';
1✔
12

13
export interface WorktreeServiceOptions {
14
  projectStore: ProjectStore;
15
}
16

17
export class WorktreeService {
1✔
18
  private readonly projectStore: ProjectStore;
36✔
19

20
  constructor(options: WorktreeServiceOptions) {
36✔
21
    this.projectStore = options.projectStore;
36✔
22
  }
36✔
23

24
  /**
25
   * Check if a project is a git repository
26
   */
27
  async isGitRepository(projectId: string): Promise<boolean> {
36✔
28
    const project = await this.projectStore.getById(projectId);
3✔
29
    if (!project) {
3✔
30
      return false;
1✔
31
    }
1✔
32

33
    const gitService = new GitService(project.path);
2✔
34
    return gitService.isGitRepository();
2✔
35
  }
3✔
36

37
  /**
38
   * Get all worktrees for a project with status information
39
   * Phase 5: Now calculates actual modifiedCount, ahead, behind values
40
   */
41
  async getWorktrees(projectId: string): Promise<WorktreeWithStatus[]> {
36✔
42
    const project = await this.projectStore.getById(projectId);
29✔
43
    if (!project) {
29✔
44
      return [];
2✔
45
    }
2✔
46

47
    const gitService = new GitService(project.path);
27✔
48

49
    // Check if it's a git repo with commits
50
    if (!(await gitService.hasCommits())) {
29✔
51
      return [];
3✔
52
    }
3✔
53

54
    // Get raw worktrees and main branch
55
    const worktrees = await gitService.listWorktrees();
24✔
56
    const mainBranch = await gitService.getMainBranch();
24✔
57

58
    // Enrich with status information (Phase 5)
59
    const enrichedWorktrees = await Promise.all(
24✔
60
      worktrees.map(async (wt) => {
24✔
61
        // Create a GitService for each worktree to get its specific status
62
        const wtGitService = new GitService(wt.path);
40✔
63
        const modifiedCount = await wtGitService.getModifiedFileCount();
40✔
64
        const { ahead, behind } = await wtGitService.getAheadBehind(mainBranch);
40✔
65

66
        return {
40✔
67
          ...wt,
40✔
68
          name: this.getWorktreeName(wt.branch, wt.path),
40✔
69
          modifiedCount,
40✔
70
          ahead,
40✔
71
          behind,
40✔
72
        };
40✔
73
      }),
24✔
74
    );
24✔
75

76
    return enrichedWorktrees;
24✔
77
  }
29✔
78

79
  /**
80
   * Get display name for a worktree
81
   * Uses branch name, or directory name for detached HEAD
82
   */
83
  private getWorktreeName(branch: string, path: string): string {
36✔
84
    if (branch && branch !== '(detached)') {
40✔
85
      return branch;
40✔
86
    }
40!
87

88
    // For detached HEAD, use the directory name
NEW
89
    const parts = path.split('/');
×
NEW
90
    return parts[parts.length - 1] ?? 'detached';
×
91
  }
40✔
92

93
  /**
94
   * Get the main branch name for a project
95
   * Returns 'main' by default if project not found or not a git repo
96
   */
97
  async getMainBranch(projectId: string): Promise<string> {
36✔
98
    const project = await this.projectStore.getById(projectId);
5✔
99
    if (!project) {
5✔
100
      return 'main';
1✔
101
    }
1✔
102

103
    const gitService = new GitService(project.path);
4✔
104
    return gitService.getMainBranch();
4✔
105
  }
5✔
106

107
  /**
108
   * Create a new worktree for a project
109
   * @param projectId - The project ID
110
   * @param name - The branch name for the new worktree
111
   * @param baseBranch - Optional base branch to create from
112
   * @returns The created worktree with status information
113
   */
114
  async createWorktree(
36✔
115
    projectId: string,
22✔
116
    name: string,
22✔
117
    baseBranch?: string,
22✔
118
  ): Promise<WorktreeWithStatus> {
22✔
119
    const project = await this.projectStore.getById(projectId);
22✔
120
    if (!project) {
22✔
121
      throw new Error('Project not found');
2✔
122
    }
2✔
123

124
    const gitService = new GitService(project.path);
20✔
125

126
    // Verify it's a git repo with commits
127
    if (!(await gitService.hasCommits())) {
22✔
128
      throw new Error('Project is not a git repository with commits');
2✔
129
    }
2✔
130

131
    // Create the worktree path in .worktrees directory next to the project
132
    const worktreePath = join(project.path, '.worktrees', name);
18✔
133

134
    // Create the worktree
135
    await gitService.createWorktree(worktreePath, name, baseBranch);
18✔
136

137
    // Get the updated worktree list to return the new one with full info
138
    const worktrees = await this.getWorktrees(projectId);
14✔
139
    const newWorktree = worktrees.find((wt) => wt.path === worktreePath);
14✔
140

141
    if (!newWorktree) {
22!
NEW
142
      throw new Error('Worktree was created but could not be found');
×
NEW
143
    }
✔
144

145
    return newWorktree;
14✔
146
  }
22✔
147

148
  /**
149
   * Delete a worktree for a project
150
   * @param projectId - The project ID
151
   * @param worktreePath - The path of the worktree to delete
152
   * @param force - Force removal even if worktree has uncommitted changes
153
   * @returns true if worktree was deleted successfully
154
   */
155
  async deleteWorktree(projectId: string, worktreePath: string, force?: boolean): Promise<boolean> {
36✔
156
    const project = await this.projectStore.getById(projectId);
8✔
157
    if (!project) {
8✔
158
      throw new Error('Project not found');
1✔
159
    }
1✔
160

161
    const gitService = new GitService(project.path);
7✔
162
    await gitService.removeWorktree(worktreePath, force);
7✔
163

164
    return true;
4✔
165
  }
8✔
166
}
36✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc