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.
Files changed (135) hide show
  1. {gitlabber-2.0.0/gitlabber.egg-info → gitlabber-2.1.0}/PKG-INFO +58 -12
  2. {gitlabber-2.0.0 → gitlabber-2.1.0}/README.rst +55 -11
  3. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/__init__.py +1 -1
  4. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/auth.py +0 -2
  5. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/cli.py +93 -6
  6. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/gitlab_tree.py +1 -1
  7. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/rate_limiter.py +0 -1
  8. gitlabber-2.1.0/gitlabber/token_storage.py +117 -0
  9. {gitlabber-2.0.0 → gitlabber-2.1.0/gitlabber.egg-info}/PKG-INFO +58 -12
  10. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber.egg-info/SOURCES.txt +11 -0
  11. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber.egg-info/requires.txt +3 -0
  12. {gitlabber-2.0.0 → gitlabber-2.1.0}/pyproject.toml +4 -1
  13. gitlabber-2.1.0/tests/__pycache__/conftest.cpython-311-pytest-9.0.1.pyc +0 -0
  14. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/conftest.cpython-313-pytest-9.0.1.pyc +0 -0
  15. gitlabber-2.1.0/tests/__pycache__/gitlab_test_utils.cpython-311.pyc +0 -0
  16. gitlabber-2.1.0/tests/__pycache__/gitlab_test_utils.cpython-313.pyc +0 -0
  17. gitlabber-2.1.0/tests/__pycache__/io_test_util.cpython-311.pyc +0 -0
  18. gitlabber-2.1.0/tests/__pycache__/io_test_util.cpython-313.pyc +0 -0
  19. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_archive.cpython-313-pytest-9.0.1.pyc +0 -0
  20. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_auth.cpython-313-pytest-9.0.1.pyc +0 -0
  21. gitlabber-2.1.0/tests/__pycache__/test_cli.cpython-311-pytest-9.0.1.pyc +0 -0
  22. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_cli.cpython-313-pytest-9.0.1.pyc +0 -0
  23. gitlabber-2.1.0/tests/__pycache__/test_cli_token_storage.cpython-313-pytest-9.0.1.pyc +0 -0
  24. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_config.cpython-313-pytest-9.0.1.pyc +0 -0
  25. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-313-pytest-9.0.1.pyc +0 -0
  26. gitlabber-2.1.0/tests/__pycache__/test_exceptions.cpython-313-pytest-9.0.1.pyc +0 -0
  27. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_format.cpython-313-pytest-9.0.1.pyc +0 -0
  28. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_git.cpython-313-pytest-9.0.1.pyc +0 -0
  29. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_gitlab_tree.cpython-313-pytest-9.0.1.pyc +0 -0
  30. gitlabber-2.1.0/tests/__pycache__/test_helpers.cpython-311-pytest-9.0.1.pyc +0 -0
  31. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_helpers.cpython-313-pytest-9.0.1.pyc +0 -0
  32. gitlabber-2.1.0/tests/__pycache__/test_integration.cpython-311-pytest-9.0.1.pyc +0 -0
  33. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_integration.cpython-313-pytest-9.0.1.pyc +0 -0
  34. gitlabber-2.1.0/tests/__pycache__/test_main.cpython-313-pytest-9.0.1.pyc +0 -0
  35. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_method.cpython-313-pytest-9.0.1.pyc +0 -0
  36. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_naming.cpython-313-pytest-9.0.1.pyc +0 -0
  37. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_performance.cpython-313-pytest-9.0.1.pyc +0 -0
  38. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_progress.cpython-313-pytest-9.0.1.pyc +0 -0
  39. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_rate_limiter.cpython-313-pytest-9.0.1.pyc +0 -0
  40. gitlabber-2.1.0/tests/__pycache__/test_token_storage.cpython-313-pytest-9.0.1.pyc +0 -0
  41. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_url_builder.cpython-313-pytest-9.0.1.pyc +0 -0
  42. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/conftest.py +0 -1
  43. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/gitlab_test_utils.py +1 -2
  44. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/io_test_util.py +1 -2
  45. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_cli.py +1 -1
  46. gitlabber-2.1.0/tests/test_cli_token_storage.py +239 -0
  47. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_config.py +0 -2
  48. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_e2e.py +8 -10
  49. gitlabber-2.1.0/tests/test_exceptions.py +93 -0
  50. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_gitlab_tree.py +1 -3
  51. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_helpers.py +1 -3
  52. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_integration.py +0 -3
  53. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_main.py +1 -2
  54. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_performance.py +8 -8
  55. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_rate_limiter.py +1 -4
  56. gitlabber-2.1.0/tests/test_token_storage.py +126 -0
  57. gitlabber-2.0.0/tests/__pycache__/gitlab_test_utils.cpython-311.pyc +0 -0
  58. gitlabber-2.0.0/tests/__pycache__/gitlab_test_utils.cpython-313.pyc +0 -0
  59. gitlabber-2.0.0/tests/__pycache__/io_test_util.cpython-311.pyc +0 -0
  60. gitlabber-2.0.0/tests/__pycache__/io_test_util.cpython-313.pyc +0 -0
  61. gitlabber-2.0.0/tests/__pycache__/test_main.cpython-313-pytest-9.0.1.pyc +0 -0
  62. {gitlabber-2.0.0 → gitlabber-2.1.0}/LICENSE.txt +0 -0
  63. {gitlabber-2.0.0 → gitlabber-2.1.0}/MANIFEST.in +0 -0
  64. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/__main__.py +0 -0
  65. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/archive.py +0 -0
  66. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/config.py +0 -0
  67. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/exceptions.py +0 -0
  68. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/format.py +0 -0
  69. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/git.py +0 -0
  70. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/method.py +0 -0
  71. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/naming.py +0 -0
  72. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/progress.py +0 -0
  73. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/tree_builder.py +0 -0
  74. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber/url_builder.py +0 -0
  75. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber.egg-info/dependency_links.txt +0 -0
  76. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber.egg-info/entry_points.txt +0 -0
  77. {gitlabber-2.0.0 → gitlabber-2.1.0}/gitlabber.egg-info/top_level.txt +0 -0
  78. {gitlabber-2.0.0 → gitlabber-2.1.0}/setup.cfg +0 -0
  79. {gitlabber-2.0.0 → gitlabber-2.1.0}/setup.py +0 -0
  80. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__init__.py +0 -0
  81. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/__init__.cpython-311.pyc +0 -0
  82. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  83. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  84. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/gitlab_test_utils.cpython-312.pyc +0 -0
  85. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/io_test_util.cpython-312.pyc +0 -0
  86. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_archive.cpython-311-pytest-8.2.2.pyc +0 -0
  87. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_archive.cpython-312-pytest-8.3.3.pyc +0 -0
  88. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_archive.cpython-312-pytest-8.3.4.pyc +0 -0
  89. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_archive.cpython-312-pytest-8.3.5.pyc +0 -0
  90. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_cli.cpython-311-pytest-8.2.2.pyc +0 -0
  91. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_cli.cpython-312-pytest-8.3.3.pyc +0 -0
  92. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_cli.cpython-312-pytest-8.3.4.pyc +0 -0
  93. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_cli.cpython-312-pytest-8.3.5.pyc +0 -0
  94. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-311-pytest-8.2.2.pyc +0 -0
  95. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-312-pytest-8.3.3.pyc +0 -0
  96. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-312-pytest-8.3.4.pyc +0 -0
  97. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-312-pytest-8.3.5.pyc +0 -0
  98. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_e2e.cpython-313.pyc +0 -0
  99. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_format.cpython-311-pytest-8.2.2.pyc +0 -0
  100. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_format.cpython-312-pytest-8.3.3.pyc +0 -0
  101. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_format.cpython-312-pytest-8.3.4.pyc +0 -0
  102. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_format.cpython-312-pytest-8.3.5.pyc +0 -0
  103. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_git.cpython-311-pytest-8.2.2.pyc +0 -0
  104. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_git.cpython-312-pytest-8.3.3.pyc +0 -0
  105. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_git.cpython-312-pytest-8.3.4.pyc +0 -0
  106. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_git.cpython-312-pytest-8.3.5.pyc +0 -0
  107. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_gitlab_tree.cpython-311-pytest-8.2.2.pyc +0 -0
  108. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_gitlab_tree.cpython-312-pytest-8.3.3.pyc +0 -0
  109. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_gitlab_tree.cpython-312-pytest-8.3.4.pyc +0 -0
  110. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_gitlab_tree.cpython-312-pytest-8.3.5.pyc +0 -0
  111. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_integration.cpython-311-pytest-8.2.2.pyc +0 -0
  112. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_integration.cpython-312-pytest-8.3.3.pyc +0 -0
  113. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_integration.cpython-312-pytest-8.3.4.pyc +0 -0
  114. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_integration.cpython-312-pytest-8.3.5.pyc +0 -0
  115. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_method.cpython-311-pytest-8.2.2.pyc +0 -0
  116. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_method.cpython-312-pytest-8.3.3.pyc +0 -0
  117. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_method.cpython-312-pytest-8.3.4.pyc +0 -0
  118. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_method.cpython-312-pytest-8.3.5.pyc +0 -0
  119. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_naming.cpython-311-pytest-8.2.2.pyc +0 -0
  120. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_naming.cpython-312-pytest-8.3.3.pyc +0 -0
  121. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_naming.cpython-312-pytest-8.3.4.pyc +0 -0
  122. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_naming.cpython-312-pytest-8.3.5.pyc +0 -0
  123. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/__pycache__/test_performance.cpython-313.pyc +0 -0
  124. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test-input.yaml +0 -0
  125. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test-output.json +0 -0
  126. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test-output.tree +0 -0
  127. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test-output.yaml +0 -0
  128. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_archive.py +0 -0
  129. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_auth.py +0 -0
  130. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_format.py +0 -0
  131. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_git.py +0 -0
  132. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_method.py +0 -0
  133. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_naming.py +0 -0
  134. {gitlabber-2.0.0 → gitlabber-2.1.0}/tests/test_progress.py +0 -0
  135. {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.0.0
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=master
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
- .. code-block:: bash
295
+ .. code-block:: bash
250
296
 
251
- # Clone only projects from a specific group
252
- gitlabber -i '/MyGroup/**' .
297
+ # Clone only projects from a specific group
298
+ gitlabber -i '/MyGroup/**' .
253
299
 
254
300
  Exclude Archived Projects
255
301
  -------------------------
256
302
 
257
- .. code-block:: bash
303
+ .. code-block:: bash
258
304
 
259
- # Clone all non-archived projects
260
- gitlabber -a exclude .
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
- .. code-block:: bash
313
+ .. code-block:: bash
268
314
 
269
- export GIT_PYTHON_TRACE='full'
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=master
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
- .. code-block:: bash
255
+ .. code-block:: bash
212
256
 
213
- # Clone only projects from a specific group
214
- gitlabber -i '/MyGroup/**' .
257
+ # Clone only projects from a specific group
258
+ gitlabber -i '/MyGroup/**' .
215
259
 
216
260
  Exclude Archived Projects
217
261
  -------------------------
218
262
 
219
- .. code-block:: bash
263
+ .. code-block:: bash
220
264
 
221
- # Clone all non-archived projects
222
- gitlabber -a exclude .
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
- .. code-block:: bash
273
+ .. code-block:: bash
230
274
 
231
- export GIT_PYTHON_TRACE='full'
275
+ export GIT_PYTHON_TRACE='full'
232
276
 
233
277
  Troubleshooting
234
278
  ---------------
@@ -5,4 +5,4 @@ while maintaining the directory structure. It supports filtering, progress
5
5
  tracking, and various configuration options.
6
6
  """
7
7
 
8
- __version__ = '2.0.0'
8
+ __version__ = '2.1.0'
@@ -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
- # Always print tree, even if empty (so JSON output is valid)
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
- # Only error if trying to sync an empty tree
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, Any, Union
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
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import threading
6
6
  import time
7
- from typing import Optional
8
7
 
9
8
 
10
9
  class RateLimitedExecutor:
@@ -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
+