11 Commits

Author SHA1 Message Date
mike
f08b4956fd Comment out some debugs (whoops) 2026-01-05 14:39:27 -08:00
mike
e6052e1f40 Update README 2026-01-05 14:29:39 -08:00
mike
52f39a5f9e Give user the option to disable SSL/TLS for the source and destination individually 2026-01-05 14:28:57 -08:00
mike
9afa6277bb Bumping python, which I thought I just did ... 2026-01-05 14:27:36 -08:00
mike
15f59d4741 Bump pyenv+pipenv+deps 2026-01-05 13:21:54 -08:00
mike
782729ee58 Give option to specify working directory (totally useless but too late now) 2025-01-04 02:13:40 -08:00
mike
c43fee7024 Bump python/pipenv/pipenvlock and add init-environment script. 2025-01-04 01:02:05 -08:00
mike
64ef546799 Point Pipfile to my forked repo (for now), and relock. 2024-06-18 02:20:52 -07:00
mike
ec81cab79d Seems to be working now 2024-06-18 02:04:43 -07:00
mike
27a8f73390 Work 2024-06-18 00:57:09 -07:00
mike
17a6422b1b Started refactoring to py-gitea 2024-06-17 23:45:40 -07:00
8 changed files with 504 additions and 176 deletions

View File

@@ -1 +1 @@
3.12.1
3.14.2

View File

@@ -4,10 +4,11 @@ verify_ssl = true
name = "pypi"
[packages]
giteapy-soteria = {git = "https://github.com/Yousif-CS/giteapy.git"}
py-gitea = "*"
# py-gitea = {git = "https://github.com/mikeperalta1/py-gitea.git"}
[dev-packages]
[requires]
python_version = "3.12"
python_version = "3.14"

180
Pipfile.lock generated
View File

@@ -1,11 +1,11 @@
{
"_meta": {
"hash": {
"sha256": "5041607b8b692ebdc03484547b2d4336083196ff75b38c3140b608a7d59abaf8"
"sha256": "2a8acb06970c59def755a6b1fc93b6bc20fc36e1797ff9ce24546b26fdeebca5"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.12"
"python_version": "3.14"
},
"sources": [
{
@@ -18,39 +18,171 @@
"default": {
"certifi": {
"hashes": [
"sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1",
"sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"
"sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c",
"sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"
],
"markers": "python_version >= '3.6'",
"version": "==2023.11.17"
"markers": "python_version >= '3.7'",
"version": "==2026.1.4"
},
"giteapy-soteria": {
"git": "https://github.com/Yousif-CS/giteapy.git",
"ref": "e0a089bdfb7ef6130b43727c50e78f176379db20"
},
"python-dateutil": {
"charset-normalizer": {
"hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
"sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad",
"sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93",
"sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394",
"sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89",
"sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc",
"sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86",
"sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63",
"sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d",
"sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f",
"sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8",
"sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0",
"sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505",
"sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161",
"sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af",
"sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152",
"sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318",
"sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72",
"sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4",
"sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e",
"sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3",
"sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576",
"sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c",
"sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1",
"sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8",
"sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1",
"sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2",
"sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44",
"sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26",
"sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88",
"sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016",
"sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede",
"sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf",
"sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a",
"sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc",
"sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0",
"sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84",
"sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db",
"sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1",
"sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7",
"sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed",
"sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8",
"sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133",
"sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e",
"sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef",
"sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14",
"sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2",
"sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0",
"sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d",
"sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828",
"sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f",
"sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf",
"sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6",
"sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328",
"sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090",
"sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa",
"sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381",
"sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c",
"sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb",
"sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc",
"sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a",
"sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec",
"sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc",
"sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac",
"sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e",
"sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313",
"sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569",
"sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3",
"sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d",
"sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525",
"sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894",
"sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3",
"sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9",
"sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a",
"sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9",
"sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14",
"sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25",
"sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50",
"sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf",
"sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1",
"sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3",
"sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac",
"sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e",
"sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815",
"sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c",
"sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6",
"sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6",
"sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e",
"sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4",
"sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84",
"sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69",
"sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15",
"sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191",
"sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0",
"sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897",
"sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd",
"sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2",
"sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794",
"sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d",
"sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074",
"sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3",
"sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224",
"sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838",
"sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a",
"sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d",
"sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d",
"sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f",
"sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8",
"sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490",
"sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966",
"sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9",
"sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3",
"sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e",
"sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
"version": "==2.8.2"
"markers": "python_version >= '3.7'",
"version": "==3.4.4"
},
"six": {
"idna": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
"version": "==1.16.0"
"markers": "python_version >= '3.8'",
"version": "==3.11"
},
"immutabledict": {
"hashes": [
"sha256:97c31d098a2c850e93a958badeef765e4736ed7942ec73e439facd764a3a7217",
"sha256:cb6ed3090df593148f94cb407d218ca526fd2639694afdb553dc4f50ce6feeca"
],
"markers": "python_version >= '3.8' and python_version < '4.0'",
"version": "==4.2.2"
},
"py-gitea": {
"hashes": [
"sha256:87bd5a684d29e8af4cef8cde3bd7abcc3f7dfc7394c58f7f313468ea91656cd4",
"sha256:f32a73aa951cb499b8cc2c0891a313794cd582c9dc2dc6aa80c237cbaacef361"
],
"index": "pypi",
"markers": "python_version >= '3.11'",
"version": "==0.2.10"
},
"requests": {
"hashes": [
"sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6",
"sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"
],
"markers": "python_version >= '3.9'",
"version": "==2.32.5"
},
"urllib3": {
"hashes": [
"sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3",
"sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"
"sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797",
"sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"
],
"markers": "python_version >= '3.8'",
"version": "==2.1.0"
"markers": "python_version >= '3.9'",
"version": "==2.6.2"
}
},
"develop": {}

View File

@@ -1,3 +1,4 @@
# Mike's Gitea Repo Migrator
Just a script to help make it a little easier to migrate an entire organization (with bulk selection) from one Gitea instance to another.
@@ -10,7 +11,7 @@ Current license: You are free to clone and use this program but all other rights
## Requirements
* python 3.12
* python 3.14.2
* pipenv
## Installation
@@ -43,9 +44,13 @@ $ python ./main.py --help
You'll need to generate an *Application Token* for both the source and destination servers, and pass the token along to the command line.
### SSL Verification
### SSL/TLS Verification
Pass the long switch `--no-verify-ssl` if any server sits behind a self-signed or wonky SSL certificate.
You can disable SSL/TLS verification (risky) in the following ways:
* Pass the long switch `--no-verify-ssl` to completely disable SSL/TLS verification.
* Pass the switch `--no-verify-source-ssl` to disable certificate verification for the source server.
* Pass the switch `--no-verify-destination-ssl` to disable certificate verification for the destination server.
### Destination Repo Names
@@ -55,3 +60,6 @@ You can tweak the destination repo names a bit by using a string that includes `
Topics will be duplicated from all source repos to their corresponding destination repos. You can specify additional topics with the `--destination-topic` switch. For example, to add the topic `migrated` to every repo, pass the switch `--destination-topic migrated`.

56
domain/API.py Normal file
View File

@@ -0,0 +1,56 @@
import gitea
class API:
__DEFAULT_API_PATH = "/api/v1"
def __init__(self, verify_ssl, ca_bundle):
self.__verify_ssl = verify_ssl
self.__ca_bundle = ca_bundle
@staticmethod
def _make_api_base_url(hostname, port):
base = f"https://{hostname}"
if port is not None:
base += f":{port}"
return base
def get(
self,
hostname: str,
port: int,
token: str,
verify_ssl: bool = None
) -> gitea.Gitea:
url = API._make_api_base_url(
hostname=hostname,
port=port
)
ssl_verify_arg = True
if verify_ssl is not None:
ssl_verify_arg = verify_ssl
else:
ssl_verify_arg = self.__verify_ssl
# print(f"API::get -> hostname {hostname}")
# print(f"API::get -> verify_ssl was {verify_ssl}")
# print(f"API::get -> ssl_verify_arg became {ssl_verify_arg}")
if self.__ca_bundle is not None:
ssl_verify_arg = self.__ca_bundle
g = gitea.Gitea(
gitea_url=url,
token_text=token,
verify=ssl_verify_arg
)
return g

View File

@@ -1,6 +1,9 @@
import giteapy
from domain.API import API
import gitea
import logging
import sys
@@ -9,21 +12,20 @@ import certifi
class Migrator:
__DEFAULT_API_PATH = "/api/v1"
__REPO_ORIGINAL_NAME_TOKEN = "%N%"
def __init__(
self,
source_host, source_port, source_token,
destination_host, destination_port, destination_token,
verify_ssl: bool = True, verify_source_ssl: bool = True, verify_destination_ssl: bool = True,
ca_bundle: str = None
):
# noinspection PyTypeChecker
self.__logger: logging.Logger = None
self._init_logger()
self.__verify_ssl = True
self.__source_host = source_host
self.__source_port = source_port
self.__source_token = source_token
@@ -31,6 +33,31 @@ class Migrator:
self.__destination_host = destination_host
self.__destination_port = destination_port
self.__destination_token = destination_token
self.__verify_ssl = verify_ssl
self.__verify_source_ssl = verify_source_ssl
self.__verify_destination_ssl = verify_destination_ssl
self.__ca_bundle = ca_bundle
api = API(
verify_ssl=self.__verify_ssl,
ca_bundle=self.__ca_bundle,
)
self.__source_api = api.get(
hostname=self.__source_host,
port=self.__source_port,
token=self.__source_token,
verify_ssl=self.__verify_source_ssl,
)
self.__destination_api = api.get(
hostname=self.__destination_host,
port=self.__destination_port,
token=self.__destination_token,
verify_ssl=self.__verify_destination_ssl,
)
def _init_logger(self):
@@ -41,6 +68,7 @@ class Migrator:
self.__logger = logger
"""
def _get_user_api(self, hostname, port, token) -> giteapy.UserApi:
conf = giteapy.Configuration()
@@ -50,7 +78,9 @@ class Migrator:
api = giteapy.UserApi(giteapy.ApiClient(conf))
return api
"""
"""
def _get_repo_api(self, hostname, port, token) -> giteapy.RepositoryApi:
conf = giteapy.Configuration()
@@ -60,7 +90,9 @@ class Migrator:
api = giteapy.RepositoryApi(giteapy.ApiClient(conf))
return api
"""
"""
def _get_org_apis(self) -> (giteapy.OrganizationApi, giteapy.OrganizationApi):
api_source = self._get_org_api(
@@ -73,7 +105,9 @@ class Migrator:
)
return api_source, api_destination
"""
"""
def _get_org_api(self, hostname, port, token) -> giteapy.OrganizationApi:
conf = giteapy.Configuration()
@@ -83,26 +117,15 @@ class Migrator:
api = giteapy.OrganizationApi(giteapy.ApiClient(conf))
return api
"""
def _make_api_base(self, hostname, port):
base = f"https://{hostname}"
if port is not None:
base += f":{port}"
base += self.__DEFAULT_API_PATH
return base
def _make_destination_repo_name(self, pattern: str, repo: giteapy.Repository):
def _make_destination_repo_name(self, pattern: str, repo: gitea.Repository):
repo_name = pattern.replace(self.__REPO_ORIGINAL_NAME_TOKEN, repo.name)
return repo_name
def set_verify_ssl(self, b: bool):
self.__verify_ssl = b
"""
def set_ca_bundle(self, bundle_path: str):
self.__logger.info("Setting certificate bundle path")
@@ -111,6 +134,10 @@ class Migrator:
self.__logger.info(f"Old path: {certifi.where()}")
certifi.core._CACERT_PATH = bundle_path
self.__logger.info(f"New path: {certifi.where()}")
# TODO: JUST TESTING
self.__verify_ssl = bundle_path
"""
def migrate_entire_org(
self,
@@ -126,36 +153,48 @@ class Migrator:
assert do_destination_copy_topics is not None, "Destination directive to copy source topics should be specified"
# api_source, api_destination = self._get_org_apis()
api_source: giteapy.OrganizationApi
api_destination: giteapy.OrganizationApi
# api_source: giteapy.OrganizationApi
# api_destination: giteapy.OrganizationApi
# Tattle on certify
self.__logger.info(f"Certifi is currently using CA bundle: {certifi.where()}")
# Grab all org repos
source_repos = self._fetch_all_org_repos(org=source_org)
source_repos = self._fetch_all_org_repos(
org_name=source_org
)
self.__logger.info(f"Found {len(source_repos)} repos on source:")
for repo in source_repos:
repo: giteapy.Repository
self.__logger.info(f"- #{repo.id} {repo.full_name}")
repo: gitea.Repository
self.__logger.info(f"- {repo.get_full_name()}")
print()
# Filter
source_repos = self._filter_repos_for_required_topics(repos=source_repos, topics=source_topics)
source_repos = self._filter_repos_for_required_topics(
repos=source_repos,
topics_required=source_topics
)
print()
self.__logger.info(f"Have {len(source_repos)} remaining repos after topic filtering:")
for repo in source_repos:
repo: gitea.Repository
self.__logger.info(f"- {repo.get_full_name()}")
repos_migrate = []
repos_ignore = []
go_right_now = False
for repo in source_repos:
repo: giteapy.Repository
repo: gitea.Repository
while True:
if interactive:
response = input(f"Migrate repo #{repo.id} \"{repo.full_name}\" ? (Y)es, (N)o, (G)o right now, (Q)uit ==> ")
response = input(
f"Migrate repo #{repo.id} \"{repo.full_name}\" ?"
" (Y)es, (N)o, (G)o right now, (Q)uit ==> "
)
response = response.lower()
else:
response = "y"
@@ -183,28 +222,32 @@ class Migrator:
if go_right_now:
break
#
# Announce repo destination names
self.__logger.info("")
if len(repos_migrate):
self.__logger.info("Repos to migrate:")
for repo in repos_migrate:
repo: giteapy.Repository
destination_name = self._make_destination_repo_name(pattern=destination_repo_name, repo=repo)
repo: gitea.Repository
destination_name = self._make_destination_repo_name(
pattern=destination_repo_name, repo=repo
)
self.__logger.info(
f"#{repo.id} \"{repo.name}\"\n> \"{destination_name}\""
)
else:
self.__logger.info("No repos marked to migrate")
# Announce manually ignored repos
self.__logger.info("")
if len(repos_ignore):
self.__logger.info("Repos to ignore:")
for repo in repos_ignore:
repo: giteapy.Repository
self.__logger.info(f"#{repo.id} \"{repo.name}\"")
repo: gitea.Repository
self.__logger.info(f"#{repo.id} \"{repo.get_full_name()}\"")
else:
self.__logger.info("No repos marked to ignore")
# Migrate
if len(repos_migrate):
confirmation = input("Do you confirm the above selections? Enter MIGRATE ==> ")
@@ -220,7 +263,8 @@ class Migrator:
repos=repos_migrate
)
self.__logger.info(
f"{len(source_repos_successful)} of {len(repos_migrate)} repos successfully migrated."
f"{len(source_repos_successful)} of {len(repos_migrate)}"
" repos successfully migrated."
)
if len(source_repos_failed) > 0:
self.__logger.error(f"Failed to migrate {len(source_repos_failed)} repos:")
@@ -234,62 +278,64 @@ class Migrator:
f"Failed to migrate repo: {repo.name}\n> {exception}"
)
self._delete_migrated_repos(source_org_name=source_org, repos=source_repos_successful)
self._delete_migrated_repos(
source_org_name=source_org,
repos=source_repos_successful
)
else:
self.__logger.info("Confirmation not received; Won't do anything.")
def _fetch_all_org_repos(self, org: str):
def _fetch_all_org_repos(self, org_name: str):
api_source, api_destination = self._get_org_apis()
api_source: giteapy.OrganizationApi
org = gitea.Organization.request(
gitea=self.__source_api,
name=org_name
)
source_repos = []
# Grabs all pages automatically
repos = org.get_repositories()
page = 0
while True:
page += 1 # Starts at 1 for some reason
source_repos_page = api_source.org_list_repos(org, page=page, limit=25)
if len(source_repos_page) == 0:
break
source_repos.extend(source_repos_page)
return source_repos
return repos
def _filter_repos_for_required_topics(
self,
repos: list[giteapy.Repository],
topics: list[str]
repos: list[gitea.Repository],
topics_required: list[str]
) -> list[giteapy.Repository]:
) -> list[gitea.Repository]:
self.__logger.info(f"Filtering source repos for required topics: {topics}")
self.__logger.info(
f"Filtering source repos for required topics: {topics_required}"
)
repos_keep = []
repos_reject = []
repo_topics = {}
api_source_repos = self._get_repo_api(
hostname=self.__source_host, port=self.__source_port,
token=self.__source_token
)
for repo in repos:
repo_topics[repo.id] = api_source_repos.repo_list_topics(owner=repo.owner.login, repo=repo.name)
repo_topics[repo.id] = repo_topics[repo.id].topics
repo: gitea.Repository
if self._check_required_topics(topics_present=repo_topics[repo.id], topics_required=topics):
repo_key = repo.get_full_name()
topics_present = repo.get_topics()
repo_topics[repo_key] = topics_present
if self._check_required_topics(
topics_present=repo_topics[repo_key],
topics_required=topics_required
):
repos_keep.append(repo)
else:
repos_reject.append(repo)
self.__logger.info("")
self.__logger.info(f"\nKeeping {len(repos_keep)} repos because they contain all required topics ({topics}):")
self.__logger.info(
f"\nKeeping {len(repos_keep)} repos"
f" because they contain all required topics ({topics_required}):"
)
if len(repos_keep) > 0:
for repo in repos_keep:
self.__logger.info(f"> {repo.full_name}")
@@ -297,10 +343,12 @@ class Migrator:
self.__logger.info("> None")
self.__logger.info("")
self.__logger.info(f"Rejecting {len(repos_reject)} repos because they don't contain all required topics:")
self.__logger.info(
f"Rejecting {len(repos_reject)} repos because they don't contain all required topics:"
)
if len(repos_reject) > 0:
for repo in repos_reject:
self.__logger.info(f"> {repo.full_name} ({repo_topics[repo.id]})")
self.__logger.info(f"> {repo.full_name}")
else:
self.__logger.info("> None")
@@ -324,118 +372,114 @@ class Migrator:
repos: list
):
api_source, api_destination = self._get_org_apis()
# api_source, api_destination = self._get_org_apis()
# destination_org = api_destination.org_get(org=destination_org_name)
# destination_org: giteapy.Organization
destination_org = api_destination.org_get(org=destination_org_name)
destination_org: giteapy.Organization
self.__logger.info(f"Destination organization: {destination_org.full_name}")
api_source_repos = self._get_repo_api(
hostname=self.__source_host, port=self.__source_port,
token=self.__source_token
api_dest_org = gitea.Organization.request(
gitea=self.__destination_api,
name=destination_org_name
)
self.__logger.info(f"Destination organization: {api_dest_org.full_name}")
source_repos_successful = []
source_repos_failed = []
for source_repo in repos:
source_repo: giteapy.Repository
source_repo: gitea.Repository
this_destination_repo_name = destination_repo_name.replace("%N%", source_repo.name)
self.__logger.info(f"Migrating: {source_repo.name} ==> {this_destination_repo_name}")
source_repo_topics = api_source_repos.repo_list_topics(owner=source_repo.owner.login, repo=source_repo.name)
source_repo_topics = source_repo_topics.topics
migrate_body = giteapy.MigrateRepoForm(
mirror=False,
clone_addr=source_repo.clone_url,
uid=destination_org.id,
private=source_repo.private,
repo_name=this_destination_repo_name,
description=source_repo.description,
labels=True, issues=True, pull_requests=True, releases=True, milestones=True, wiki=True
this_destination_repo_name = destination_repo_name.replace(
"%N%",
source_repo.name
)
# TODO: These three lines represent feature request to giteapy authors
migrate_body.auth_token = self.__source_token
migrate_body.swagger_types["auth_token"] = "str"
migrate_body.attribute_map["auth_token"] = "auth_token"
self.__logger.debug("Migrate body:")
self.__logger.debug(migrate_body)
self.__logger.info(
f"Migrating: {source_repo.name} ==> {this_destination_repo_name}"
)
source_repo_topics = source_repo.get_topics()
try:
destination_api = self._get_repo_api(
hostname=self.__destination_host,
port=self.__destination_port,
token=self.__destination_token,
repo_new = gitea.Repository.migrate_repo(
gitea=self.__destination_api,
service="gitea", # type of remote service
clone_addr=source_repo.clone_url,
repo_name=this_destination_repo_name,
description=source_repo.description,
private=source_repo.private,
auth_token=self.__source_token,
auth_username=None,
auth_password=None,
mirror=False,
mirror_interval=None,
# lfs=False,
# lfs_endpoint="",
wiki=True,
labels=True,
issues=True,
pull_requests=True,
releases=True,
milestones=True,
repo_owner=destination_org_name,
)
except giteapy.rest.ApiException as e:
self.__logger.error(f"Failed to generate destination API: {e}")
source_repos_failed.append(
(source_repo, e)
)
continue
try:
repo_new = destination_api.repo_migrate(body=migrate_body)
except giteapy.rest.ApiException as e:
self.__logger.error(f"Failed to execute repo migration request via API: {e}")
except Exception as e:
self.__logger.error(
f"Failed to execute repo migration request:"
f"\n{e}"
)
source_repos_failed.append(
(source_repo, e)
)
continue
self.__logger.debug(f"Migration result: {repo_new}")
repo_new: giteapy.Repository
repo_new: gitea.Repository
assert repo_new.name == this_destination_repo_name,\
assert repo_new.name == this_destination_repo_name, \
"New repository didn't end up with the correct name. Failure?"
# Copy source topics?
if do_destination_copy_topics:
for topic in source_repo_topics:
self.__logger.debug(f"Appending source topic to new repo: {topic}")
destination_api.repo_add_topc(
owner=destination_org.username,
repo=repo_new.name,
topic=topic,
)
repo_new.add_topic(topic=topic)
# Add specified topics
for topic in destination_topics:
self.__logger.debug(f"Appending topic to new repo: {topic}")
destination_api.repo_add_topc(
owner=destination_org.username,
repo=repo_new.name,
topic=topic,
)
repo_new.add_topic(topic=topic)
source_repos_successful.append(source_repo)
return source_repos_successful, source_repos_failed
def _delete_migrated_repos(self, source_org_name: str, repos: list[giteapy.Repository]):
def _delete_migrated_repos(self, source_org_name: str, repos: list[gitea.Repository]):
if len(repos) == 0:
self.__logger.warning(f"Cannot delete any migrated repos because none were successful!")
return
repo_api = self._get_repo_api(
hostname=self.__source_host,
port=self.__source_port,
token=self.__source_token
)
repo_api: giteapy.RepositoryApi
self.__logger.info("")
self.__logger.info(f"Can now delete {len(repos)} successfully migrated repos:")
self.__logger.info(f"Can now delete repos from source org: {source_org_name}")
self.__logger.info(f"Will delete {len(repos)} successfully migrated repos:")
for r in repos:
r: gitea.Repository
self.__logger.info(f"> #{r.id} \"{r.full_name}\" ==> {r.clone_url}")
response = input("Would you like to delete the successfully migrated repos? Type DELETE ==> ")
# Ask the user to confirm deletion
response = input(
"Would you like to delete the successfully migrated repos? Type DELETE ==> "
)
if response != "DELETE":
self.__logger.info("Okay, won't delete migrated repos.")
return
@@ -444,7 +488,9 @@ class Migrator:
do_delete_all = False
for repo in repos:
self.__logger.info(f"Next repo to delete: #{repo.id} \"{repo.full_name}\"")
repo: gitea.Repository
self.__logger.info(f"Next repo to delete: \"{repo.full_name}\"")
do_delete = True if do_delete_all else False
if do_delete is False:
@@ -478,7 +524,4 @@ class Migrator:
self.__logger.info(f"Deleting repo: {repo.full_name}")
repo_api.repo_delete(
owner=source_org_name,
repo=repo.name
)
repo.delete()

54
init-environment Executable file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
log()
{
echo "[Mike's Gitea Repo Migrator - Init Env] $1"
}
complain()
{
echo "[Mike's Gitea Repo Migrator - Init Env] $1" 1>&2
}
die()
{
complain "Fatal: $1"
exit 1
}
SCRIPT_PATH=$(readlink -f "$0")
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
SCRIPT_NAME=$(basename "$SCRIPT_PATH")
log "Begin ${SCRIPT_NAME}"
log "Script path: ${SCRIPT_PATH}"
log "Script dir: ${SCRIPT_DIR}"
log "PATH: ${PATH}"
log "PWD before switching: $(pwd)"
cd "${SCRIPT_DIR}" || die "Failed to switch to project directory: ${SCRIPT_DIR}"
log "PWD after switching: $(pwd)"
log "Printing environment:"
printenv
log "Ensuring python installation with pyenv"
pyenv versions
pyenv install --skip-existing || die "Failed to ensure python installation with pyenv"
log "Installing/upgrading pip and pipenv"
pip install --upgrade pip pipenv || die "Failed to install/upgrade pip and pipenv"
#log "Removing old pip environment"
#pipenv --rm # Don't die because this will return an error if the env didn't already exist
# Delete and relock, because frikkin nvidia driver deps are inconsistent between x86_64 and RPi
#log "Re-locking pip dependencies"
#rm Pipfile.lock
#pipenv lock || die "Unable to pipenv lock !"
# Actually install/sync
log "Syncing pip dependencies"
pipenv sync || die "Failed to sync pip environment with pipenv"

52
main.py
View File

@@ -4,6 +4,7 @@ from domain.Migrator import Migrator
import argparse
import os
def main():
@@ -12,6 +13,16 @@ def main():
prog="Mike's Gitea Repo Migrator - Move repositories from one Gitea instance to another"
)
parser.add_argument(
"--cwd", "--working-directory",
dest="working_directory",
required=False,
default=None,
help="Specify the working directory"
)
######################
##### Source Arguments
parser.add_argument(
"--source-hostname", "--source-host",
dest="source_hostname",
@@ -45,7 +56,9 @@ def main():
help="Specify zero or more topics required topics for source repositories."
" Any repository that doesn't have all required topics will be skipped"
)
###########################
##### Destination Arguments
parser.add_argument(
"--destination-hostname", "--dest-hostname", "--destination-host", "--dest-host",
dest="destination_hostname",
@@ -115,15 +128,32 @@ def main():
action="store_false",
help="Do not ask to confirm each migration; Migrate all repos quickly.",
)
###################
##### SSL/TLS Stuff
parser.add_argument(
"--no-verify-ssl",
dest="verify_ssl",
default=True,
action="store_false",
help="Don't verify SSL certificates",
help="Don't verify SSL/TLS certificates",
)
parser.add_argument(
"--no-verify-source-ssl",
dest="verify_source_ssl",
default=None,
action="store_false",
help="Don't verify the SSL/TLS certificate for the source host",
)
parser.add_argument(
"--no-verify-destination-ssl",
dest="verify_destination_ssl",
default=None,
action="store_false",
help="Don't verify the SSL/TLS certificate for the destination host",
)
# Doesn't seem to be helpful?
parser.add_argument(
"--ca-bundle",
dest="ca_bundle",
@@ -132,19 +162,23 @@ def main():
)
args = parser.parse_args()
if args.working_directory is not None:
os.chdir(args.working_directory)
mig = Migrator(
source_host=args.source_hostname,
source_port=args.source_port,
source_token=args.source_token,
destination_host=args.destination_hostname,
destination_port=args.destination_port,
destination_token=args.destination_token
destination_token=args.destination_token,
verify_ssl=args.verify_ssl,
verify_source_ssl=args.verify_source_ssl,
verify_destination_ssl=args.verify_destination_ssl,
ca_bundle=args.ca_bundle
)
mig.set_verify_ssl(args.verify_ssl)
if args.ca_bundle:
mig.set_ca_bundle(args.ca_bundle)
mig.migrate_entire_org(
interactive=args.interactive,
source_org=args.source_org,