Skip to content

Instantly share code, notes, and snippets.

@niktekusho
Last active May 19, 2023 12:12
Show Gist options
  • Save niktekusho/780d6161257d5703dd73c7f42d096b74 to your computer and use it in GitHub Desktop.
Save niktekusho/780d6161257d5703dd73c7f42d096b74 to your computer and use it in GitHub Desktop.
Quick Node.js script that allowed me to "re-clone" git repositories in different machines. Usage node read-repos.mjs [root directory] > output-script.sh
'use strict';
// Usage: node read-repos.mjs [root directory] > output-script.sh
import { spawn } from 'child_process'
import { log } from 'console'
import { readdir, readFile, access } from 'fs/promises'
import { EOL } from 'os'
import { sep, join } from 'path'
import { argv, cwd, stdout } from 'process'
function getGitConfig(repoPath) {
return join(repoPath, '.git', 'config')
}
const isPiped = !stdout.isTTY ?? true
async function isGitRepo(path) {
const gitConfigPath = getGitConfig(path)
try {
await access(gitConfigPath)
return true
} catch (_) {
return false
}
}
async function getRemoteUrlForRepo(path) {
const gitConfigPath = getGitConfig(path)
const gitConfigContent = await readFile(gitConfigPath, 'utf8')
const urlLine = gitConfigContent.split(EOL).find(line => line.trimStart().startsWith('url'))
return urlLine?.split('=')[1].trim()
}
const excluded = new Set('node_modules')
async function walk(root, { maxDepth = 3 } = {}) {
const dirsToCheck = [root]
const remoteUrls = []
const warnings = {}
while (dirsToCheck.length > 0) {
const path = dirsToCheck.shift()
const depth = path.replace(root, '').split(sep).length
if (await isGitRepo(path)) {
consoleLog(`Git repo found: ${path}`)
const isClean = await isGitRepositoryClean(path)
if (isClean) {
const remoteUrl = await getRemoteUrlForRepo(path)
if (remoteUrl == null) {
warnings[path] = {
message: 'Repository must be backed up manually'
}
} else {
consoleLog(`Remote url: ${remoteUrl}`)
remoteUrls.push(remoteUrl)
}
} else {
warnings[path] = {
message: 'Repository is not clean (check manually)'
}
}
} else {
if (depth < maxDepth) {
const children = await readdir(path, { withFileTypes: true })
children.filter(d => d.isDirectory())
.filter(d => !excluded.has(d))
.map(d => join(path, d.name))
.forEach(d => dirsToCheck.push(d))
}
}
}
return {
warnings,
remoteUrls,
}
}
function createGitCloneScriptLines({ remoteUrls }) {
return [
'#!/bin/sh',
'',
`echo "Cloning ${remoteUrls.length} repositories in $(pwd)"`,
'',
...remoteUrls.map(remoteUrl => `git clone ${remoteUrl}`)
]
}
async function isGitRepositoryClean(repoPath) {
return new Promise((resolve, reject) => {
const gitStatusProcess = spawn('git', ['status', '--porcelain'], {
cwd: repoPath,
})
const {stdout} = gitStatusProcess
// If data is pumped into stdout, the 'git status --porcelain' command
// found some files that are not committed (aka repository is NOT clean)
stdout.on('data', (_) => resolve(false))
gitStatusProcess.on('error', reject)
// If the process exited, we can resolve the promise with 'true'.
// TODO: check the exit code?
gitStatusProcess.on('exit', (_) => resolve(true))
})
}
function writeGitCloneScriptToStdOut({ remoteUrls }) {
createGitCloneScriptLines({ remoteUrls }).forEach(line => stdout.write(`${line}${EOL}`))
}
function consoleLog(msg) {
// Log ONLY if the script is NOT being piped
if (!isPiped) {
log(msg)
}
}
async function main() {
const root = argv[2] ?? cwd()
consoleLog(`Starts walking from ${root}`)
const result = await walk(root)
if (result.remoteUrls.length === 0) {
consoleLog('No repositories to clone. Skipping script creation.')
} else {
writeGitCloneScriptToStdOut(result)
}
const warningsCount = Object.keys(result.warnings).length
if (warningsCount > 0) {
consoleLog(`${warningsCount} warnings found:`)
for (const repo in result.warnings) {
consoleLog(`${repo} - ${result.warnings[repo].message}`)
}
}
}
main().catch(console.error)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment