diff --git a/.gitignore b/.gitignore
index 61138c7..fa47885 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
-.env
+**/*.env
*.log
gitlab2gitea/gitea_projects.json
gitlab2gitea/gitlab_projects.json
+.venv*
+.vscode
\ No newline at end of file
diff --git a/README.md b/README.md
index be00e14..1dabfaf 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,10 @@ We're planning to release a growing amount of open source software, that is free
### What scripts can be found here?
-| Name | Description | License | Current Version | Written in | Supported Distros | Path |
-| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------- | ---------- | -------------------- | ------------------------------------------------------------------------------------------------------------ |
-| Borgmatic Setup Tool | If you plan to use borg as backup solution, you should also take a look at [borgmatic](https://torsion.org/borgmatic/). It's a Python wrapper for the award winning backup tool [borgbackup](https://borgbackup.readthedocs.io/en/stable/index.html) that simplifies creating secure and reliable backups even more. You can even store your configurations in files. This script will do the setup for you to. | [MIT]([LICENSE](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/LICENSE)) | v1.2.1 | Bash | Debian and derivates | [Borgmatic Setup](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/borgmatic) |
-| Git Rewrite Author | **USE WITH CAUTION!!!**
This script will rewrite the entire history of the remote end and set the author email to the provided one.
**This is NOT reversible!** | [MIT]([LICENSE](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/LICENSE)) | v1.0.0 | Bash | Most Linux distros | [Git Scripts](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/git) |
-| UFW Beautifier | Simple Python script to get a fancy formatted `ufw.log` | [MIT]([LICENSE](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/LICENSE)) | v1.0.0 | Python | Most Linux distros | [UFW Beautifier](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/ufw) |
-| Gitlab2Gitea Migration | Python script to perform a full migration from Gitlab to Gitea | [MIT]([LICENSE](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/LICENSE)) | v1.0.0-pre | Python | Most Linux distros | [Gitlab to Gitea](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/gitlab2gitea) |
\ No newline at end of file
+| Name | Description | License | Current Version | Written in | Supported Distros | Path |
+| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------- | ---------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
+| Borgmatic Setup Tool | If you plan to use borg as backup solution, you should also take a look at [borgmatic](https://torsion.org/borgmatic/). It's a Python wrapper for the award winning backup tool [borgbackup](https://borgbackup.readthedocs.io/en/stable/index.html) that simplifies creating secure and reliable backups even more. You can even store your configurations in files. This script will do the setup for you to. | [MIT]([LICENSE](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/LICENSE)) | v1.2.1 | Bash | Debian and derivates | [Borgmatic Setup](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/borgmatic/) |
+| Git Rewrite Author | **USE WITH CAUTION!!!**
This script will rewrite the entire history of the remote end and set the author email to the provided one.
**This is NOT reversible!** | [MIT]([LICENSE](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/LICENSE)) | v1.0.0 | Bash | Most Linux distros | [Git Scripts](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/git/) |
+| UFW Beautifier | Simple Python script to get a fancy formatted `ufw.log` | [MIT]([LICENSE](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/LICENSE)) | v1.0.0 | Python | Most Linux distros | [UFW Beautifier](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/ufw/) |
+| Gitlab2Gitea Migration | Python script to perform a full migration from Gitlab to Gitea | [MIT]([LICENSE](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/LICENSE)) | v1.0.0-pre | Python | Most Linux distros | [Gitlab to Gitea](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/gitlab2gitea/) |
+| Gitea Teams Auto Mapper | Python script to automatically copy teams from a source organization to other organizations | [MIT]([LICENSE](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/LICENSE)) | v1.0.0 | Python | Most Linux distros | [Gitlab Teams Auto Mapper](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/gitea/auto_mapper/) |
\ No newline at end of file
diff --git a/gitea/auto_mapper/README.md b/gitea/auto_mapper/README.md
new file mode 100644
index 0000000..3d6c953
--- /dev/null
+++ b/gitea/auto_mapper/README.md
@@ -0,0 +1,42 @@
+# Gitea Teams auto mapping
+
+## Description
+This script will copy / update all teams (excluding `Owners`) from a specified source organization to all other organizations (except for excluded ones). It can be used to achieve a more controllable permissions hierarchy within Gitea, similar to sub-groups as being available in Gitlab.
+
+## Author(s)
+- [Enrico Ludwig](https://git.zion-networks.de/eludwig) <[enrico.ludwig@zion-networks.de](mailto:enrico.ludwig@zion-networks.de?subject=Gitlab%20to%20Gitea%20migration%20script)>
+
+## License
+MIT License. For more details, refer to the [LICENSE](https://git.zion-networks.de/ZionNetworks/linux-bash-scripts/src/branch/main/LICENSE) file.
+
+## Usage
+```sh
+pip install -r requirements.txt
+python3 auto_mapper.py [options]
+```
+
+## Available script arguments
+
+| Argument | Type | Description | Valid values |
+| --------------- | --------------- | ----------------------------------------------- | ----------------------------------------------------------------- |
+| `--host` | Key-Value | Specify the Gitea instance host | An IP address or hostname |
+| `--port` | Key-Value | Specify the Gitea instance port | A valid port from 1 to 65535 |
+| `--token` | Key-Value | Specify the Gitea instance token | A valid Gitea user token string |
+| `--source-orga` | Key-Value | Specify the source organization | The name of the source organization |
+| `--exclude` | Multi Key-Value | Specify organizations to exclude | Can be used multiple times to exclude specific organizations |
+| `--ssl` | Switch | Specify if the Gitea instance uses SSL | Enable SSL support (https) |
+| `--debug` | Switch | Enable debug logging | Enable debug output |
+| `--dry-run` | Switch | Enable dry-run mode, no changes will be made | Enable dry-run mode to prevent changes |
+| `--update` | Switch | Updates existing teams in target organizations | Enable to update already existing teams at target organizations |
+| `--override` | Switch | Override existing teams in target organizations | Enable to override already existing teams at target organizations |
+
+## Example
+```sh
+python3 auto_mapper.py \
+ --host "127.0.0.1" \
+ --port 3000 \
+ --token "your-secret-user-token" \
+ --source-orga "MyTemplateOrga"
+```
+
+For any issues, please refer to the contact details provided in the author's contact information section.
\ No newline at end of file
diff --git a/gitea/auto_mapper/auto_mapper.py b/gitea/auto_mapper/auto_mapper.py
new file mode 100644
index 0000000..5c66edc
--- /dev/null
+++ b/gitea/auto_mapper/auto_mapper.py
@@ -0,0 +1,232 @@
+import argparse
+import os
+import socket
+import sys
+from gitea import *
+from rich.console import Console
+from rich.spinner import Spinner
+from rich.logging import RichHandler
+from loguru import logger
+
+# fmt: off
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--host", help="Specify the Gitea instance host")
+parser.add_argument("--port", help="Specify the Gitea instance port")
+parser.add_argument("--token", help="Specify the Gitea instance token")
+parser.add_argument("--ssl", help="Specify if the Gitea instance uses SSL", action="store_true")
+parser.add_argument("--debug", help="Enable debug logging", action="store_true")
+parser.add_argument("--source-orga", help="Specify the source organization")
+parser.add_argument("--dry-run", help="Enable dry-run mode, no changes will be made", action="store_true")
+parser.add_argument("--exclude", help="Specify organizations to exclude", nargs="+")
+parser.add_argument("--update", help="Updates existing teams in target organizations", action="store_true")
+parser.add_argument("--override", help="Override existing teams in target organizations", action="store_true")
+args = parser.parse_args()
+
+GITEA_INSTANCE : str = args.host if args.host else None
+GITEA_PORT : int = args.port if args.port else None
+GITEA_TOKEN : str = args.token if args.token else None
+GITEA_SSL : bool = args.ssl if args.ssl else False
+SOURCE_ORGA : str = args.source_orga if args.source_orga else None
+DRY_RUN : bool = args.dry_run if args.dry_run else False
+EXCLUDE_ORGAS : list = args.exclude if args.exclude else []
+UPDATE_TEAMS : bool = args.update if args.update else False
+OVERRIDE_TEAMS : bool = args.override if args.override else False
+
+# fmt: on
+
+console = Console()
+logger.remove() # Remove default handler
+logger.add(
+ RichHandler(console=console, show_time=True, show_level=True, show_path=False),
+ format="{time:YYYY-MM-DD HH:mm:ss} - {message}",
+ level="DEBUG" if args.debug else "INFO",
+)
+
+
+def check_host(host, port):
+
+ if not host:
+ raise Exception("Host not specified")
+
+ if not port:
+ port = 3000
+ logger.warning(f"Port not specified, defaulting to {port}")
+
+ try:
+ port = int(port)
+ if port < 1 or port > 65535:
+ raise ValueError("Invalid port number")
+ except ValueError:
+ raise Exception(f"{port} is not a valid port")
+
+ try:
+ socket.inet_aton(host)
+ except socket.error:
+ if host.startswith("http://"):
+ host = host[7:]
+ elif host.startswith("https://"):
+ host = host[8:]
+ else:
+ raise Exception(f"{host} is not a valid host")
+
+ try:
+ socket.gethostbyname(host)
+ except Exception as e:
+ raise Exception(f"{host} is not a valid host: {e}")
+
+ try:
+ logger.debug(f"Checking connection to {host}:{port}")
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.settimeout(5)
+ s.connect((host, port))
+ s.close()
+ except Exception as e:
+ raise Exception(f"Connection to {host}:{port} failed: {e}")
+
+
+# Log with a spinner
+with console.status("- Checking Gitea Endpoint", spinner="dots") as status:
+ try:
+ check_host(GITEA_INSTANCE, GITEA_PORT)
+ logger.info("Gitea endpoint is valid")
+ except Exception as e:
+ logger.error(f"Failed to check Gitea endpoint: {e}")
+
+# Create a Gitea API client
+gitea_client = None
+with console.status("- Creating Gitea Client", spinner="dots") as status:
+ try:
+ if GITEA_SSL:
+ logger.debug("Using SSL")
+ gitea_client = Gitea(f"https://{GITEA_INSTANCE}:{GITEA_PORT}", GITEA_TOKEN)
+ else:
+ logger.debug("Not using SSL")
+ gitea_client = Gitea(f"http://{GITEA_INSTANCE}:{GITEA_PORT}", GITEA_TOKEN)
+ except Exception as e:
+ logger.error(f"Failed to create Gitea client: {e}")
+
+if not gitea_client:
+ os._exit(1)
+
+# Check if the token is valid
+with console.status("- Checking Gitea Token", spinner="dots") as status:
+ try:
+ v = gitea_client.get_version()
+ u = gitea_client.get_user()
+ logger.info(f"Connected to Gitea {v}")
+ logger.info(f"Authenticated as {u.username}")
+ except Exception as e:
+ logger.error(f"Failed to check Gitea token: {e}")
+
+# Get the source organization
+source_orga = None
+with console.status("- Getting Source Organization", spinner="dots") as status:
+ try:
+ source_orga = Organization.request(gitea_client, SOURCE_ORGA)
+ logger.info(f"Source organization is '{source_orga.name}'")
+ except Exception as e:
+ logger.error(f"Failed to get source organization: {e}")
+
+if not source_orga:
+ os._exit(1)
+
+# Get the source organization teams
+source_orga_teams = None
+with console.status("- Getting Source Organization Teams", spinner="dots") as status:
+ try:
+ source_orga_teams = source_orga.get_teams()
+ source_orga_teams = [
+ team for team in source_orga_teams if team.name != "Owners"
+ ] # Skip the default team 'Owners'
+ logger.info(f"Source organization has {len(source_orga_teams)} teams:")
+ for team in source_orga_teams:
+ logger.info(f" - {team.name}")
+ logger.info("Note: The default team 'Owners' will always be skipped!")
+ except Exception as e:
+ logger.error(f"Failed to get source organization teams: {e}")
+
+if not source_orga_teams:
+ os._exit(1)
+
+# Get all other organizations except for the source organization
+all_orgas = None
+with console.status("- Getting All Organizations", spinner="dots") as status:
+ try:
+ all_orgas = gitea_client.get_orgs()
+ all_orgas = [orga for orga in all_orgas if orga.name != source_orga.name]
+ logger.info(f"Found {len(all_orgas)} other organizations:")
+ for orga in all_orgas:
+ logger.info(f" - {orga.name}")
+ logger.info(
+ f"Note: The source organization {source_orga.name} will always be skipped!"
+ )
+ except Exception as e:
+ logger.error(f"Failed to get all organizations: {e}")
+
+if not all_orgas:
+ os._exit(1)
+
+# Copy teams from source organization to all other organizations except for the source organization
+with console.status("- Copying Teams", spinner="dots") as status:
+ if DRY_RUN:
+ logger.warning("Dry-run mode enabled, no changes will be made")
+
+ if OVERRIDE_TEAMS:
+ logger.info("Update mode enabled, existing teams will be updated")
+
+ for orga in all_orgas:
+ if orga.name in EXCLUDE_ORGAS:
+ logger.info(f"Skipping organization '{orga.name}'")
+ continue
+
+ logger.info(f"{source_orga.name} -> {orga.name}")
+ for team in source_orga_teams:
+ try:
+ # check if the team already exists in the target organization
+ existing_team = orga.get_team(team.name, True)
+
+ if OVERRIDE_TEAMS and existing_team:
+ logger.info(f"\tDeleting existing team '{team.name}'")
+ if not DRY_RUN:
+ existing_team.delete()
+ existing_team = None
+
+ logger.info(f"\tCreating team '{team.name}'")
+ if not DRY_RUN and not existing_team:
+ new_team = gitea_client.create_team(
+ org=orga,
+ name=team.name,
+ description=team.description,
+ permission="read",
+ includes_all_repositories=False,
+ can_create_org_repo=False,
+ units=[
+ "repo.code",
+ "repo.issues",
+ "repo.ext_issues",
+ "repo.wiki",
+ "repo.pulls",
+ "repo.releases",
+ "repo.ext_wiki",
+ "repo.actions",
+ "repo.projects",
+ ],
+ units_map={
+ "repo.code": "none",
+ "repo.ext_issues": "none",
+ "repo.ext_wiki": "none",
+ "repo.issues": "none",
+ "repo.projects": "none",
+ "repo.pulls": "none",
+ "repo.releases": "none",
+ "repo.wiki": "none",
+ },
+ )
+
+ if not DRY_RUN and existing_team:
+ logger.info(f"\tUpdating existing team '{team.name}'")
+ existing_team.description = team.description
+
+ except Exception as e:
+ logger.error(f"Failed to copy team '{team.name}': {e}")
diff --git a/gitea/auto_mapper/requirements.txt b/gitea/auto_mapper/requirements.txt
new file mode 100644
index 0000000..cc73eda
--- /dev/null
+++ b/gitea/auto_mapper/requirements.txt
@@ -0,0 +1,9 @@
+rich
+loguru
+tqdm
+blessings
+InquirerPy
+pyfiglet
+alive-progress
+py-gitea
+python-dotenv
\ No newline at end of file