add support for including/excluding specific organizations and preserving organization structure during mirroring

This commit is contained in:
Arunavo Ray
2025-04-02 14:03:22 +05:30
parent 936246b9a7
commit 35e1b7e655
4 changed files with 162 additions and 17 deletions

View File

@@ -25,6 +25,8 @@ Additionally, you can now mirror:
- Issues from GitHub repositories (including labels)
- Starred repositories from your GitHub account
- Repositories from organizations you belong to
- Filter which organizations to include or exclude
- Maintain original organization structure in Gitea
- A single repository instead of all repositories
- Repositories to a specific Gitea organization
@@ -51,6 +53,9 @@ All configuration is performed through environment variables. Flags are consider
| MIRROR_ISSUES | no | bool | FALSE | If set to `true` the issues of your GitHub repositories will be mirrored to Gitea. Requires `GITHUB_TOKEN`. |
| MIRROR_STARRED | no | bool | FALSE | If set to `true` repositories you've starred on GitHub will be mirrored to Gitea. Requires `GITHUB_TOKEN`. |
| MIRROR_ORGANIZATIONS | no | bool | FALSE | If set to `true` repositories from organizations you belong to will be mirrored to Gitea. Requires `GITHUB_TOKEN`. |
| INCLUDE_ORGS | no | string | "" | Comma-separated list of GitHub organization names to include when mirroring organizations. If not specified, all organizations will be included. |
| EXCLUDE_ORGS | no | string | "" | Comma-separated list of GitHub organization names to exclude when mirroring organizations. Takes precedence over `INCLUDE_ORGS`. |
| PRESERVE_ORG_STRUCTURE | no | bool | FALSE | If set to `true`, each GitHub organization will be mirrored to a Gitea organization with the same name. If the organization doesn't exist, it will be created. |
| SINGLE_REPO | no | string | - | URL of a single GitHub repository to mirror (e.g., https://github.com/username/repo or username/repo). When specified, only this repository will be mirrored. Requires `GITHUB_TOKEN`. |
| GITEA_ORGANIZATION | no | string | - | Name of a Gitea organization to mirror repositories to. If doesn't exist, will be created. |
| GITEA_ORG_VISIBILITY | no | string | public | Visibility of the Gitea organization to create. Can be "public" or "private". |
@@ -77,6 +82,37 @@ docker container run \
jaedle/mirror-to-gitea:latest
```
### Mirror Only Specific Organizations
```sh
docker container run \
-d \
--restart always \
-e GITHUB_USERNAME=github-user \
-e GITEA_URL=https://your-gitea.url \
-e GITEA_TOKEN=please-exchange-with-token \
-e GITHUB_TOKEN=your-github-token \
-e MIRROR_ORGANIZATIONS=true \
-e INCLUDE_ORGS=org1,org2,org3 \
jaedle/mirror-to-gitea:latest
```
### Mirror Organizations with Preserved Structure
```sh
docker container run \
-d \
--restart always \
-e GITHUB_USERNAME=github-user \
-e GITEA_URL=https://your-gitea.url \
-e GITEA_TOKEN=please-exchange-with-token \
-e GITHUB_TOKEN=your-github-token \
-e MIRROR_ORGANIZATIONS=true \
-e PRESERVE_ORG_STRUCTURE=true \
-e GITEA_ORG_VISIBILITY=private \
jaedle/mirror-to-gitea:latest
```
### Mirror a Single Repository
```sh
@@ -123,6 +159,11 @@ services:
- MIRROR_ISSUES=true
- MIRROR_STARRED=true
- MIRROR_ORGANIZATIONS=true
# Organization specific options
# - INCLUDE_ORGS=org1,org2
# - EXCLUDE_ORGS=org3,org4
# - PRESERVE_ORG_STRUCTURE=true
# Other options
# - SINGLE_REPO=https://github.com/organization/repository
# - GITEA_ORGANIZATION=my-organization
# - GITEA_ORG_VISIBILITY=public
@@ -154,6 +195,9 @@ export GITEA_TOKEN='...'
export MIRROR_ISSUES='true'
export MIRROR_STARRED='true'
export MIRROR_ORGANIZATIONS='true'
# export INCLUDE_ORGS='org1,org2'
# export EXCLUDE_ORGS='org3,org4'
# export PRESERVE_ORG_STRUCTURE='true'
# export SINGLE_REPO='https://github.com/user/repo'
# export GITEA_ORGANIZATION='my-organization'
# export GITEA_ORG_VISIBILITY='public'

View File

@@ -39,6 +39,15 @@ export function configuration() {
mirrorStarred: readBoolean("MIRROR_STARRED"),
mirrorOrganizations: readBoolean("MIRROR_ORGANIZATIONS"),
singleRepo: readEnv("SINGLE_REPO"),
includeOrgs: (readEnv("INCLUDE_ORGS") || "")
.split(",")
.map((o) => o.trim())
.filter((o) => o.length > 0),
excludeOrgs: (readEnv("EXCLUDE_ORGS") || "")
.split(",")
.map((o) => o.trim())
.filter((o) => o.length > 0),
preserveOrgStructure: readBoolean("PRESERVE_ORG_STRUCTURE"),
},
gitea: {
url: mustReadEnv("GITEA_URL"),

View File

@@ -24,7 +24,12 @@ async function getRepositories(octokit, mirrorOptions) {
// Fetch organization repos if the option is enabled
const orgRepos = mirrorOptions.mirrorOrganizations
? await fetchOrganizationRepositories(octokit)
? await fetchOrganizationRepositories(
octokit,
mirrorOptions.includeOrgs,
mirrorOptions.excludeOrgs,
mirrorOptions.preserveOrgStructure
)
: [];
// Combine all repositories and filter duplicates
@@ -99,20 +104,49 @@ async function fetchStarredRepositories(octokit) {
.then(toRepositoryList);
}
async function fetchOrganizationRepositories(octokit) {
async function fetchOrganizationRepositories(octokit, includeOrgs = [], excludeOrgs = [], preserveOrgStructure = false) {
try {
// First get all organizations the user belongs to
const orgs = await octokit.paginate("GET /user/orgs");
const allOrgs = await octokit.paginate("GET /user/orgs");
// Filter organizations based on include/exclude lists
let orgsToProcess = allOrgs;
if (includeOrgs.length > 0) {
// Only include specific organizations
orgsToProcess = orgsToProcess.filter(org =>
includeOrgs.includes(org.login)
);
}
if (excludeOrgs.length > 0) {
// Exclude specific organizations
orgsToProcess = orgsToProcess.filter(org =>
!excludeOrgs.includes(org.login)
);
}
console.log(`Processing repositories from ${orgsToProcess.length} organizations`);
// Then fetch repositories for each organization
const orgRepoPromises = orgs.map(org =>
const orgRepoPromises = orgsToProcess.map(org =>
octokit.paginate("GET /orgs/{org}/repos", { org: org.login })
.then(repos => {
// Add organization context to each repository if preserveOrgStructure is enabled
if (preserveOrgStructure) {
return repos.map(repo => ({
...repo,
organization: org.login
}));
}
return repos;
})
);
// Wait for all requests to complete and flatten the results
const orgRepos = await Promise.all(orgRepoPromises)
.then(repoArrays => repoArrays.flat())
.then(toRepositoryList);
.then(repos => toRepositoryList(repos, preserveOrgStructure));
return orgRepos;
} catch (error) {
@@ -139,9 +173,9 @@ function filterDuplicates(repositories) {
return unique;
}
function toRepositoryList(repositories) {
function toRepositoryList(repositories, preserveOrgStructure = false) {
return repositories.map((repository) => {
return {
const repoInfo = {
name: repository.name,
url: repository.clone_url,
private: repository.private,
@@ -150,6 +184,13 @@ function toRepositoryList(repositories) {
full_name: repository.full_name,
has_issues: repository.has_issues,
};
// Add organization context if it exists and preserveOrgStructure is enabled
if (preserveOrgStructure && repository.organization) {
repoInfo.organization = repository.organization;
}
return repoInfo;
});
}

View File

@@ -43,6 +43,9 @@ async function main() {
mirrorStarred: config.github.mirrorStarred,
mirrorOrganizations: config.github.mirrorOrganizations,
singleRepo: config.github.singleRepo,
includeOrgs: config.github.includeOrgs,
excludeOrgs: config.github.excludeOrgs,
preserveOrgStructure: config.github.preserveOrgStructure,
});
// Apply include/exclude filters
@@ -59,21 +62,69 @@ async function main() {
token: config.gitea.token,
};
// Get Gitea user or organization ID
const giteaTarget = config.gitea.organization
? await getGiteaOrganization(gitea, config.gitea.organization)
: await getGiteaUser(gitea);
if (!giteaTarget) {
console.error("Failed to get Gitea user or organization. Exiting.");
// Get Gitea user information
const giteaUser = await getGiteaUser(gitea);
if (!giteaUser) {
console.error("Failed to get Gitea user. Exiting.");
process.exit(1);
}
// Create a map to store organization targets if preserving structure
const orgTargets = new Map();
if (config.github.preserveOrgStructure) {
// Get unique organization names from repositories
const uniqueOrgs = new Set(
filteredRepositories
.filter(repo => repo.organization)
.map(repo => repo.organization)
);
// Create or get each organization in Gitea
for (const orgName of uniqueOrgs) {
console.log(`Preparing Gitea organization for GitHub organization: ${orgName}`);
// Create the organization if it doesn't exist
await createGiteaOrganization(
gitea,
orgName,
config.gitea.visibility,
config.dryRun
);
// Get the organization details
const orgTarget = await getGiteaOrganization(gitea, orgName);
if (orgTarget) {
orgTargets.set(orgName, orgTarget);
} else {
console.error(`Failed to get or create Gitea organization: ${orgName}`);
}
}
}
// Mirror repositories
const queue = new PQueue({ concurrency: 4 });
await queue.addAll(
filteredRepositories.map((repository) => {
return async () => {
// Determine the target (user or organization)
let giteaTarget;
if (config.github.preserveOrgStructure && repository.organization) {
// Use the organization as target
giteaTarget = orgTargets.get(repository.organization);
if (!giteaTarget) {
console.error(`No Gitea organization found for ${repository.organization}, using user instead`);
giteaTarget = config.gitea.organization
? await getGiteaOrganization(gitea, config.gitea.organization)
: giteaUser;
}
} else {
// Use the specified organization or user
giteaTarget = config.gitea.organization
? await getGiteaOrganization(gitea, config.gitea.organization)
: giteaUser;
}
await mirror(
repository,
gitea,
@@ -316,17 +367,17 @@ async function mirrorIssues(repository, gitea, giteaTarget, githubToken, dryRun)
async function mirror(repository, gitea, giteaTarget, githubToken, mirrorIssuesFlag, dryRun) {
if (await isAlreadyMirroredOnGitea(repository, gitea, giteaTarget)) {
console.log(
`Repository ${repository.name} is already mirrored; doing nothing.`
`Repository ${repository.name} is already mirrored in ${giteaTarget.type} ${giteaTarget.name}; doing nothing.`
);
return;
}
if (dryRun) {
console.log(`DRY RUN: Would mirror repository to gitea: ${repository.name}`);
console.log(`DRY RUN: Would mirror repository to ${giteaTarget.type} ${giteaTarget.name}: ${repository.name}`);
return;
}
console.log(`Mirroring repository to gitea: ${repository.name}`);
console.log(`Mirroring repository to ${giteaTarget.type} ${giteaTarget.name}: ${repository.name}`);
try {
await mirrorOnGitea(repository, gitea, giteaTarget, githubToken);