@buildappolis/env-manager
Version:
Enterprise-grade environment variable management system with encryption, snapshots, and validation
184 lines (161 loc) • 4.81 kB
text/typescript
import { exec } from 'child_process'
import { promisify } from 'util'
import path from 'path'
import fs from 'fs'
const execAsync = promisify(exec)
export interface GitInfo {
isGitRepo: boolean
branch?: string
commit?: string
commitMessage?: string
author?: string
remoteUrl?: string
isDirty?: boolean
ahead?: number
behind?: number
lastCommitDate?: string
tags?: string[]
}
export class GitUtils {
private projectPath: string
constructor(projectPath?: string) {
this.projectPath = projectPath || process.env.PROJECT_ROOT || process.cwd()
}
async getGitInfo(): Promise<GitInfo> {
const info: GitInfo = { isGitRepo: false }
try {
// Check if it's a git repository
const gitDir = path.join(this.projectPath, '.git')
if (!fs.existsSync(gitDir)) {
return info
}
info.isGitRepo = true
// Get current branch
try {
const { stdout: branch } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: this.projectPath
})
info.branch = branch.trim()
} catch (e) {
info.branch = 'unknown'
}
// Get current commit hash
try {
const { stdout: commit } = await execAsync('git rev-parse --short HEAD', {
cwd: this.projectPath
})
info.commit = commit.trim()
} catch (e) {
info.commit = 'unknown'
}
// Get commit message
try {
const { stdout: message } = await execAsync('git log -1 --pretty=%B', {
cwd: this.projectPath
})
info.commitMessage = message.trim().split('\n')[0] // First line only
} catch (e) {
info.commitMessage = ''
}
// Get author
try {
const { stdout: author } = await execAsync('git log -1 --pretty=%an', {
cwd: this.projectPath
})
info.author = author.trim()
} catch (e) {
info.author = ''
}
// Get last commit date
try {
const { stdout: date } = await execAsync('git log -1 --pretty=%ai', {
cwd: this.projectPath
})
info.lastCommitDate = date.trim()
} catch (e) {
info.lastCommitDate = ''
}
// Get remote URL
try {
const { stdout: remote } = await execAsync('git config --get remote.origin.url', {
cwd: this.projectPath
})
info.remoteUrl = this.sanitizeRemoteUrl(remote.trim())
} catch (e) {
info.remoteUrl = ''
}
// Check if working directory is dirty
try {
const { stdout: status } = await execAsync('git status --porcelain', {
cwd: this.projectPath
})
info.isDirty = status.trim().length > 0
} catch (e) {
info.isDirty = false
}
// Get ahead/behind status
try {
const { stdout: tracking } = await execAsync('git rev-list --left-right --count HEAD...@{u}', {
cwd: this.projectPath
})
const [ahead, behind] = tracking.trim().split('\t').map(n => parseInt(n, 10))
info.ahead = ahead || 0
info.behind = behind || 0
} catch (e) {
// No upstream branch
info.ahead = 0
info.behind = 0
}
// Get tags at current commit
try {
const { stdout: tags } = await execAsync('git tag --points-at HEAD', {
cwd: this.projectPath
})
info.tags = tags.trim().split('\n').filter(t => t.length > 0)
} catch (e) {
info.tags = []
}
} catch (error) {
console.error('Error getting git info:', error)
}
return info
}
async getAllBranches(): Promise<string[]> {
try {
const { stdout } = await execAsync('git branch -a', {
cwd: this.projectPath
})
return stdout
.split('\n')
.map(b => b.trim())
.filter(b => b.length > 0)
.map(b => b.replace(/^\*\s+/, '')) // Remove current branch marker
.map(b => b.replace(/^remotes\/origin\//, '')) // Clean remote branch names
.filter((b, i, arr) => arr.indexOf(b) === i) // Remove duplicates
} catch (error) {
console.error('Error getting branches:', error)
return []
}
}
async switchBranch(branch: string): Promise<boolean> {
try {
await execAsync(`git checkout ${branch}`, {
cwd: this.projectPath
})
return true
} catch (error) {
console.error('Error switching branch:', error)
return false
}
}
private sanitizeRemoteUrl(url: string): string {
// Remove credentials from URLs
if (url.includes('@')) {
return url.replace(/^.*@/, 'https://')
}
return url
}
}
export function getGitUtils(projectPath?: string): GitUtils {
return new GitUtils(projectPath)
}