Skip to content

Instantly share code, notes, and snippets.

@fcsonline
Created July 22, 2022 11:24
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fcsonline/6f04319b1d7b99a09eb842f7e14aa063 to your computer and use it in GitHub Desktop.
Save fcsonline/6f04319b1d7b99a09eb842f7e14aa063 to your computer and use it in GitHub Desktop.
Migration script to merge multiple repositories into a single monorepo
#!/bin/bash
# NOTE: To be able to run this migration you need git >= 2.22.0
#
# If you don't have it, install it with:
# sudo add-apt-repository ppa:git-core/ppa
# sudo apt update
# sudo apt install git
set -e
echo 'Installing git-filter-repo'
python3 -m pip install --user git-filter-repo
if [ -z "$GITHUB_TOKEN" ]; then
echo "\$GITHUB_TOKEN is empty"
exit
fi
# NOTE: Add here all repositories.
# Syntax: <name> <subfolder_name> <master_branch>
ITEMS=(
"mobile mobile main"
"frontend frontend master"
"backend backend master"
)
# NOTE: Change here the monorepo repository name.
MONOREPO="monorepo"
for ITEM in "${ITEMS[@]}"; do
REPOSITORY=$(echo "$ITEM" | cut -d' ' -f 1)
FOLDER=$(echo "$ITEM" | cut -d' ' -f 2)
MASTER_BRANCH=$(echo "$ITEM" | cut -d' ' -f 3)
URL=$(echo "git@github.com:factorialco/${REPOSITORY}.git")
TMP="$(mktemp -d -t "${FOLDER##*/}.XXXX")"
echo "🟢 Cloning ${URL} content..."
git clone "$URL" "$TMP"
echo "🟢 Initial cleanup..."
git remote remove "$FOLDER" || true
echo "🟢 Migrating all branches from $URL content into $FOLDER folder..."
pushd "$TMP"
for BRANCH in $(git branch -a | grep "^ *remotes/" | grep -v HEAD | grep -v $MASTER_BRANCH); do
git branch --track ${BRANCH#remotes/origin/} $BRANCH;
done
git filter-repo --refs $(git branch | grep -v '\*' | xargs -0) --force --to-subdirectory $FOLDER/
git filter-repo --refs $MASTER_BRANCH --force --to-subdirectory $FOLDER/
popd
echo "🟢 Linking monorepo with temporal ${FOLDER} folder..."
git remote add "$FOLDER" "$TMP"
git fetch "$FOLDER"
echo "🟢 Bringing back the old branches..."
git branch | grep -v "*" | xargs -r git branch -D
for BRANCH in $(git branch -a | grep "^ *remotes/$FOLDER" | grep -v HEAD | grep -v $MASTER_BRANCH); do
git checkout -b "${BRANCH/remotes\/}" $BRANCH
done
echo "🟢 Going back the main..."
git checkout main
echo "🟢 Merging temporal ${FOLDER} folder into monorepo..."
git merge "$FOLDER/$MASTER_BRANCH" --allow-unrelated-histories --no-ff -m "[Monorepo] Add ${FOLDER}"
git push -f origin main
echo "🟢 Cleaning up..."
rm -rf "$TMP"
git remote rm "$FOLDER"
git gc
git prune
for BRANCH in $(git branch -a | grep "^ *$FOLDER" | grep -v HEAD | grep -v $MASTER_BRANCH); do
echo "🟢 Pushing $BRANCH branch..."
git push -f origin $BRANCH
git branch -D $BRANCH
done
echo "🟢 Creating pull requests for $FOLDER..."
npm ci
REPOSITORY=$REPOSITORY FOLDER=$FOLDER MASTER_BRANCH=$MASTER_BRANCH MONOREPO=$MONOREPO node ./pullrequest.js
echo "🟢 Process completed for $FOLDER repository"
done
echo "💯 Monorepo process completed!"
import { graphql } from '@octokit/graphql'
if (!process.env.GITHUB_TOKEN) {
throw new Error('You have to pass a GITHUB_TOKEN')
}
if (!process.env.REPOSITORY) {
throw new Error('You have to pass a REPOSITORY')
}
if (!process.env.FOLDER) {
throw new Error('You have to pass a FOLDER')
}
if (!process.env.MASTER_BRANCH) {
throw new Error('You have to pass a MASTER_BRANCH')
}
if (!process.env.MONOREPO) {
throw new Error('You have to pass a MONOREPO')
}
const graphqlWithAuth = graphql.defaults({
headers: {
authorization: `token ${process.env.GITHUB_TOKEN}`
}
})
const fetchMonoRepository = async (name) => {
const { repository } = await graphqlWithAuth(`
{
repository(owner: "factorialco", name: "${name}") {
id
}
}
`)
return repository.id
}
const fetchPullRequest = async (repositoryId, after) => {
const { repository } = await graphqlWithAuth(`
query fetchPullRequest ($after: String, $limit: Int!){
repository(owner: "factorialco", name: "${repositoryId}") {
pullRequests(after: $after, first: $limit, states: [OPEN]) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
isDraft
title
body
url
assignees(last: 10) {
edges {
node {
id
}
}
}
author {
login
}
baseRefName
headRefName
}
}
}
}
}
`, {
after,
limit: 5
})
return repository
}
const timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const migrateRepository = async (repositoryId, folder) => {
const createPullRequest = async (element) => {
const { id, title, body, url, isDraft, baseRefName, headRefName, author, assignees } = element.node
const newBaseRefName = baseRefName === process.env.MASTER_BRANCH ? 'main' : `${folder}/${baseRefName}`
const newHeadRefName = `${folder}/${headRefName}`
const newBody = `${body}\n\n### :monkey: Monorepo\nOriginal Pull Request: ${url}`
console.log(`Waiting 1 minute before creating pull request with title '${title}'`)
await timeout(60_000)
console.log(`Creating pull request by ${author.login} with title '${title}'`)
try {
const { createPullRequest } = await graphqlWithAuth(`
mutation createPullRequest($title: String!, $body: String){
createPullRequest (
input: {
repositoryId: "${monorepoId}",
title: $title,
body: $body,
baseRefName: "${newBaseRefName}",
headRefName: "${newHeadRefName}",
draft: ${isDraft},
}
) {
pullRequest {
id
url
}
clientMutationId
}
}
`, {
title,
body: newBody
})
console.log(`Done! New pull request: ${createPullRequest.pullRequest.url} (Mutation: ${createPullRequest.clientMutationId})`)
if (author.login === 'dependabot') {
console.log(`Depenedabot pull request. Skipping asssinees step`)
return
}
const { user } = await graphqlWithAuth(`
{
user(login: "${author.login}") {
id
}
}
`)
const newAssignees = [
...assignees.edges.map(assignee => assignee.node.id),
user.id
]
console.log(`Assigning pull request to ${newAssignees}`)
const { addAssigneesToAssignable } = await graphqlWithAuth(`
mutation addAssignee($newAssignees: [ID!]!) {
addAssigneesToAssignable(input: {
assignableId: "${createPullRequest.pullRequest.id}",
assigneeIds: $newAssignees
}) {
clientMutationId
}
}
`, {
newAssignees: [...new Set(newAssignees)]
})
console.log(`Pull request assigned to ${author.login} (Mutation: ${addAssigneesToAssignable.clientMutationId})`)
} catch (error) {
console.log('ERROR', error)
}
}
const monorepoId = await fetchMonoRepository(process.env.MONOREPO)
let hasNextPage = true
let after = null
do {
console.log(`Fetching a new page of pullRequests (cursor: ${after})`)
const repository = await fetchPullRequest(repositoryId, after)
repository.pullRequests.edges.forEach(createPullRequest)
hasNextPage = repository.pullRequests.pageInfo.hasNextPage
after = repository.pullRequests.pageInfo.endCursor
} while (hasNextPage)
}
migrateRepository(process.env.REPOSITORY, process.env.FOLDER)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment