Last active
February 16, 2026 14:17
-
-
Save mshivam019/11d618bb850ea7984189a35c978704bd to your computer and use it in GitHub Desktop.
This is used to migrate an org from bit bucket to github
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import fs from "fs"; | |
| import path from "path"; | |
| import { spawnSync } from "child_process"; | |
| /* ================= ENV ================= */ | |
| const BB_WORKSPACE = ""; | |
| const BB_TOKEN = ""; | |
| const BB_USERNAME = ""; | |
| const BB_EMAIL = ""; | |
| const GH_ORG = ""; | |
| const GH_TOKEN = ""; | |
| if (!BB_WORKSPACE || !BB_EMAIL || !BB_USERNAME || !BB_TOKEN || !GH_ORG || !GH_TOKEN) { | |
| console.error("β Missing environment variables"); | |
| process.exit(1); | |
| } | |
| /* ================= PATHS ================= */ | |
| const ROOT = process.cwd(); | |
| const WORKDIR = path.join(ROOT, "migration"); | |
| const PROGRESS_FILE = path.join(ROOT, "progress.json"); | |
| const LARGE_FILE_LIMIT = 100 * 1024 * 1024; | |
| if (!fs.existsSync(WORKDIR)) fs.mkdirSync(WORKDIR); | |
| /* ================= PROGRESS ================= */ | |
| function loadProgress() { | |
| if (!fs.existsSync(PROGRESS_FILE)) { | |
| const init = { completed: [] }; | |
| fs.writeFileSync(PROGRESS_FILE, JSON.stringify(init, null, 2)); | |
| return init; | |
| } | |
| return JSON.parse(fs.readFileSync(PROGRESS_FILE, "utf8")); | |
| } | |
| function saveProgress(p) { | |
| fs.writeFileSync(PROGRESS_FILE, JSON.stringify(p, null, 2)); | |
| } | |
| /* ================= EXEC ================= */ | |
| function run(cmd, cwd) { | |
| const res = spawnSync(cmd, { | |
| cwd, | |
| shell: true, | |
| stdio: "inherit", | |
| }); | |
| if (res.status !== 0) { | |
| throw new Error(`Command failed: ${cmd}`); | |
| } | |
| } | |
| /* ================= FETCH HELPERS ================= */ | |
| function bbFetch(url, options = {}) { | |
| return fetch(`https://api.bitbucket.org/2.0${url}`, { | |
| ...options, | |
| headers: { | |
| Authorization: | |
| "Basic " + | |
| Buffer.from(`${BB_EMAIL}:${BB_TOKEN}`).toString("base64"), | |
| "Content-Type": "application/json", | |
| ...(options.headers || {}), | |
| }, | |
| }); | |
| } | |
| function ghFetch(url, options = {}) { | |
| return fetch(`https://api.github.com${url}`, { | |
| ...options, | |
| headers: { | |
| Authorization: `Bearer ${GH_TOKEN}`, | |
| Accept: "application/vnd.github+json", | |
| "Content-Type": "application/json", | |
| ...(options.headers || {}), | |
| }, | |
| }); | |
| } | |
| /* ================= BITBUCKET ================= */ | |
| async function getAllBitbucketRepos() { | |
| let repos = []; | |
| let url = `/repositories/${BB_WORKSPACE}?pagelen=100`; | |
| while (url) { | |
| const r = await bbFetch(url); | |
| const data = await r.json(); | |
| repos.push(...data.values); | |
| url = data.next | |
| ? data.next.replace("https://api.bitbucket.org/2.0", "") | |
| : null; | |
| } | |
| return repos; | |
| } | |
| async function getBitbucketDefaultBranch(repo) { | |
| const r = await bbFetch(`/repositories/${BB_WORKSPACE}/${repo}`); | |
| const data = await r.json(); | |
| return data.mainbranch?.name; | |
| } | |
| /* ================= GITHUB ================= */ | |
| async function ensureGithubRepo(name) { | |
| const res = await ghFetch(`/repos/${GH_ORG}/${name}`); | |
| if (res.status === 404) { | |
| console.log(`π Creating GitHub repo: ${name}`); | |
| await ghFetch(`/orgs/${GH_ORG}/repos`, { | |
| method: "POST", | |
| body: JSON.stringify({ name, private: true }), | |
| }); | |
| return; | |
| } | |
| if (!res.ok) { | |
| throw new Error(`GitHub repo check failed for ${name}`); | |
| } | |
| } | |
| async function githubBranchExists(repo, branch) { | |
| const r = await ghFetch(`/repos/${GH_ORG}/${repo}/branches/${branch}`); | |
| return r.status === 200; | |
| } | |
| async function getGithubDefaultBranch(repo) { | |
| const r = await ghFetch(`/repos/${GH_ORG}/${repo}`); | |
| const data = await r.json(); | |
| return data.default_branch; | |
| } | |
| async function setGithubDefaultBranch(repo, branch) { | |
| await ghFetch(`/repos/${GH_ORG}/${repo}`, { | |
| method: "PATCH", | |
| body: JSON.stringify({ default_branch: branch }), | |
| }); | |
| } | |
| /* ================= AUTO LFS ================= */ | |
| function setupLFS(repoDir) { | |
| const output = spawnSync( | |
| "git rev-list --objects --all", | |
| { cwd: repoDir, shell: true, encoding: "utf8" } | |
| ).stdout; | |
| if (!output) return; | |
| const largeFiles = output | |
| .split("\n") | |
| .map((l) => l.split(" ")[1]) | |
| .filter(Boolean) | |
| .filter((f) => { | |
| try { | |
| return fs.statSync(path.join(repoDir, f)).size > LARGE_FILE_LIMIT; | |
| } catch { | |
| return false; | |
| } | |
| }); | |
| if (!largeFiles.length) return; | |
| console.log(`π¦ Enabling Git LFS (${largeFiles.length} large files)`); | |
| run("git lfs install", repoDir); | |
| largeFiles.forEach((f) => run(`git lfs track "${f}"`, repoDir)); | |
| run("git add .gitattributes", repoDir); | |
| run(`git commit -m "Track large files with Git LFS"`, repoDir); | |
| } | |
| /* ================= MIGRATE ================= */ | |
| async function migrateRepo(repo, progress) { | |
| const name = repo.slug; | |
| const repoDir = path.join(WORKDIR, `${name}.git`); | |
| const lockFile = path.join(repoDir, "MIGRATED.lock"); | |
| if (progress.completed.includes(name)) { | |
| console.log(`βοΈ Skipping ${name}`); | |
| return; | |
| } | |
| console.log(`\nπ Migrating ${name}`); | |
| const bbDefaultBranch = await getBitbucketDefaultBranch(name); | |
| if (!fs.existsSync(repoDir)) { | |
| run( | |
| `git clone --mirror https://${BB_USERNAME}:${BB_TOKEN}@bitbucket.org/${BB_WORKSPACE}/${name}.git`, | |
| WORKDIR | |
| ); | |
| } | |
| if (fs.existsSync(lockFile)) { | |
| console.log(`π Already pushed ${name}`); | |
| progress.completed.push(name); | |
| saveProgress(progress); | |
| return; | |
| } | |
| await ensureGithubRepo(name); | |
| setupLFS(repoDir); | |
| run( | |
| `git push --mirror https://${GH_TOKEN}@github.com/${GH_ORG}/${name}.git`, | |
| repoDir | |
| ); | |
| /* ===== FIX DEFAULT BRANCH ===== */ | |
| if (bbDefaultBranch && (await githubBranchExists(name, bbDefaultBranch))) { | |
| const ghDefault = await getGithubDefaultBranch(name); | |
| if (ghDefault !== bbDefaultBranch) { | |
| console.log( | |
| `πΏ Fixing default branch: ${ghDefault} β ${bbDefaultBranch}` | |
| ); | |
| await setGithubDefaultBranch(name, bbDefaultBranch); | |
| } else { | |
| console.log(`β Default branch correct (${bbDefaultBranch})`); | |
| } | |
| } else { | |
| console.log(`β οΈ Could not verify default branch for ${name}`); | |
| } | |
| fs.writeFileSync(lockFile, "done"); | |
| progress.completed.push(name); | |
| saveProgress(progress); | |
| } | |
| /* ================= MAIN ================= */ | |
| (async () => { | |
| try { | |
| const progress = loadProgress(); | |
| const repos = await getAllBitbucketRepos(); | |
| for (const repo of repos) { | |
| await migrateRepo(repo, progress); | |
| } | |
| console.log("\nπ MIGRATION COMPLETED SUCCESSFULLY"); | |
| } catch (e) { | |
| console.error("\nβ Migration stopped due to error"); | |
| console.error(e.message); | |
| process.exit(1); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment