Skip to content

Conversation

@256thFission
Copy link

Problem

On Windows, repo operations failed because the backend invoked Unix commands like test, rm -rf, ls, and bash -c, which produced invalid mixed paths (ie. /workspace/repos/C:\Users\...).

Fix

  • Added the following cross‑platform helpers in backend/src/services/file-operations.ts
    • directoryExists
    • removeDirectory
    • listDirectoryNames
  • Updated [backend/src/services/repo.ts] to use these new operations
  • Remove all bash -c wrappers

Testing

  • Ran backend lint/tests on Windows
  • Built docker and verified fix

Copilot AI review requested due to automatic review settings January 13, 2026 17:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes Windows filesystem interoperability by replacing Unix-specific shell commands (test, rm -rf, ls, bash -c) with cross-platform Node.js filesystem APIs. The changes ensure the backend can operate on Windows without relying on Unix commands that produce invalid mixed paths.

Changes:

  • Added three new cross-platform helper functions in file-operations.ts: directoryExists, removeDirectory, and listDirectoryNames
  • Replaced all Unix shell command invocations in repo.ts with the new helper functions
  • Removed all bash -c wrapper commands

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
backend/src/services/file-operations.ts Added three new helper functions for directory operations using Node.js fs promises API
backend/src/services/repo.ts Replaced Unix shell commands with cross-platform helper functions across all repository operations

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +125 to +133
export async function directoryExists(dirPath: string): Promise<boolean> {
try {
const fullPath = path.isAbsolute(dirPath) ? dirPath : path.join(getReposPath(), dirPath)
const stats = await fs.stat(fullPath)
return stats.isDirectory()
} catch {
return false
}
}
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The directoryExists function's automatic path resolution behavior (prepending getReposPath() for relative paths) is inconsistent with how callers use it. All callers in repo.ts already construct absolute paths using path.join(getReposPath(), ...) before passing them to this function, making the internal path resolution redundant. This could lead to confusion or bugs if a caller passes a relative path expecting different behavior. Consider either: (1) removing the automatic path resolution and requiring absolute paths, or (2) documenting this behavior clearly and updating callers to pass relative paths directly.

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +142
export async function removeDirectory(dirPath: string): Promise<void> {
try {
const fullPath = path.isAbsolute(dirPath) ? dirPath : path.join(getReposPath(), dirPath)
await fs.rm(fullPath, { recursive: true, force: true })
} catch (error) {
throw new Error(`Failed to remove directory ${dirPath}: ${error}`)
}
}
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same path resolution issue as directoryExists. All callers construct absolute paths before calling this function (e.g., removeDirectory(path.join(getReposPath(), dirName))), making the internal path resolution logic redundant and potentially confusing. Consider standardizing the approach across all file operation functions.

Copilot uses AI. Check for mistakes.
}
}
return directories
} catch {
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling silently returns an empty array on any error, which could mask actual problems (e.g., permission issues, invalid paths). This differs from removeDirectory which throws errors. Consider either throwing errors like the other functions, or at minimum logging the error before returning an empty array so failures aren't completely silent.

Suggested change
} catch {
} catch (error) {
logger.error(`Failed to list directory names for ${dirPath}:`, error)

Copilot uses AI. Check for mistakes.
localBranchExists = 'missing'
}
if (localBranchExists.trim() === 'missing') {
if (branch && (error.message.includes('Remote branch') || error.message.includes('not found'))) {
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks if branch exists before entering the error handling block, but branch is already guaranteed to be truthy at this point in the code flow (line 370 condition is branch && baseRepoExists && useWorktree). The redundant check reduces code clarity.

Suggested change
if (branch && (error.message.includes('Remote branch') || error.message.includes('not found'))) {
if (error.message.includes('Remote branch') || error.message.includes('not found')) {

Copilot uses AI. Check for mistakes.
localBranchExists = 'missing'
}

if (localBranchExists === 'missing') {
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes from string-based existence checks ('exists'/'missing') to boolean checks are correct, but the logic at lines 399-411 for handling local branch creation when a remote branch is not found lacks test coverage. This is a critical code path for branch creation fallback that should be tested to ensure the Windows compatibility fix works correctly in this scenario.

Copilot uses AI. Check for mistakes.
if (branch && (error.message.includes('Remote branch') || error.message.includes('not found'))) {
logger.info(`Branch '${branch}' not found, cloning default branch and creating branch locally`)
await executeGitWithFallback(['git', 'clone', normalizedRepoUrl, worktreeDirName], { cwd: getReposPath(), env })
let localBranchExists = 'missing'
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial value of localBranchExists is unused, since it is always overwritten.

Suggested change
let localBranchExists = 'missing'
let localBranchExists: 'exists' | 'missing'

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant