gitlabber 2.0.0__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gitlabber-2.0.0/gitlabber.egg-info → gitlabber-2.1.0}/PKG-INFO +58 -12
- {gitlabber-2.0.0 → gitlabber-2.1.0}/README.rst +55 -11
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/__init__.py +1 -1
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/auth.py +0 -2
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/cli.py +93 -6
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/gitlab_tree.py +1 -1
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/rate_limiter.py +0 -1
- gitlabber-2.1.0/gitlabber/token_storage.py +117 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0/gitlabber.egg-info}/PKG-INFO +58 -12
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber.egg-info/SOURCES.txt +11 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber.egg-info/requires.txt +3 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/pyproject.toml +4 -1
- gitlabber-2.1.0/tests/__pycache__/conftest.cpython-311-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/conftest.cpython-313-pytest-9.0.1.pyc +0 -0
- gitlabber-2.1.0/tests/__pycache__/gitlab_test_utils.cpython-311.pyc +0 -0
- gitlabber-2.1.0/tests/__pycache__/gitlab_test_utils.cpython-313.pyc +0 -0
- gitlabber-2.1.0/tests/__pycache__/io_test_util.cpython-311.pyc +0 -0
- gitlabber-2.1.0/tests/__pycache__/io_test_util.cpython-313.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_archive.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_auth.cpython-313-pytest-9.0.1.pyc +0 -0
- gitlabber-2.1.0/tests/__pycache__/test_cli.cpython-311-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_cli.cpython-313-pytest-9.0.1.pyc +0 -0
- gitlabber-2.1.0/tests/__pycache__/test_cli_token_storage.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_config.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-313-pytest-9.0.1.pyc +0 -0
- gitlabber-2.1.0/tests/__pycache__/test_exceptions.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_format.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_git.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_gitlab_tree.cpython-313-pytest-9.0.1.pyc +0 -0
- gitlabber-2.1.0/tests/__pycache__/test_helpers.cpython-311-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_helpers.cpython-313-pytest-9.0.1.pyc +0 -0
- gitlabber-2.1.0/tests/__pycache__/test_integration.cpython-311-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_integration.cpython-313-pytest-9.0.1.pyc +0 -0
- gitlabber-2.1.0/tests/__pycache__/test_main.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_method.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_naming.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_performance.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_progress.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_rate_limiter.cpython-313-pytest-9.0.1.pyc +0 -0
- gitlabber-2.1.0/tests/__pycache__/test_token_storage.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_url_builder.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/conftest.py +0 -1
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/gitlab_test_utils.py +1 -2
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/io_test_util.py +1 -2
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_cli.py +1 -1
- gitlabber-2.1.0/tests/test_cli_token_storage.py +239 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_config.py +0 -2
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_e2e.py +8 -10
- gitlabber-2.1.0/tests/test_exceptions.py +93 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_gitlab_tree.py +1 -3
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_helpers.py +1 -3
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_integration.py +0 -3
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_main.py +1 -2
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_performance.py +8 -8
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_rate_limiter.py +1 -4
- gitlabber-2.1.0/tests/test_token_storage.py +126 -0
- gitlabber-2.0.0/tests/__pycache__/gitlab_test_utils.cpython-311.pyc +0 -0
- gitlabber-2.0.0/tests/__pycache__/gitlab_test_utils.cpython-313.pyc +0 -0
- gitlabber-2.0.0/tests/__pycache__/io_test_util.cpython-311.pyc +0 -0
- gitlabber-2.0.0/tests/__pycache__/io_test_util.cpython-313.pyc +0 -0
- gitlabber-2.0.0/tests/__pycache__/test_main.cpython-313-pytest-9.0.1.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/LICENSE.txt +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/MANIFEST.in +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/__main__.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/archive.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/config.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/exceptions.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/format.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/git.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/method.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/naming.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/progress.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/tree_builder.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/url_builder.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber.egg-info/dependency_links.txt +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber.egg-info/entry_points.txt +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber.egg-info/top_level.txt +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/setup.cfg +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/setup.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__init__.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/__init__.cpython-311.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/__init__.cpython-312.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/gitlab_test_utils.cpython-312.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/io_test_util.cpython-312.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_archive.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_archive.cpython-312-pytest-8.3.3.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_archive.cpython-312-pytest-8.3.4.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_archive.cpython-312-pytest-8.3.5.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_cli.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_cli.cpython-312-pytest-8.3.3.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_cli.cpython-312-pytest-8.3.4.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_cli.cpython-312-pytest-8.3.5.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-312-pytest-8.3.3.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-312-pytest-8.3.4.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-312-pytest-8.3.5.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-313.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_format.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_format.cpython-312-pytest-8.3.3.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_format.cpython-312-pytest-8.3.4.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_format.cpython-312-pytest-8.3.5.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_git.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_git.cpython-312-pytest-8.3.3.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_git.cpython-312-pytest-8.3.4.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_git.cpython-312-pytest-8.3.5.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_gitlab_tree.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_gitlab_tree.cpython-312-pytest-8.3.3.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_gitlab_tree.cpython-312-pytest-8.3.4.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_gitlab_tree.cpython-312-pytest-8.3.5.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_integration.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_integration.cpython-312-pytest-8.3.3.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_integration.cpython-312-pytest-8.3.4.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_integration.cpython-312-pytest-8.3.5.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_method.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_method.cpython-312-pytest-8.3.3.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_method.cpython-312-pytest-8.3.4.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_method.cpython-312-pytest-8.3.5.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_naming.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_naming.cpython-312-pytest-8.3.3.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_naming.cpython-312-pytest-8.3.4.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_naming.cpython-312-pytest-8.3.5.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_performance.cpython-313.pyc +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test-input.yaml +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test-output.json +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test-output.tree +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test-output.yaml +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_archive.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_auth.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_format.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_git.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_method.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_naming.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_progress.py +0 -0
- {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_url_builder.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitlabber
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A Gitlab clone/pull utility for backing up or cloning Gitlab groups
|
|
5
5
|
Author-email: Erez Mazor <erezmazor@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -34,9 +34,11 @@ Requires-Dist: coverage; extra == "test"
|
|
|
34
34
|
Requires-Dist: pytest; extra == "test"
|
|
35
35
|
Requires-Dist: pytest-cov; extra == "test"
|
|
36
36
|
Requires-Dist: pytest-integration; extra == "test"
|
|
37
|
+
Provides-Extra: keyring
|
|
38
|
+
Requires-Dist: keyring>=24.0.0; extra == "keyring"
|
|
37
39
|
Dynamic: license-file
|
|
38
40
|
|
|
39
|
-
.. image:: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml/badge.svg?branch=
|
|
41
|
+
.. image:: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml/badge.svg?branch=main
|
|
40
42
|
:target: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml
|
|
41
43
|
|
|
42
44
|
.. image:: https://codecov.io/gh/ezbz/gitlabber/branch/main/graph/badge.svg
|
|
@@ -44,6 +46,9 @@ Dynamic: license-file
|
|
|
44
46
|
|
|
45
47
|
.. image:: https://badge.fury.io/py/gitlabber.svg
|
|
46
48
|
:target: https://badge.fury.io/py/gitlabber
|
|
49
|
+
|
|
50
|
+
.. image:: https://img.shields.io/pypi/dm/gitlabber
|
|
51
|
+
:target: https://pypi.org/project/gitlabber/
|
|
47
52
|
|
|
48
53
|
.. image:: https://img.shields.io/pypi/l/gitlabber.svg
|
|
49
54
|
:target: https://pypi.python.org/pypi/gitlabber/
|
|
@@ -71,7 +76,7 @@ Installation
|
|
|
71
76
|
------------
|
|
72
77
|
|
|
73
78
|
System Requirements
|
|
74
|
-
|
|
79
|
+
~~~~~~~~~~~~~~~~~~~
|
|
75
80
|
* Python 3.11 or higher
|
|
76
81
|
* Git 2.0 or higher
|
|
77
82
|
* Network access to GitLab instance
|
|
@@ -92,6 +97,12 @@ Installation Methods
|
|
|
92
97
|
cd gitlabber
|
|
93
98
|
pip install -e .
|
|
94
99
|
|
|
100
|
+
* Optional: Install with secure token storage support:
|
|
101
|
+
|
|
102
|
+
.. code-block:: bash
|
|
103
|
+
|
|
104
|
+
pip install gitlabber[keyring]
|
|
105
|
+
|
|
95
106
|
* You'll need to create an `access token <https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html>`_ from GitLab with API scopes `read_repository`
|
|
96
107
|
and ``read_api`` (or ``api``, for GitLab versions <12.0)
|
|
97
108
|
|
|
@@ -127,6 +138,34 @@ Usage
|
|
|
127
138
|
| exclude | -x | `GITLABBER_EXCLUDE` |
|
|
128
139
|
+------------------+------------------+---------------------------+
|
|
129
140
|
|
|
141
|
+
* **Secure Token Storage**: Gitlabber supports secure token storage using OS-native keyring (Keychain on macOS, Secret Service on Linux, Windows Credential Manager). This allows you to store your GitLab token securely and avoid passing it via CLI or environment variables.
|
|
142
|
+
|
|
143
|
+
**Token Resolution Priority:**
|
|
144
|
+
|
|
145
|
+
1. CLI argument (``-t/--token``) - highest priority
|
|
146
|
+
2. Stored token (from secure storage)
|
|
147
|
+
3. Environment variable (``GITLAB_TOKEN``)
|
|
148
|
+
|
|
149
|
+
**Usage:**
|
|
150
|
+
|
|
151
|
+
.. code-block:: bash
|
|
152
|
+
|
|
153
|
+
# Install keyring (optional, for secure storage)
|
|
154
|
+
pip install gitlabber[keyring]
|
|
155
|
+
|
|
156
|
+
# Store token securely (one-time setup)
|
|
157
|
+
gitlabber --store-token -u https://gitlab.com
|
|
158
|
+
Enter token: [hidden input]
|
|
159
|
+
Token stored securely in keyring for https://gitlab.com ✓
|
|
160
|
+
|
|
161
|
+
# Use stored token automatically (no -t flag needed)
|
|
162
|
+
gitlabber -u https://gitlab.com .
|
|
163
|
+
|
|
164
|
+
# Override with CLI token if needed
|
|
165
|
+
gitlabber -t <token> -u https://gitlab.com .
|
|
166
|
+
|
|
167
|
+
**Note:** If keyring is not installed, gitlabber falls back to environment variables or CLI arguments (current behavior).
|
|
168
|
+
|
|
130
169
|
* To view the tree run the command with your includes/excludes and the ``-p`` flag. It will print your tree like so:
|
|
131
170
|
|
|
132
171
|
.. code-block:: bash
|
|
@@ -157,7 +196,7 @@ Usage
|
|
|
157
196
|
.. code-block:: bash
|
|
158
197
|
|
|
159
198
|
usage: gitlabber [-h] [-t token] [-T] [-u url] [--verbose] [-p] [--print-format {json,yaml,tree}] [-n {name,path}] [-m {ssh,http}]
|
|
160
|
-
[-a {include,exclude,only}] [-i csv] [-x csv] [-c N] [--api-concurrency N] [-r] [-F] [-d] [-s] [-g term] [-U] [-o options] [--version]
|
|
199
|
+
[-a {include,exclude,only}] [-i csv] [-x csv] [-c N] [--api-concurrency N] [-r] [-F] [-d] [-s] [-g term] [-U] [-o options] [--version] [--store-token]
|
|
161
200
|
[dest]
|
|
162
201
|
|
|
163
202
|
Gitlabber - clones or pulls entire groups/projects tree from gitlab
|
|
@@ -198,6 +237,7 @@ Usage
|
|
|
198
237
|
-o options, --git-options options
|
|
199
238
|
provide additional options as csv for the git command (e.g., --depth=1). See: clone/multi_options https://gitpython.readthedocs.io/en/stable/reference.html#
|
|
200
239
|
--version print the version
|
|
240
|
+
--store-token store token securely in OS keyring (requires keyring package)
|
|
201
241
|
|
|
202
242
|
examples:
|
|
203
243
|
|
|
@@ -233,6 +273,12 @@ Usage
|
|
|
233
273
|
# API concurrency speeds up tree discovery, git concurrency speeds up cloning
|
|
234
274
|
gitlabber --api-concurrency 5 -c 10 -t <token> -u <url> .
|
|
235
275
|
|
|
276
|
+
store token securely for future use (one-time setup)
|
|
277
|
+
gitlabber --store-token -u https://gitlab.com
|
|
278
|
+
|
|
279
|
+
use stored token (no -t flag needed)
|
|
280
|
+
gitlabber -u https://gitlab.com .
|
|
281
|
+
|
|
236
282
|
**Performance Results:**
|
|
237
283
|
* Sequential (``--api-concurrency 1``): ~96 seconds
|
|
238
284
|
* With ``--api-concurrency 5``: ~21 seconds (**4.6x speedup**)
|
|
@@ -246,27 +292,27 @@ Common Use Cases
|
|
|
246
292
|
Clone Specific Groups
|
|
247
293
|
---------------------
|
|
248
294
|
|
|
249
|
-
|
|
295
|
+
.. code-block:: bash
|
|
250
296
|
|
|
251
|
-
|
|
252
|
-
|
|
297
|
+
# Clone only projects from a specific group
|
|
298
|
+
gitlabber -i '/MyGroup/**' .
|
|
253
299
|
|
|
254
300
|
Exclude Archived Projects
|
|
255
301
|
-------------------------
|
|
256
302
|
|
|
257
|
-
|
|
303
|
+
.. code-block:: bash
|
|
258
304
|
|
|
259
|
-
|
|
260
|
-
|
|
305
|
+
# Clone all non-archived projects
|
|
306
|
+
gitlabber -a exclude .
|
|
261
307
|
|
|
262
308
|
Debugging
|
|
263
309
|
---------
|
|
264
310
|
* You can use the ``--verbose`` flag to print Gitlabber debug messages
|
|
265
311
|
* For more verbose GitLab messages, you can get the `GitPython <https://gitpython.readthedocs.io/en/stable>`_ module to print more debug messages by setting the environment variable:
|
|
266
312
|
|
|
267
|
-
|
|
313
|
+
.. code-block:: bash
|
|
268
314
|
|
|
269
|
-
|
|
315
|
+
export GIT_PYTHON_TRACE='full'
|
|
270
316
|
|
|
271
317
|
Troubleshooting
|
|
272
318
|
---------------
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.. image:: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml/badge.svg?branch=
|
|
1
|
+
.. image:: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml/badge.svg?branch=main
|
|
2
2
|
:target: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml
|
|
3
3
|
|
|
4
4
|
.. image:: https://codecov.io/gh/ezbz/gitlabber/branch/main/graph/badge.svg
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
.. image:: https://badge.fury.io/py/gitlabber.svg
|
|
8
8
|
:target: https://badge.fury.io/py/gitlabber
|
|
9
|
+
|
|
10
|
+
.. image:: https://img.shields.io/pypi/dm/gitlabber
|
|
11
|
+
:target: https://pypi.org/project/gitlabber/
|
|
9
12
|
|
|
10
13
|
.. image:: https://img.shields.io/pypi/l/gitlabber.svg
|
|
11
14
|
:target: https://pypi.python.org/pypi/gitlabber/
|
|
@@ -33,7 +36,7 @@ Installation
|
|
|
33
36
|
------------
|
|
34
37
|
|
|
35
38
|
System Requirements
|
|
36
|
-
|
|
39
|
+
~~~~~~~~~~~~~~~~~~~
|
|
37
40
|
* Python 3.11 or higher
|
|
38
41
|
* Git 2.0 or higher
|
|
39
42
|
* Network access to GitLab instance
|
|
@@ -54,6 +57,12 @@ Installation Methods
|
|
|
54
57
|
cd gitlabber
|
|
55
58
|
pip install -e .
|
|
56
59
|
|
|
60
|
+
* Optional: Install with secure token storage support:
|
|
61
|
+
|
|
62
|
+
.. code-block:: bash
|
|
63
|
+
|
|
64
|
+
pip install gitlabber[keyring]
|
|
65
|
+
|
|
57
66
|
* You'll need to create an `access token <https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html>`_ from GitLab with API scopes `read_repository`
|
|
58
67
|
and ``read_api`` (or ``api``, for GitLab versions <12.0)
|
|
59
68
|
|
|
@@ -89,6 +98,34 @@ Usage
|
|
|
89
98
|
| exclude | -x | `GITLABBER_EXCLUDE` |
|
|
90
99
|
+------------------+------------------+---------------------------+
|
|
91
100
|
|
|
101
|
+
* **Secure Token Storage**: Gitlabber supports secure token storage using OS-native keyring (Keychain on macOS, Secret Service on Linux, Windows Credential Manager). This allows you to store your GitLab token securely and avoid passing it via CLI or environment variables.
|
|
102
|
+
|
|
103
|
+
**Token Resolution Priority:**
|
|
104
|
+
|
|
105
|
+
1. CLI argument (``-t/--token``) - highest priority
|
|
106
|
+
2. Stored token (from secure storage)
|
|
107
|
+
3. Environment variable (``GITLAB_TOKEN``)
|
|
108
|
+
|
|
109
|
+
**Usage:**
|
|
110
|
+
|
|
111
|
+
.. code-block:: bash
|
|
112
|
+
|
|
113
|
+
# Install keyring (optional, for secure storage)
|
|
114
|
+
pip install gitlabber[keyring]
|
|
115
|
+
|
|
116
|
+
# Store token securely (one-time setup)
|
|
117
|
+
gitlabber --store-token -u https://gitlab.com
|
|
118
|
+
Enter token: [hidden input]
|
|
119
|
+
Token stored securely in keyring for https://gitlab.com ✓
|
|
120
|
+
|
|
121
|
+
# Use stored token automatically (no -t flag needed)
|
|
122
|
+
gitlabber -u https://gitlab.com .
|
|
123
|
+
|
|
124
|
+
# Override with CLI token if needed
|
|
125
|
+
gitlabber -t <token> -u https://gitlab.com .
|
|
126
|
+
|
|
127
|
+
**Note:** If keyring is not installed, gitlabber falls back to environment variables or CLI arguments (current behavior).
|
|
128
|
+
|
|
92
129
|
* To view the tree run the command with your includes/excludes and the ``-p`` flag. It will print your tree like so:
|
|
93
130
|
|
|
94
131
|
.. code-block:: bash
|
|
@@ -119,7 +156,7 @@ Usage
|
|
|
119
156
|
.. code-block:: bash
|
|
120
157
|
|
|
121
158
|
usage: gitlabber [-h] [-t token] [-T] [-u url] [--verbose] [-p] [--print-format {json,yaml,tree}] [-n {name,path}] [-m {ssh,http}]
|
|
122
|
-
[-a {include,exclude,only}] [-i csv] [-x csv] [-c N] [--api-concurrency N] [-r] [-F] [-d] [-s] [-g term] [-U] [-o options] [--version]
|
|
159
|
+
[-a {include,exclude,only}] [-i csv] [-x csv] [-c N] [--api-concurrency N] [-r] [-F] [-d] [-s] [-g term] [-U] [-o options] [--version] [--store-token]
|
|
123
160
|
[dest]
|
|
124
161
|
|
|
125
162
|
Gitlabber - clones or pulls entire groups/projects tree from gitlab
|
|
@@ -160,6 +197,7 @@ Usage
|
|
|
160
197
|
-o options, --git-options options
|
|
161
198
|
provide additional options as csv for the git command (e.g., --depth=1). See: clone/multi_options https://gitpython.readthedocs.io/en/stable/reference.html#
|
|
162
199
|
--version print the version
|
|
200
|
+
--store-token store token securely in OS keyring (requires keyring package)
|
|
163
201
|
|
|
164
202
|
examples:
|
|
165
203
|
|
|
@@ -195,6 +233,12 @@ Usage
|
|
|
195
233
|
# API concurrency speeds up tree discovery, git concurrency speeds up cloning
|
|
196
234
|
gitlabber --api-concurrency 5 -c 10 -t <token> -u <url> .
|
|
197
235
|
|
|
236
|
+
store token securely for future use (one-time setup)
|
|
237
|
+
gitlabber --store-token -u https://gitlab.com
|
|
238
|
+
|
|
239
|
+
use stored token (no -t flag needed)
|
|
240
|
+
gitlabber -u https://gitlab.com .
|
|
241
|
+
|
|
198
242
|
**Performance Results:**
|
|
199
243
|
* Sequential (``--api-concurrency 1``): ~96 seconds
|
|
200
244
|
* With ``--api-concurrency 5``: ~21 seconds (**4.6x speedup**)
|
|
@@ -208,27 +252,27 @@ Common Use Cases
|
|
|
208
252
|
Clone Specific Groups
|
|
209
253
|
---------------------
|
|
210
254
|
|
|
211
|
-
|
|
255
|
+
.. code-block:: bash
|
|
212
256
|
|
|
213
|
-
|
|
214
|
-
|
|
257
|
+
# Clone only projects from a specific group
|
|
258
|
+
gitlabber -i '/MyGroup/**' .
|
|
215
259
|
|
|
216
260
|
Exclude Archived Projects
|
|
217
261
|
-------------------------
|
|
218
262
|
|
|
219
|
-
|
|
263
|
+
.. code-block:: bash
|
|
220
264
|
|
|
221
|
-
|
|
222
|
-
|
|
265
|
+
# Clone all non-archived projects
|
|
266
|
+
gitlabber -a exclude .
|
|
223
267
|
|
|
224
268
|
Debugging
|
|
225
269
|
---------
|
|
226
270
|
* You can use the ``--verbose`` flag to print Gitlabber debug messages
|
|
227
271
|
* For more verbose GitLab messages, you can get the `GitPython <https://gitpython.readthedocs.io/en/stable>`_ module to print more debug messages by setting the environment variable:
|
|
228
272
|
|
|
229
|
-
|
|
273
|
+
.. code-block:: bash
|
|
230
274
|
|
|
231
|
-
|
|
275
|
+
export GIT_PYTHON_TRACE='full'
|
|
232
276
|
|
|
233
277
|
Troubleshooting
|
|
234
278
|
---------------
|
|
@@ -6,9 +6,7 @@ authentication and provides a no-op provider for testing.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
|
-
from typing import Optional
|
|
10
9
|
from gitlab import Gitlab
|
|
11
|
-
from gitlab.exceptions import GitlabAuthenticationError
|
|
12
10
|
|
|
13
11
|
class AuthProvider(ABC):
|
|
14
12
|
"""Interface for GitLab authentication providers."""
|
|
@@ -23,6 +23,7 @@ from .format import PrintFormat
|
|
|
23
23
|
from .gitlab_tree import GitlabTree
|
|
24
24
|
from .method import CloneMethod
|
|
25
25
|
from .naming import FolderNaming
|
|
26
|
+
from .token_storage import TokenStorage, TokenStorageError
|
|
26
27
|
|
|
27
28
|
logging.basicConfig(
|
|
28
29
|
level=logging.INFO,
|
|
@@ -140,6 +141,57 @@ def _require(value: Optional[str], message: str) -> str:
|
|
|
140
141
|
return value
|
|
141
142
|
|
|
142
143
|
|
|
144
|
+
def _resolve_token(
|
|
145
|
+
cli_token: Optional[str],
|
|
146
|
+
url: str,
|
|
147
|
+
settings: GitlabberSettings,
|
|
148
|
+
) -> str:
|
|
149
|
+
"""Resolve token from various sources in priority order.
|
|
150
|
+
|
|
151
|
+
Priority:
|
|
152
|
+
1. CLI argument (-t/--token) - highest priority
|
|
153
|
+
2. Stored token (from secure storage)
|
|
154
|
+
3. Environment variable (GITLAB_TOKEN) - from settings
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
cli_token: Token from CLI argument
|
|
158
|
+
url: GitLab instance URL
|
|
159
|
+
settings: Settings loaded from environment variables
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Resolved token string
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
typer.Exit: If no token found
|
|
166
|
+
"""
|
|
167
|
+
# 1. CLI token (highest priority)
|
|
168
|
+
if cli_token:
|
|
169
|
+
return cli_token
|
|
170
|
+
|
|
171
|
+
# 2. Stored token (if available)
|
|
172
|
+
storage = TokenStorage()
|
|
173
|
+
if storage.is_available():
|
|
174
|
+
stored = storage.retrieve(url)
|
|
175
|
+
if stored:
|
|
176
|
+
return stored
|
|
177
|
+
|
|
178
|
+
# 3. Environment variable (current behavior)
|
|
179
|
+
if settings.token:
|
|
180
|
+
return settings.token
|
|
181
|
+
|
|
182
|
+
# 4. Error - no token found
|
|
183
|
+
from .exceptions import format_error_with_suggestion
|
|
184
|
+
error_msg, suggestion = format_error_with_suggestion(
|
|
185
|
+
'config_missing',
|
|
186
|
+
"Please specify a valid token with -t/--token or the GITLAB_TOKEN environment variable.",
|
|
187
|
+
{}
|
|
188
|
+
)
|
|
189
|
+
typer.secho(error_msg, err=True)
|
|
190
|
+
if suggestion:
|
|
191
|
+
typer.secho(f"\n💡 {suggestion}", err=True)
|
|
192
|
+
raise typer.Exit(1)
|
|
193
|
+
|
|
194
|
+
|
|
143
195
|
def run_gitlabber(
|
|
144
196
|
*,
|
|
145
197
|
dest: Optional[str],
|
|
@@ -202,14 +254,13 @@ def run_gitlabber(
|
|
|
202
254
|
Raises:
|
|
203
255
|
typer.Exit: If required parameters are missing or tree is empty
|
|
204
256
|
"""
|
|
205
|
-
token_value = _require(
|
|
206
|
-
token or settings.token,
|
|
207
|
-
"Please specify a valid token with -t/--token or the GITLAB_TOKEN environment variable.",
|
|
208
|
-
)
|
|
209
257
|
url_value = _require(
|
|
210
258
|
url or settings.url,
|
|
211
259
|
"Please specify a valid gitlab base url with -u/--url or the GITLAB_URL environment variable.",
|
|
212
260
|
)
|
|
261
|
+
|
|
262
|
+
# Resolve token with priority: CLI -> Stored -> Env var
|
|
263
|
+
token_value = _resolve_token(token, url_value, settings)
|
|
213
264
|
if not print_tree_only and dest is None and not user_projects:
|
|
214
265
|
typer.secho(
|
|
215
266
|
"Please specify a destination for the gitlab tree.",
|
|
@@ -279,10 +330,10 @@ def run_gitlabber(
|
|
|
279
330
|
tree.load_tree()
|
|
280
331
|
|
|
281
332
|
if print_tree_only:
|
|
282
|
-
#
|
|
333
|
+
# In print mode, allow empty trees to be printed (user might want to see empty result)
|
|
283
334
|
tree.print_tree(print_format)
|
|
284
335
|
else:
|
|
285
|
-
#
|
|
336
|
+
# In sync mode, empty tree is an error
|
|
286
337
|
if tree.is_empty():
|
|
287
338
|
from .exceptions import format_error_with_suggestion
|
|
288
339
|
error_msg, suggestion = format_error_with_suggestion(
|
|
@@ -439,6 +490,11 @@ def cli(
|
|
|
439
490
|
is_eager=True,
|
|
440
491
|
help="Print version and exit",
|
|
441
492
|
),
|
|
493
|
+
store_token: bool = typer.Option(
|
|
494
|
+
False,
|
|
495
|
+
"--store-token",
|
|
496
|
+
help="Store token securely in OS keyring (requires keyring package)",
|
|
497
|
+
),
|
|
442
498
|
) -> None:
|
|
443
499
|
"""Main CLI command for gitlabber.
|
|
444
500
|
|
|
@@ -453,6 +509,37 @@ def cli(
|
|
|
453
509
|
sys.exit(0)
|
|
454
510
|
|
|
455
511
|
settings = GitlabberSettings()
|
|
512
|
+
|
|
513
|
+
# Handle token storage
|
|
514
|
+
if store_token:
|
|
515
|
+
url_value = url or settings.url
|
|
516
|
+
if not url_value:
|
|
517
|
+
typer.secho(
|
|
518
|
+
"Error: URL required for storing token. Use -u/--url or GITLAB_URL.",
|
|
519
|
+
err=True,
|
|
520
|
+
)
|
|
521
|
+
raise typer.Exit(1)
|
|
522
|
+
|
|
523
|
+
# Get token from CLI or prompt
|
|
524
|
+
token_to_store = token
|
|
525
|
+
if not token_to_store:
|
|
526
|
+
token_to_store = typer.prompt("Enter token", hide_input=True)
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
storage = TokenStorage()
|
|
530
|
+
if not storage.is_available():
|
|
531
|
+
typer.secho(
|
|
532
|
+
"Error: keyring not available. Install with: pip install keyring",
|
|
533
|
+
err=True,
|
|
534
|
+
)
|
|
535
|
+
raise typer.Exit(1)
|
|
536
|
+
storage.store(url_value, token_to_store)
|
|
537
|
+
typer.echo(f"Token stored securely in keyring for {url_value} ✓")
|
|
538
|
+
except TokenStorageError as e:
|
|
539
|
+
typer.secho(f"Error: {str(e)}", err=True)
|
|
540
|
+
raise typer.Exit(1)
|
|
541
|
+
raise typer.Exit(0) # Exit after storing
|
|
542
|
+
|
|
456
543
|
include_shared_value = not exclude_shared
|
|
457
544
|
|
|
458
545
|
run_gitlabber(
|
|
@@ -5,7 +5,7 @@ the project hierarchy from GitLab, filtering it, and synchronizing
|
|
|
5
5
|
repositories to the local filesystem.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Optional,
|
|
8
|
+
from typing import Optional, Union
|
|
9
9
|
from gitlab import Gitlab
|
|
10
10
|
from gitlab.exceptions import GitlabAuthenticationError
|
|
11
11
|
from anytree import Node, RenderTree
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Secure token storage using OS keyring.
|
|
2
|
+
|
|
3
|
+
This module provides a simple interface for storing and retrieving GitLab
|
|
4
|
+
tokens securely using the OS-native keyring. If keyring is not available,
|
|
5
|
+
the storage gracefully degrades and returns None.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TokenStorageError(Exception):
|
|
12
|
+
"""Exception raised for token storage errors."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TokenStorage:
|
|
17
|
+
"""Simple token storage using OS keyring.
|
|
18
|
+
|
|
19
|
+
This class provides secure storage for GitLab tokens using the OS-native
|
|
20
|
+
keyring (Keychain on macOS, Secret Service on Linux, Credential Manager
|
|
21
|
+
on Windows). If the keyring library is not available, all operations
|
|
22
|
+
gracefully fail and return None.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
SERVICE_NAME: Service name used in keyring (identifies gitlabber)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
SERVICE_NAME = "gitlabber"
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
"""Initialize token storage.
|
|
32
|
+
|
|
33
|
+
Attempts to import the keyring library. If unavailable, storage
|
|
34
|
+
operations will return None or raise TokenStorageError.
|
|
35
|
+
"""
|
|
36
|
+
self._keyring = None
|
|
37
|
+
self._try_import_keyring()
|
|
38
|
+
|
|
39
|
+
def _try_import_keyring(self) -> None:
|
|
40
|
+
"""Try to import keyring, fail silently if unavailable."""
|
|
41
|
+
try:
|
|
42
|
+
import keyring
|
|
43
|
+
self._keyring = keyring
|
|
44
|
+
except ImportError:
|
|
45
|
+
self._keyring = None
|
|
46
|
+
|
|
47
|
+
def is_available(self) -> bool:
|
|
48
|
+
"""Check if keyring storage is available.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if keyring is available, False otherwise
|
|
52
|
+
"""
|
|
53
|
+
return self._keyring is not None
|
|
54
|
+
|
|
55
|
+
def store(self, url: str, token: str) -> None:
|
|
56
|
+
"""Store token for a GitLab URL.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
url: GitLab instance URL (e.g., https://gitlab.com)
|
|
60
|
+
token: GitLab personal access token
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
TokenStorageError: If keyring is not available or storage fails
|
|
64
|
+
"""
|
|
65
|
+
if not self.is_available():
|
|
66
|
+
raise TokenStorageError(
|
|
67
|
+
"Keyring not available. Install with: pip install keyring"
|
|
68
|
+
)
|
|
69
|
+
try:
|
|
70
|
+
self._keyring.set_password(self.SERVICE_NAME, url, token)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
raise TokenStorageError(f"Failed to store token: {str(e)}") from e
|
|
73
|
+
|
|
74
|
+
def retrieve(self, url: str) -> Optional[str]:
|
|
75
|
+
"""Retrieve token for a GitLab URL.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
url: GitLab instance URL
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Token if found and keyring is available, None otherwise
|
|
82
|
+
"""
|
|
83
|
+
if not self.is_available():
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
return self._keyring.get_password(self.SERVICE_NAME, url)
|
|
87
|
+
except Exception:
|
|
88
|
+
# Keyring errors (permissions, etc.) - return None gracefully
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def delete(self, url: str) -> None:
|
|
92
|
+
"""Delete stored token for a GitLab URL.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
url: GitLab instance URL
|
|
96
|
+
"""
|
|
97
|
+
if not self.is_available():
|
|
98
|
+
return
|
|
99
|
+
try:
|
|
100
|
+
self._keyring.delete_password(self.SERVICE_NAME, url)
|
|
101
|
+
except Exception:
|
|
102
|
+
# Ignore if not found or other errors
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def list_stored_urls(self) -> list[str]:
|
|
106
|
+
"""List all URLs with stored tokens.
|
|
107
|
+
|
|
108
|
+
Note: Most keyring backends don't support listing credentials.
|
|
109
|
+
This method returns an empty list for compatibility.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of URLs (empty for keyring backends)
|
|
113
|
+
"""
|
|
114
|
+
# Keyring doesn't support listing credentials
|
|
115
|
+
# This is a limitation of most keyring backends
|
|
116
|
+
return []
|
|
117
|
+
|