mirror of
https://github.com/amalgamated-tools/mirror-to-gitea.git
synced 2025-12-23 22:18:05 -05:00
add support for including/excluding specific organizations and preserving organization structure during mirroring
This commit is contained in:
44
README.md
44
README.md
@@ -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'
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user