gitlabber 1.2.2__tar.gz → 1.2.4__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-1.2.2/gitlabber.egg-info → gitlabber-1.2.4}/PKG-INFO +49 -49
- {gitlabber-1.2.2 → gitlabber-1.2.4}/README.rst +46 -39
- gitlabber-1.2.4/gitlabber/__init__.py +1 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber/cli.py +11 -6
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber/git.py +6 -1
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber/gitlab_tree.py +13 -10
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber/progress.py +6 -7
- {gitlabber-1.2.2 → gitlabber-1.2.4/gitlabber.egg-info}/PKG-INFO +49 -49
- {gitlabber-1.2.2 → gitlabber-1.2.4}/setup.py +2 -2
- gitlabber-1.2.4/tests/__pycache__/gitlab_test_utils.cpython-311.pyc +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/__pycache__/test_archive.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.4/tests/__pycache__/test_cli.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/__pycache__/test_e2e.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.4/tests/__pycache__/test_git.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/__pycache__/test_gitlab_tree.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/gitlab_test_utils.py +23 -24
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test-output.json +4 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test-output.yaml +4 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test_cli.py +8 -8
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test_e2e.py +22 -20
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test_git.py +11 -11
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test_gitlab_tree.py +2 -2
- gitlabber-1.2.2/gitlabber/__init__.py +0 -1
- gitlabber-1.2.2/tests/__pycache__/gitlab_test_utils.cpython-311.pyc +0 -0
- gitlabber-1.2.2/tests/__pycache__/test_cli.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.2/tests/__pycache__/test_git.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/LICENSE.txt +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/MANIFEST.in +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber/__main__.py +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber/archive.py +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber/format.py +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber/method.py +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber/naming.py +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber.egg-info/SOURCES.txt +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber.egg-info/dependency_links.txt +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber.egg-info/entry_points.txt +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber.egg-info/requires.txt +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/gitlabber.egg-info/top_level.txt +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/setup.cfg +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/__init__.py +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/__pycache__/__init__.cpython-311.pyc +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/__pycache__/io_test_util.cpython-311.pyc +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/__pycache__/test_format.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/__pycache__/test_integration.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/__pycache__/test_method.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/__pycache__/test_naming.cpython-311-pytest-8.2.2.pyc +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/io_test_util.py +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test-input.yaml +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test-output.tree +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test_archive.py +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test_format.py +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test_integration.py +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test_method.py +0 -0
- {gitlabber-1.2.2 → gitlabber-1.2.4}/tests/test_naming.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: gitlabber
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: A Gitlab clone/pull utility for backing up or cloning Gitlab groups
|
|
5
5
|
Home-page: https://github.com/ezbz/gitlabber
|
|
6
|
-
Download-URL: https://github.com/ezbz/gitlabber/archive/
|
|
6
|
+
Download-URL: https://github.com/ezbz/gitlabber/archive/main.zip
|
|
7
7
|
Author: Erez Mazor
|
|
8
8
|
Author-email: erezmazor@gmail.com
|
|
9
9
|
License: MIT
|
|
@@ -21,20 +21,13 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
23
|
Requires-Python: >=3
|
|
24
|
-
Description-Content-Type: text/
|
|
24
|
+
Description-Content-Type: text/x-rst
|
|
25
25
|
License-File: LICENSE.txt
|
|
26
|
-
Requires-Dist: docopt
|
|
27
|
-
Requires-Dist: anytree
|
|
28
|
-
Requires-Dist: globre
|
|
29
|
-
Requires-Dist: pyyaml
|
|
30
|
-
Requires-Dist: tqdm
|
|
31
|
-
Requires-Dist: GitPython
|
|
32
|
-
Requires-Dist: python-gitlab
|
|
33
26
|
|
|
34
27
|
.. image:: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml/badge.svg?branch=master
|
|
35
28
|
:target: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml
|
|
36
29
|
|
|
37
|
-
.. image:: https://codecov.io/gh/ezbz/gitlabber/branch/
|
|
30
|
+
.. image:: https://codecov.io/gh/ezbz/gitlabber/branch/main/graph/badge.svg
|
|
38
31
|
:target: https://codecov.io/gh/ezbz/gitlabber
|
|
39
32
|
|
|
40
33
|
.. image:: https://badge.fury.io/py/gitlabber.svg
|
|
@@ -62,7 +55,6 @@ Purpose
|
|
|
62
55
|
Gitlabber clones or pulls all projects under a subset of groups / subgroups by building a tree from the GitLab API and allowing you to specify which subset of the tree you want to clone using glob patterns and/or regex expressions.
|
|
63
56
|
|
|
64
57
|
|
|
65
|
-
|
|
66
58
|
Installation
|
|
67
59
|
------------
|
|
68
60
|
|
|
@@ -73,12 +65,13 @@ Installation
|
|
|
73
65
|
pip install gitlabber
|
|
74
66
|
|
|
75
67
|
* 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`
|
|
76
|
-
and
|
|
68
|
+
and ``read_api`` (or ``api``, for GitLab versions <12.0)
|
|
77
69
|
|
|
78
70
|
Usage
|
|
79
71
|
-----
|
|
80
72
|
|
|
81
|
-
* Arguments can be provided via the CLI arguments directly or via environment variables
|
|
73
|
+
* Arguments can be provided via the CLI arguments directly or via environment variables:
|
|
74
|
+
|
|
82
75
|
+---------------+---------------+---------------------------+
|
|
83
76
|
| Argument | Flag | Environment Variable |
|
|
84
77
|
+===============+===============+===========================+
|
|
@@ -95,7 +88,7 @@ Usage
|
|
|
95
88
|
| exclude | -x | `GITLABBER_EXCLUDE` |
|
|
96
89
|
+---------------+---------------+---------------------------+
|
|
97
90
|
|
|
98
|
-
* To view the tree run the command with your includes/excludes and the
|
|
91
|
+
* To view the tree run the command with your includes/excludes and the ``-p`` flag. It will print your tree like so:
|
|
99
92
|
|
|
100
93
|
.. code-block:: bash
|
|
101
94
|
|
|
@@ -111,72 +104,78 @@ Usage
|
|
|
111
104
|
|
|
112
105
|
* To see how to use glob patterns and regex to filter tree nodes, see the `globre project page <https://pypi.org/project/globre/#details>`_.
|
|
113
106
|
|
|
107
|
+
* Include/Exclude patterns do not work at the API level but work on the results returned from the API, for large Gitlab installations this can take a lot of time, if you need to reduce the amound of API calls for such projects use the ``--group-search`` parameter to search only for the top level groups the interest you using the `Gitlab Group Search API <https://docs.gitlab.com/ee/api/groups.html#search-for-group>` which allows you to do a partial like query for a Group's path or name
|
|
108
|
+
|
|
114
109
|
* Cloning vs Pulling: when running Gitlabber consecutively with the same parameters, it will scan the local tree structure; if the project directory exists and is a valid git repository (has .git folder in it) Gitlabber will perform a git pull in the directory, otherwise the project directory will be created and the GitLab project will be cloned into it.
|
|
115
110
|
|
|
116
|
-
* Cloning submodules: use the
|
|
111
|
+
* Cloning submodules: use the ``-r`` flag to recurse git submodules, uses the ``--recursive`` for cloning and utilizes `GitPython's smart update method <https://github.com/gitpython-developers/GitPython/blob/20f4a9d49b466a18f1af1fdfb480bc4520a4cdc2/git/objects/submodule/root.py#L67>`_ for updating cloned repositories
|
|
117
112
|
|
|
118
113
|
* Printed Usage:
|
|
119
114
|
|
|
120
115
|
.. code-block:: bash
|
|
121
116
|
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
usage: gitlabber [-h] [-t token] [-T] [-u url] [--verbose] [-p] [--print-format {json,yaml,tree}] [-n {name,path}] [-m {ssh,http}]
|
|
118
|
+
[-a {include,exclude,only}] [-i csv] [-x csv] [-r] [-F] [-d] [-s] [-g term] [--version]
|
|
119
|
+
[dest]
|
|
124
120
|
|
|
125
121
|
Gitlabber - clones or pulls entire groups/projects tree from gitlab
|
|
126
|
-
|
|
122
|
+
|
|
127
123
|
positional arguments:
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
dest destination path for the cloned tree (created if doesn't exist)
|
|
125
|
+
|
|
130
126
|
options:
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
-h, --help show this help message and exit
|
|
128
|
+
-t token, --token token
|
|
133
129
|
gitlab personal access token https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
-T, --hide-token use an inline URL token (avoids storing the gitlab personal access token in the .git/config)
|
|
131
|
+
-u url, --url url base gitlab url (e.g.: 'http://gitlab.mycompany.com')
|
|
132
|
+
--verbose print more verbose output
|
|
133
|
+
-p, --print print the tree without cloning
|
|
134
|
+
--print-format {json,yaml,tree}
|
|
139
135
|
print format (default: 'tree')
|
|
140
|
-
|
|
136
|
+
-n {name,path}, --naming {name,path}
|
|
141
137
|
the folder naming strategy for projects from the gitlab API attributes (default: "name")
|
|
142
|
-
|
|
138
|
+
-m {ssh,http}, --method {ssh,http}
|
|
143
139
|
the git transport method to use for cloning (default: "ssh")
|
|
144
|
-
|
|
140
|
+
-a {include,exclude,only}, --archived {include,exclude,only}
|
|
145
141
|
include archived projects and groups in the results (default: "include")
|
|
146
|
-
|
|
142
|
+
-i csv, --include csv
|
|
147
143
|
comma delimited list of glob patterns of paths to projects or groups to clone/pull
|
|
148
|
-
|
|
144
|
+
-x csv, --exclude csv
|
|
149
145
|
comma delimited list of glob patterns of paths to projects or groups to exclude from clone/pull
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
146
|
+
-r, --recursive clone/pull git submodules recursively
|
|
147
|
+
-F, --use-fetch clone/fetch git repository (mirrored repositories)
|
|
148
|
+
-d, --dont-checkout don't checkout pulled git repository
|
|
149
|
+
-s, --include-shared include shared projects in the results
|
|
150
|
+
-g term, --group-search term
|
|
151
|
+
only include groups matching the search term, filtering done at the API level (useful for large projects, see: https://docs.gitlab.com/ee/api/groups.html#search-for-group works with partial names of path or name)
|
|
152
|
+
--version print the version
|
|
153
|
+
|
|
156
154
|
examples:
|
|
157
|
-
|
|
155
|
+
|
|
158
156
|
clone an entire gitlab tree using a url and a token:
|
|
159
157
|
gitlabber -t <personal access token> -u <gitlab url>
|
|
160
|
-
|
|
158
|
+
|
|
161
159
|
only print the gitlab tree:
|
|
162
160
|
gitlabber -p .
|
|
163
|
-
|
|
161
|
+
|
|
164
162
|
clone only projects under subgroup 'MySubGroup' to location '~/GitlabRoot':
|
|
165
163
|
gitlabber -i '/MyGroup/MySubGroup**' ~/GitlabRoot
|
|
166
|
-
|
|
164
|
+
|
|
167
165
|
clone only projects under group 'MyGroup' excluding any projects under subgroup 'MySubGroup':
|
|
168
166
|
gitlabber -i '/MyGroup**' -x '/MyGroup/MySubGroup**' .
|
|
169
|
-
|
|
167
|
+
|
|
170
168
|
clone an entire gitlab tree except projects under groups named 'ArchiveGroup':
|
|
171
169
|
gitlabber -x '/ArchiveGroup**' .
|
|
172
|
-
|
|
170
|
+
|
|
173
171
|
clone projects that start with a case insensitive 'w' using a regular expression:
|
|
174
172
|
gitlabber -i '/{[w].*}' .
|
|
175
173
|
|
|
176
174
|
|
|
175
|
+
|
|
177
176
|
Debugging
|
|
178
177
|
---------
|
|
179
|
-
* You can use the
|
|
178
|
+
* You can use the ``--verbose`` flag to print Gitlabber debug messages
|
|
180
179
|
* 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:
|
|
181
180
|
|
|
182
181
|
.. code-block:: bash
|
|
@@ -185,10 +184,11 @@ Debugging
|
|
|
185
184
|
|
|
186
185
|
Troubleshooting
|
|
187
186
|
---------------
|
|
188
|
-
*
|
|
189
|
-
|
|
187
|
+
* ``GitlabHttpError: 503``: make sure you provide the base URL to your GitLab installation (e.g., `https://gitlab.my.com` and not `https://gitlab.my.com/some/nested/path`)
|
|
188
|
+
* ``git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)`` OR ``ERROR: The project you were looking for could not be found or you don't have permission to view it.``: if you are using Git's SSH method, follow the `SSH Guide <https://docs.gitlab.com/ee/user/ssh.html>`_ from Gitlab and ensure you have your SSH key in Gitlab for clone or use the HTTP method (``-m http`` flag)
|
|
189
|
+
|
|
190
190
|
Known Limitations
|
|
191
191
|
-----------------
|
|
192
192
|
* Renaming, moving and deleting projects: Gitlabber doesn't maintain local tree state (projects and groups). For that reason is does not rename move or delete local projects when they are modified on the server. When projects are moved or renamed, Gitlabber will clone them again under their new name or location. When deleted, Gitlabber will not delete the local project.
|
|
193
|
-
* Folder naming strategy: Consecutively running Gitlabber with different values for the
|
|
193
|
+
* Folder naming strategy: Consecutively running Gitlabber with different values for the ``-n`` parameter will produce undesirable results. Use the same value as previous runs, or simply don't change it from the default (project name).
|
|
194
194
|
* If you're going to clone a large number of projects, observe rate limits `for gitlab.com <https://docs.gitlab.com/ee/user/gitlab_com/index.html#gitlabcom-specific-rate-limits/>`_, and `for on-premise installations <https://docs.gitlab.com/ee/security/rate_limits.html>`_.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
.. image:: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml/badge.svg?branch=master
|
|
2
2
|
:target: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml
|
|
3
3
|
|
|
4
|
-
.. image:: https://codecov.io/gh/ezbz/gitlabber/branch/
|
|
4
|
+
.. image:: https://codecov.io/gh/ezbz/gitlabber/branch/main/graph/badge.svg
|
|
5
5
|
:target: https://codecov.io/gh/ezbz/gitlabber
|
|
6
6
|
|
|
7
7
|
.. image:: https://badge.fury.io/py/gitlabber.svg
|
|
@@ -29,7 +29,6 @@ Purpose
|
|
|
29
29
|
Gitlabber clones or pulls all projects under a subset of groups / subgroups by building a tree from the GitLab API and allowing you to specify which subset of the tree you want to clone using glob patterns and/or regex expressions.
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
|
|
33
32
|
Installation
|
|
34
33
|
------------
|
|
35
34
|
|
|
@@ -40,12 +39,13 @@ Installation
|
|
|
40
39
|
pip install gitlabber
|
|
41
40
|
|
|
42
41
|
* 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`
|
|
43
|
-
and
|
|
42
|
+
and ``read_api`` (or ``api``, for GitLab versions <12.0)
|
|
44
43
|
|
|
45
44
|
Usage
|
|
46
45
|
-----
|
|
47
46
|
|
|
48
|
-
* Arguments can be provided via the CLI arguments directly or via environment variables
|
|
47
|
+
* Arguments can be provided via the CLI arguments directly or via environment variables:
|
|
48
|
+
|
|
49
49
|
+---------------+---------------+---------------------------+
|
|
50
50
|
| Argument | Flag | Environment Variable |
|
|
51
51
|
+===============+===============+===========================+
|
|
@@ -62,7 +62,7 @@ Usage
|
|
|
62
62
|
| exclude | -x | `GITLABBER_EXCLUDE` |
|
|
63
63
|
+---------------+---------------+---------------------------+
|
|
64
64
|
|
|
65
|
-
* To view the tree run the command with your includes/excludes and the
|
|
65
|
+
* To view the tree run the command with your includes/excludes and the ``-p`` flag. It will print your tree like so:
|
|
66
66
|
|
|
67
67
|
.. code-block:: bash
|
|
68
68
|
|
|
@@ -78,72 +78,78 @@ Usage
|
|
|
78
78
|
|
|
79
79
|
* To see how to use glob patterns and regex to filter tree nodes, see the `globre project page <https://pypi.org/project/globre/#details>`_.
|
|
80
80
|
|
|
81
|
+
* Include/Exclude patterns do not work at the API level but work on the results returned from the API, for large Gitlab installations this can take a lot of time, if you need to reduce the amound of API calls for such projects use the ``--group-search`` parameter to search only for the top level groups the interest you using the `Gitlab Group Search API <https://docs.gitlab.com/ee/api/groups.html#search-for-group>` which allows you to do a partial like query for a Group's path or name
|
|
82
|
+
|
|
81
83
|
* Cloning vs Pulling: when running Gitlabber consecutively with the same parameters, it will scan the local tree structure; if the project directory exists and is a valid git repository (has .git folder in it) Gitlabber will perform a git pull in the directory, otherwise the project directory will be created and the GitLab project will be cloned into it.
|
|
82
84
|
|
|
83
|
-
* Cloning submodules: use the
|
|
85
|
+
* Cloning submodules: use the ``-r`` flag to recurse git submodules, uses the ``--recursive`` for cloning and utilizes `GitPython's smart update method <https://github.com/gitpython-developers/GitPython/blob/20f4a9d49b466a18f1af1fdfb480bc4520a4cdc2/git/objects/submodule/root.py#L67>`_ for updating cloned repositories
|
|
84
86
|
|
|
85
87
|
* Printed Usage:
|
|
86
88
|
|
|
87
89
|
.. code-block:: bash
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
usage: gitlabber [-h] [-t token] [-T] [-u url] [--verbose] [-p] [--print-format {json,yaml,tree}] [-n {name,path}] [-m {ssh,http}]
|
|
92
|
+
[-a {include,exclude,only}] [-i csv] [-x csv] [-r] [-F] [-d] [-s] [-g term] [--version]
|
|
93
|
+
[dest]
|
|
91
94
|
|
|
92
95
|
Gitlabber - clones or pulls entire groups/projects tree from gitlab
|
|
93
|
-
|
|
96
|
+
|
|
94
97
|
positional arguments:
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
dest destination path for the cloned tree (created if doesn't exist)
|
|
99
|
+
|
|
97
100
|
options:
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
-h, --help show this help message and exit
|
|
102
|
+
-t token, --token token
|
|
100
103
|
gitlab personal access token https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
-T, --hide-token use an inline URL token (avoids storing the gitlab personal access token in the .git/config)
|
|
105
|
+
-u url, --url url base gitlab url (e.g.: 'http://gitlab.mycompany.com')
|
|
106
|
+
--verbose print more verbose output
|
|
107
|
+
-p, --print print the tree without cloning
|
|
108
|
+
--print-format {json,yaml,tree}
|
|
106
109
|
print format (default: 'tree')
|
|
107
|
-
|
|
110
|
+
-n {name,path}, --naming {name,path}
|
|
108
111
|
the folder naming strategy for projects from the gitlab API attributes (default: "name")
|
|
109
|
-
|
|
112
|
+
-m {ssh,http}, --method {ssh,http}
|
|
110
113
|
the git transport method to use for cloning (default: "ssh")
|
|
111
|
-
|
|
114
|
+
-a {include,exclude,only}, --archived {include,exclude,only}
|
|
112
115
|
include archived projects and groups in the results (default: "include")
|
|
113
|
-
|
|
116
|
+
-i csv, --include csv
|
|
114
117
|
comma delimited list of glob patterns of paths to projects or groups to clone/pull
|
|
115
|
-
|
|
118
|
+
-x csv, --exclude csv
|
|
116
119
|
comma delimited list of glob patterns of paths to projects or groups to exclude from clone/pull
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
-r, --recursive clone/pull git submodules recursively
|
|
121
|
+
-F, --use-fetch clone/fetch git repository (mirrored repositories)
|
|
122
|
+
-d, --dont-checkout don't checkout pulled git repository
|
|
123
|
+
-s, --include-shared include shared projects in the results
|
|
124
|
+
-g term, --group-search term
|
|
125
|
+
only include groups matching the search term, filtering done at the API level (useful for large projects, see: https://docs.gitlab.com/ee/api/groups.html#search-for-group works with partial names of path or name)
|
|
126
|
+
--version print the version
|
|
127
|
+
|
|
123
128
|
examples:
|
|
124
|
-
|
|
129
|
+
|
|
125
130
|
clone an entire gitlab tree using a url and a token:
|
|
126
131
|
gitlabber -t <personal access token> -u <gitlab url>
|
|
127
|
-
|
|
132
|
+
|
|
128
133
|
only print the gitlab tree:
|
|
129
134
|
gitlabber -p .
|
|
130
|
-
|
|
135
|
+
|
|
131
136
|
clone only projects under subgroup 'MySubGroup' to location '~/GitlabRoot':
|
|
132
137
|
gitlabber -i '/MyGroup/MySubGroup**' ~/GitlabRoot
|
|
133
|
-
|
|
138
|
+
|
|
134
139
|
clone only projects under group 'MyGroup' excluding any projects under subgroup 'MySubGroup':
|
|
135
140
|
gitlabber -i '/MyGroup**' -x '/MyGroup/MySubGroup**' .
|
|
136
|
-
|
|
141
|
+
|
|
137
142
|
clone an entire gitlab tree except projects under groups named 'ArchiveGroup':
|
|
138
143
|
gitlabber -x '/ArchiveGroup**' .
|
|
139
|
-
|
|
144
|
+
|
|
140
145
|
clone projects that start with a case insensitive 'w' using a regular expression:
|
|
141
146
|
gitlabber -i '/{[w].*}' .
|
|
142
147
|
|
|
143
148
|
|
|
149
|
+
|
|
144
150
|
Debugging
|
|
145
151
|
---------
|
|
146
|
-
* You can use the
|
|
152
|
+
* You can use the ``--verbose`` flag to print Gitlabber debug messages
|
|
147
153
|
* 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:
|
|
148
154
|
|
|
149
155
|
.. code-block:: bash
|
|
@@ -152,10 +158,11 @@ Debugging
|
|
|
152
158
|
|
|
153
159
|
Troubleshooting
|
|
154
160
|
---------------
|
|
155
|
-
*
|
|
156
|
-
|
|
161
|
+
* ``GitlabHttpError: 503``: make sure you provide the base URL to your GitLab installation (e.g., `https://gitlab.my.com` and not `https://gitlab.my.com/some/nested/path`)
|
|
162
|
+
* ``git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)`` OR ``ERROR: The project you were looking for could not be found or you don't have permission to view it.``: if you are using Git's SSH method, follow the `SSH Guide <https://docs.gitlab.com/ee/user/ssh.html>`_ from Gitlab and ensure you have your SSH key in Gitlab for clone or use the HTTP method (``-m http`` flag)
|
|
163
|
+
|
|
157
164
|
Known Limitations
|
|
158
165
|
-----------------
|
|
159
166
|
* Renaming, moving and deleting projects: Gitlabber doesn't maintain local tree state (projects and groups). For that reason is does not rename move or delete local projects when they are modified on the server. When projects are moved or renamed, Gitlabber will clone them again under their new name or location. When deleted, Gitlabber will not delete the local project.
|
|
160
|
-
* Folder naming strategy: Consecutively running Gitlabber with different values for the
|
|
167
|
+
* Folder naming strategy: Consecutively running Gitlabber with different values for the ``-n`` parameter will produce undesirable results. Use the same value as previous runs, or simply don't change it from the default (project name).
|
|
161
168
|
* If you're going to clone a large number of projects, observe rate limits `for gitlab.com <https://docs.gitlab.com/ee/user/gitlab_com/index.html#gitlabcom-specific-rate-limits/>`_, and `for on-premise installations <https://docs.gitlab.com/ee/security/rate_limits.html>`_.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.2.4'
|
|
@@ -37,9 +37,13 @@ def main():
|
|
|
37
37
|
includes=split(args.include)
|
|
38
38
|
excludes=split(args.exclude)
|
|
39
39
|
|
|
40
|
+
args_print = vars(args).copy()
|
|
41
|
+
args_print['token'] = '__hidden__'
|
|
42
|
+
log.debug("running with args [%s]", args_print)
|
|
43
|
+
|
|
40
44
|
tree = GitlabTree(args.url, args.token, args.method, args.naming, args.archived.api_value, includes,
|
|
41
45
|
excludes, args.file, args.concurrency, args.recursive, args.verbose,
|
|
42
|
-
args.use_fetch, args.hide_token)
|
|
46
|
+
args.use_fetch, args.hide_token, group_search=args.group_search)
|
|
43
47
|
log.debug("Reading projects tree from gitlab at [%s]", args.url)
|
|
44
48
|
tree.load_tree()
|
|
45
49
|
|
|
@@ -202,16 +206,17 @@ def parse_args(argv=None):
|
|
|
202
206
|
action='store_true',
|
|
203
207
|
default=True,
|
|
204
208
|
help='include shared projects in the results')
|
|
209
|
+
parser.add_argument(
|
|
210
|
+
'-g',
|
|
211
|
+
'--group-search',
|
|
212
|
+
metavar=('term'),
|
|
213
|
+
help='only include groups matching the search term, filtering done at the API level (useful for large projects, see: https://docs.gitlab.com/ee/api/groups.html#search-for-group works with partial names of path or name)')
|
|
205
214
|
parser.add_argument(
|
|
206
215
|
'--version',
|
|
207
216
|
action='store_true',
|
|
208
217
|
help='print the version')
|
|
209
218
|
|
|
210
|
-
|
|
211
|
-
args_print = vars(args).copy()
|
|
212
|
-
args_print['token'] = 'xxxxx'
|
|
213
|
-
log.debug("running with args [%s]", args_print)
|
|
214
|
-
return args
|
|
219
|
+
return parser.parse_args(argv)
|
|
215
220
|
|
|
216
221
|
def validate_path(value):
|
|
217
222
|
if value.endswith('/'):
|
|
@@ -25,6 +25,7 @@ def sync_tree(root, dest, concurrency=1, disable_progress=False, recursive=False
|
|
|
25
25
|
actions = get_git_actions(root, dest, recursive, use_fetch, hide_token)
|
|
26
26
|
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
|
|
27
27
|
executor.map(clone_or_pull_project, actions)
|
|
28
|
+
|
|
28
29
|
elapsed = progress.finish_progress()
|
|
29
30
|
log.debug("Syncing projects took [%s]", elapsed)
|
|
30
31
|
|
|
@@ -57,6 +58,7 @@ def clone_or_pull_project(action):
|
|
|
57
58
|
'''
|
|
58
59
|
log.debug("updating existing project %s", action.path)
|
|
59
60
|
progress.show_progress(action.node.name, 'pull')
|
|
61
|
+
|
|
60
62
|
try:
|
|
61
63
|
repo = git.Repo(action.path)
|
|
62
64
|
if(not action.use_fetch):
|
|
@@ -74,6 +76,9 @@ def clone_or_pull_project(action):
|
|
|
74
76
|
'''
|
|
75
77
|
Clone new project
|
|
76
78
|
'''
|
|
79
|
+
if(action.node.type != "project"):
|
|
80
|
+
log.debug("Skipping clone of node with type [%s] (empty subgroup/group)", action.node.type)
|
|
81
|
+
return
|
|
77
82
|
log.debug("cloning new project %s", action.path)
|
|
78
83
|
progress.show_progress(action.node.name, 'clone')
|
|
79
84
|
multi_options = []
|
|
@@ -88,5 +93,5 @@ def clone_or_pull_project(action):
|
|
|
88
93
|
log.fatal("User interrupted")
|
|
89
94
|
sys.exit(0)
|
|
90
95
|
except Exception as e:
|
|
91
|
-
log.
|
|
96
|
+
log.error("Error cloning project %s", action.path, exc_info=True)
|
|
92
97
|
|
|
@@ -19,11 +19,11 @@ log = logging.getLogger(__name__)
|
|
|
19
19
|
class GitlabTree:
|
|
20
20
|
|
|
21
21
|
def __init__(self, url, token, method, naming=None, archived=None, includes=[], excludes=[], in_file=None, concurrency=1, recursive=False, disable_progress=False,
|
|
22
|
-
include_shared=True, use_fetch=False, hide_token=False):
|
|
22
|
+
include_shared=True, use_fetch=False, hide_token=False, group_search=None):
|
|
23
23
|
self.includes = includes
|
|
24
24
|
self.excludes = excludes
|
|
25
25
|
self.url = url
|
|
26
|
-
self.root = Node("", root_path="", url=url)
|
|
26
|
+
self.root = Node("", root_path="", url=url, type="root")
|
|
27
27
|
self.gitlab = Gitlab(url, private_token=token,
|
|
28
28
|
ssl_verify=GitlabTree.get_ca_path())
|
|
29
29
|
self.method = method
|
|
@@ -38,6 +38,7 @@ class GitlabTree:
|
|
|
38
38
|
self.include_shared = include_shared
|
|
39
39
|
self.use_fetch = use_fetch
|
|
40
40
|
self.hide_token = hide_token
|
|
41
|
+
self.group_search = group_search
|
|
41
42
|
|
|
42
43
|
@staticmethod
|
|
43
44
|
def get_ca_path():
|
|
@@ -93,8 +94,8 @@ class GitlabTree:
|
|
|
93
94
|
def root_path(self, node):
|
|
94
95
|
return "/".join([str(n.name) for n in node.path])
|
|
95
96
|
|
|
96
|
-
def make_node(self, name, parent, url):
|
|
97
|
-
node = Node(name=name, parent=parent, url=url)
|
|
97
|
+
def make_node(self, type, name, parent, url):
|
|
98
|
+
node = Node(name=name, parent=parent, url=url, type=type)
|
|
98
99
|
node.root_path = self.root_path(node)
|
|
99
100
|
return node
|
|
100
101
|
|
|
@@ -108,7 +109,7 @@ class GitlabTree:
|
|
|
108
109
|
log.debug("Generated URL: %s", project_url)
|
|
109
110
|
else:
|
|
110
111
|
log.debug("Hiding token from project url: %s", project_url)
|
|
111
|
-
node = self.make_node(project_id, parent,
|
|
112
|
+
node = self.make_node("project", project_id, parent,
|
|
112
113
|
url=project_url)
|
|
113
114
|
self.progress.show_progress(node.name, 'project')
|
|
114
115
|
|
|
@@ -118,16 +119,16 @@ class GitlabTree:
|
|
|
118
119
|
self.progress.update_progress_length(len(projects))
|
|
119
120
|
self.add_projects(parent, projects)
|
|
120
121
|
except GitlabListError as error:
|
|
121
|
-
log.error(f"Error getting projects on {group.name}
|
|
122
|
+
log.error(f"Error getting projects on {group.name} id: [{group.id}] error message: [{error.error_message}]")
|
|
122
123
|
|
|
123
124
|
def get_subgroups(self, group, parent):
|
|
124
|
-
subgroups = group.subgroups.list(as_list=False,
|
|
125
|
+
subgroups = group.subgroups.list(as_list=False, get_all=True)
|
|
125
126
|
self.progress.update_progress_length(len(subgroups))
|
|
126
127
|
for subgroup_def in subgroups:
|
|
127
128
|
try:
|
|
128
129
|
subgroup = self.gitlab.groups.get(subgroup_def.id)
|
|
129
130
|
subgroup_id = subgroup.name if self.naming == FolderNaming.NAME else subgroup.path
|
|
130
|
-
node = self.make_node(subgroup_id, parent, url=subgroup.web_url)
|
|
131
|
+
node = self.make_node("subgroup", subgroup_id, parent, url=subgroup.web_url)
|
|
131
132
|
self.progress.show_progress(node.name, 'group')
|
|
132
133
|
self.get_subgroups(subgroup, node)
|
|
133
134
|
self.get_projects(subgroup, node)
|
|
@@ -139,12 +140,14 @@ class GitlabTree:
|
|
|
139
140
|
raise error
|
|
140
141
|
|
|
141
142
|
def load_gitlab_tree(self):
|
|
142
|
-
|
|
143
|
+
log.debug(f"Starting group search with archived: {self.archived} search term: {self.group_search}")
|
|
144
|
+
|
|
145
|
+
groups = self.gitlab.groups.list(as_list=False, archived=self.archived, get_all=True, search=self.group_search)
|
|
143
146
|
self.progress.init_progress(len(groups))
|
|
144
147
|
for group in groups:
|
|
145
148
|
if group.parent_id is None:
|
|
146
149
|
group_id = group.name if self.naming == FolderNaming.NAME else group.path
|
|
147
|
-
node = self.make_node(group_id, self.root, url=group.web_url)
|
|
150
|
+
node = self.make_node("group", group_id, self.root, url=group.web_url)
|
|
148
151
|
self.progress.show_progress(node.name, 'group')
|
|
149
152
|
self.get_subgroups(group, node)
|
|
150
153
|
self.get_projects(group, node)
|
|
@@ -7,13 +7,12 @@ class ProgressBar:
|
|
|
7
7
|
self.progress = None
|
|
8
8
|
self.description = description
|
|
9
9
|
self.disabled = disabled
|
|
10
|
-
self.start =
|
|
10
|
+
self.start = time.time()
|
|
11
11
|
|
|
12
12
|
def init_progress(self, total):
|
|
13
13
|
if self.progress is None:
|
|
14
|
-
self.start = time.time()
|
|
15
14
|
self.progress = tqdm(total=total, unit="projects",
|
|
16
|
-
bar_format="{desc}: {percentage:.1f}%|{bar:80}| {n_fmt}/{total_fmt}{postfix}", desc=self.description, leave=False, disable=self.disabled)
|
|
15
|
+
bar_format="{desc}: {percentage:.1f}%|{bar:80}| {n_fmt}/{total_fmt}{postfix}", desc=self.description, leave=False, disable=self.disabled)
|
|
17
16
|
|
|
18
17
|
def update_progress_length(self, added):
|
|
19
18
|
if self.progress is not None:
|
|
@@ -29,7 +28,7 @@ class ProgressBar:
|
|
|
29
28
|
def finish_progress(self):
|
|
30
29
|
if self.progress is not None:
|
|
31
30
|
self.progress.close()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
end = time.time()
|
|
32
|
+
hours, rem = divmod(end-self.start, 3600)
|
|
33
|
+
minutes, seconds = divmod(rem, 60)
|
|
34
|
+
return "{:0>2}:{:0>2}:{:05.2f}".format(int(hours), int(minutes), seconds)
|