Skip to content

Commit

Permalink
added syncingore
Browse files Browse the repository at this point in the history
  • Loading branch information
RinatS committed Oct 4, 2024
1 parent f408e47 commit 78b04b3
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 80 deletions.
4 changes: 4 additions & 0 deletions .syncignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# .syncignore
node_modules/
dist/
*.log
137 changes: 57 additions & 80 deletions src/server/git/controller.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/require-await */

import simpleGit, { SimpleGitOptions } from 'simple-git'
import { getConfig } from '../../bot/config'
import { getAuthenticatedOctokit } from '../../bot/octokit'
import { generateAuthUrl } from '../../utils/auth'
import { temporaryDirectory } from '../../utils/dir'
import { logger } from '../../utils/logger'
import { SyncReposSchema } from './schema'
import fs from 'fs'
import path from 'path'

const gitApiLogger = logger.getSubLogger({ name: 'git-api' })

export const fetchExclusionConfig = async (
repoPath: string,
): Promise<string[]> => {
const configPath = path.join(repoPath, '.syncignore')
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf-8')
return content.split('\n').filter((line) => line.trim() !== '')
}
return []
}

// Syncs the fork and mirror repos
export const syncReposHandler = async ({
input,
Expand All @@ -18,99 +33,61 @@ export const syncReposHandler = async ({
gitApiLogger.info('Syncing repos', { ...input, accessToken: 'none' })

const config = await getConfig(input.orgId)

gitApiLogger.debug('Fetched config', config)

const { publicOrg, privateOrg } = config

const octokitData = await getAuthenticatedOctokit(publicOrg, privateOrg)
const contributionOctokit = octokitData.contribution.octokit
const contributionAccessToken = octokitData.contribution.accessToken

const privateOctokit = octokitData.private.octokit
const privateInstallationId = octokitData.private.installationId
const privateAccessToken = octokitData.private.accessToken

const forkRepo = await contributionOctokit.rest.repos.get({
owner: input.forkOwner,
repo: input.forkName,
})

const mirrorRepo = await privateOctokit.rest.repos.get({
owner: input.mirrorOwner,
repo: input.mirrorName,
})
const gitOptions: Partial<SimpleGitOptions> = {
baseDir: temporaryDirectory(),
binary: 'git',
maxConcurrentProcesses: 6,
}
const git = simpleGit(gitOptions)

gitApiLogger.debug('Fetched both fork and mirror repos')
const forkRepoPath = path.join(temporaryDirectory(), input.forkName)
const mirrorRepoPath = path.join(temporaryDirectory(), input.mirrorName)

const forkRemote = generateAuthUrl(
contributionAccessToken,
forkRepo.data.owner.login,
forkRepo.data.name,
// Clone the fork and mirror repositories
await git.clone(
`https://github.com/${input.forkOwner}/${input.forkName}.git`,
forkRepoPath,
)

const mirrorRemote = generateAuthUrl(
privateAccessToken,
mirrorRepo.data.owner.login,
mirrorRepo.data.name,
await git.clone(
`https://github.com/${input.mirrorOwner}/${input.mirrorName}.git`,
mirrorRepoPath,
)

// First clone the fork and mirror repos into the same folder
const tempDir = temporaryDirectory()
// Fetch exclusion configuration
const exclusionPaths = await fetchExclusionConfig(mirrorRepoPath)

const options: Partial<SimpleGitOptions> = {
config: [
`user.name=pma[bot]`,
`user.email=${privateInstallationId}+pma[bot]@users.noreply.github.com`,
// Disable any global git hooks to prevent potential interference when running the app locally
'core.hooksPath=/dev/null',
],
}

const git = simpleGit(tempDir, options)
await git.init()
await git.addRemote('fork', forkRemote)
await git.addRemote('mirror', mirrorRemote)
await git.fetch(['fork'])
await git.fetch(['mirror'])

// Check if the branch exists on both repos
const forkBranches = await git.branch(['--list', 'fork/*'])
const mirrorBranches = await git.branch(['--list', 'mirror/*'])

gitApiLogger.debug('branches', {
forkBranches: forkBranches.all,
mirrorBranches: mirrorBranches.all,
})

if (input.destinationTo === 'fork') {
await git.checkoutBranch(
input.forkBranchName,
`fork/${input.forkBranchName}`,
)
gitApiLogger.debug('Checked out branch', input.forkBranchName)
await git.mergeFromTo(
`mirror/${input.mirrorBranchName}`,
input.forkBranchName,
)
gitApiLogger.debug('Merged branches')
gitApiLogger.debug('git status', await git.status())
await git.push('fork', input.forkBranchName)
} else {
await git.checkoutBranch(
input.mirrorBranchName,
`mirror/${input.mirrorBranchName}`,
)
gitApiLogger.debug('Checked out branch', input.mirrorBranchName)
await git.mergeFromTo(
`fork/${input.forkBranchName}`,
// Checkout the mirror branch
await git
.cwd(mirrorRepoPath)
.checkoutBranch(
input.mirrorBranchName,
`origin/${input.mirrorBranchName}`,
)
gitApiLogger.debug('Merged branches')
gitApiLogger.debug('git status', await git.status())
await git.push('mirror', input.mirrorBranchName)
gitApiLogger.debug('Checked out branch', input.mirrorBranchName)

// Apply exclusion paths logic
for (const exclusionPath of exclusionPaths) {
try {
await git.cwd(mirrorRepoPath).rm(exclusionPath)
} catch (error) {
gitApiLogger.warn(`Path not found: ${exclusionPath}`, { error })
}
}

// Merge fork branch into mirror branch
await git
.cwd(mirrorRepoPath)
.mergeFromTo(`origin/${input.forkBranchName}`, input.mirrorBranchName)
gitApiLogger.debug('Merged branches')
gitApiLogger.debug('git status', await git.cwd(mirrorRepoPath).status())

// Push changes to the mirror repository
await git.cwd(mirrorRepoPath).push('origin', input.mirrorBranchName)

return {
success: true,
}
Expand Down
49 changes: 49 additions & 0 deletions test/server/git.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { fetchExclusionConfig } from '../../src/server/git/controller'
import fs from 'fs'
import path from 'path'

jest.mock('fs')
jest.mock('path')

describe('fetchExclusionConfig', () => {
const repoPath = '/fake/repo/path'
const configPath = '/fake/repo/path/.syncignore'

beforeEach(() => {
jest.resetAllMocks()
})

it('should return exclusion paths when .syncignore file exists', async () => {
const mockContent = 'path/to/exclude1\npath/to/exclude2\n'
jest.spyOn(fs, 'existsSync').mockReturnValue(true)
jest.spyOn(fs, 'readFileSync').mockReturnValue(mockContent)
jest.spyOn(path, 'join').mockReturnValue(configPath)

const result = await fetchExclusionConfig(repoPath)
expect(result).toEqual(['path/to/exclude1', 'path/to/exclude2'])
expect(fs.existsSync).toHaveBeenCalledWith(configPath)
expect(fs.readFileSync).toHaveBeenCalledWith(configPath, 'utf-8')
})

it('should return an empty array when .syncignore file does not exist', async () => {
jest.spyOn(fs, 'existsSync').mockReturnValue(false)
jest.spyOn(path, 'join').mockReturnValue(configPath)

const result = await fetchExclusionConfig(repoPath)
expect(result).toEqual([])
expect(fs.existsSync).toHaveBeenCalledWith(configPath)
expect(fs.readFileSync).not.toHaveBeenCalled()
})

it('should return an empty array when .syncignore file is empty', async () => {
const mockContent = ''
jest.spyOn(fs, 'existsSync').mockReturnValue(true)
jest.spyOn(fs, 'readFileSync').mockReturnValue(mockContent)
jest.spyOn(path, 'join').mockReturnValue(configPath)

const result = await fetchExclusionConfig(repoPath)
expect(result).toEqual([])
expect(fs.existsSync).toHaveBeenCalledWith(configPath)
expect(fs.readFileSync).toHaveBeenCalledWith(configPath, 'utf-8')
})
})

0 comments on commit 78b04b3

Please sign in to comment.