Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions src/api/v2/git.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Debug from 'debug'
import { Response } from 'express'
import { lockApi } from 'src/middleware'
import { OpenApiRequestExt } from 'src/otomi-models'
import { GitConfig, OpenApiRequestExt } from 'src/otomi-models'

const debug = Debug('otomi:api:v2:git')

Expand All @@ -16,18 +16,12 @@ const debug = Debug('otomi:api:v2:git')
*/
export const migrateGit = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
debug('migrateGit')
const { repoUrl, username, password, email, branch } = req.body as {
repoUrl: string
username?: string
password: string
email: string
branch: string
}
const newGitConfig = req.body as GitConfig

// Validate new remote connectivity; returns true if remote already has content
let remoteHasContent: boolean
try {
remoteHasContent = await req.otomi.git.testRemoteConnection(repoUrl, password, branch, username)
remoteHasContent = await req.otomi.git.testRemoteConnection(newGitConfig)
} catch (e: any) {
if (e.message.includes('not found')) {
const error = { message: `Cannot connect to new git remote`, statusCode: 404 }
Expand All @@ -39,9 +33,12 @@ export const migrateGit = async (req: OpenApiRequestExt, res: Response): Promise
return
}
}
if (remoteHasContent) {
res.json({ message: 'New repository is not empty', statusCode: 400 })
}

// Write config + commit locally → push to new remote (if empty) → push to current remote
await req.otomi.migrateGitSettings({ repoUrl, username, password, email, branch, remoteHasContent })
await req.otomi.migrateGitSettings(newGitConfig)

await lockApi()

Expand Down
3 changes: 2 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ type OtomiSpec = {
// get the latest commit from Git and checks it against the local values
const checkAgainstGit = async () => {
const otomiStack = await getSessionStack()
const latestOtomiVersion = await getLatestRemoteCommitSha()
await otomiStack.refreshGitConfig()
const latestOtomiVersion = await getLatestRemoteCommitSha(otomiStack.git)
// check the local version against the latest online version
// if the latest online is newer it will be pulled locally
if (latestOtomiVersion && latestOtomiVersion !== otomiStack.git.commitSha) {
Expand Down
9 changes: 9 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,12 @@ export const APL_SECRETS_NAMESPACE = 'apl-secrets'
export const APL_USERS_NAMESPACE = 'apl-users'
export const PLATFORM_SECRETS_NAME = 'otomi-secrets'
export const GITEA_SECRETS_NAME = 'gitea-secrets'
export const GIT_CONFIG_SECRET_NAME = 'apl-git-config'
export const GIT_DEFAULT_CONFIG = {
repoUrl: 'http://git-server.git-server.svc.cluster.local/otomi/values.git',
branch: 'main',
email: 'pipeline@cluster.local',
}
export const GIT_LEGACY_CONFIG = {
repoUrl: 'http://gitea-http.gitea.svc.cluster.local:3000/otomi/values.git',
}
90 changes: 79 additions & 11 deletions src/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const mockGitInstance = {
;(simpleGit as jest.Mock).mockReturnValue(mockGitInstance)

function makeRepo(): Git {
return new Git('/tmp/test', 'https://origin.example.com/repo.git', 'user', 'user@example.com', undefined, 'main')
return new Git('/tmp/test', 'https://origin.example.com/repo.git', 'user', 'user@example.com', '', 'main')
}

describe('Git.testRemoteConnection', () => {
Expand All @@ -26,38 +26,72 @@ describe('Git.testRemoteConnection', () => {
it('returns false when remote is empty (no refs)', async () => {
mockRaw.mockResolvedValue('')
const repo = makeRepo()
const result = await repo.testRemoteConnection('https://example.com/repo.git', 'mypass', 'main', 'myuser')
const gitConfig = {
repoUrl: 'https://example.com/repo.git',
password: 'mypass',
branch: 'main',
username: 'myuser',
email: '',
}
const result = await repo.testRemoteConnection(gitConfig)
expect(result).toBe(false)
expect(mockRaw).toHaveBeenCalledWith(['ls-remote', expect.stringContaining('myuser'), 'refs/heads/main'])
})

it('returns true when remote has existing refs', async () => {
mockRaw.mockResolvedValue('abc123\trefs/heads/main\n')
const repo = makeRepo()
const result = await repo.testRemoteConnection('https://example.com/repo.git', 'mypass', 'main', 'myuser')
const gitConfig = {
repoUrl: 'https://example.com/repo.git',
password: 'mypass',
branch: 'main',
username: 'myuser',
email: '',
}
const result = await repo.testRemoteConnection(gitConfig)
expect(result).toBe(true)
})

it('calls ls-remote with PAT only (no username) in url', async () => {
mockRaw.mockResolvedValue('abc123\trefs/heads/main\n')
const repo = makeRepo()
const result = await repo.testRemoteConnection('https://example.com/repo.git', 'mytoken', 'main')
const gitConfig = {
repoUrl: 'https://example.com/repo.git',
password: 'mytoken',
branch: 'main',
email: '',
}
const result = await repo.testRemoteConnection(gitConfig)
expect(result).toBe(true)
expect(mockRaw).toHaveBeenCalledWith(['ls-remote', 'https://mytoken@example.com/repo.git', 'refs/heads/main'])
})

it('returns false when branch does not exist but remote has other refs', async () => {
mockRaw.mockResolvedValue('')
const repo = makeRepo()
const result = await repo.testRemoteConnection('https://example.com/repo.git', 'mypass', 'feature-branch', 'myuser')
const gitConfig = {
repoUrl: 'https://example.com/repo.git',
password: 'mytoken',
username: 'myuser',
branch: 'feature-branch',
email: '',
}
const result = await repo.testRemoteConnection(gitConfig)
expect(result).toBe(false)
expect(mockRaw).toHaveBeenCalledWith(['ls-remote', expect.stringContaining('myuser'), 'refs/heads/feature-branch'])
})

it('throws when ls-remote fails (unreachable remote)', async () => {
mockRaw.mockRejectedValue(new Error('exit code 128'))
const repo = makeRepo()
await expect(repo.testRemoteConnection('https://bad.example.com/repo.git', 'p', 'main', 'u')).rejects.toThrow()
const gitConfig = {
repoUrl: 'https://bad.example.com/repo.git',
password: 'p',
username: 'u',
branch: 'main',
email: '',
}
await expect(repo.testRemoteConnection(gitConfig)).rejects.toThrow()
})
})

Expand All @@ -69,7 +103,14 @@ describe('Git.pushToNewRemote', () => {
mockRemote.mockResolvedValue('')
mockPush.mockResolvedValue({})
const repo = makeRepo()
await repo.pushToNewRemote('https://example.com/repo.git', 'main', 'p', 'u')
const gitConfig = {
repoUrl: 'https://example.com/repo.git',
branch: 'main',
password: 'p',
username: 'u',
email: '',
}
await repo.pushToNewRemote(gitConfig)

expect(mockFetch).toHaveBeenCalledWith(['origin', '--unshallow'])
expect(mockRemote).toHaveBeenCalledWith(
Expand All @@ -84,7 +125,14 @@ describe('Git.pushToNewRemote', () => {
mockRemote.mockResolvedValue('')
mockPush.mockResolvedValue({})
const repo = makeRepo()
await repo.pushToNewRemote('https://example.com/repo.git', 'main', 'p', 'u')
const gitConfig = {
repoUrl: 'https://example.com/repo.git',
branch: 'main',
password: 'p',
username: 'u',
email: '',
}
await repo.pushToNewRemote(gitConfig)

expect(mockPush).toHaveBeenCalledWith('migration-remote', 'HEAD:refs/heads/main')
})
Expand All @@ -94,7 +142,13 @@ describe('Git.pushToNewRemote', () => {
mockRemote.mockResolvedValue('')
mockPush.mockResolvedValue({})
const repo = makeRepo()
await repo.pushToNewRemote('https://example.com/repo.git', 'main', 'mytoken')
const gitConfig = {
repoUrl: 'https://example.com/repo.git',
branch: 'main',
password: 'mytoken',
email: '',
}
await repo.pushToNewRemote(gitConfig)

expect(mockRemote).toHaveBeenCalledWith(
expect.arrayContaining(['add', 'migration-remote', 'https://mytoken@example.com/repo.git']),
Expand All @@ -106,7 +160,14 @@ describe('Git.pushToNewRemote', () => {
mockRemote.mockResolvedValue('')
mockPush.mockResolvedValue({})
const repo = makeRepo() // local branch is 'main'
await repo.pushToNewRemote('https://example.com/repo.git', 'feature-branch', 'p', 'u')
const gitConfig = {
repoUrl: 'https://example.com/repo.git',
branch: 'feature-branch',
username: 'u',
password: 'p',
email: '',
}
await repo.pushToNewRemote(gitConfig)

expect(mockPush).toHaveBeenCalledWith('migration-remote', 'HEAD:refs/heads/feature-branch')
})
Expand All @@ -116,7 +177,14 @@ describe('Git.pushToNewRemote', () => {
mockRemote.mockResolvedValue('')
mockPush.mockRejectedValue(new Error('push failed'))
const repo = makeRepo()
await expect(repo.pushToNewRemote('https://example.com/repo.git', 'main', 'p', 'u')).rejects.toThrow('push failed')
const gitConfig = {
repoUrl: 'https://example.com/repo.git',
branch: 'main',
username: 'u',
password: 'p',
email: '',
}
await expect(repo.pushToNewRemote(gitConfig)).rejects.toThrow('push failed')
expect(mockRemote).toHaveBeenCalledWith(['remove', 'migration-remote'])
})
})
Loading
Loading