gitlabber 1.1.9__tar.gz → 1.2.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-1.2.0/PKG-INFO +187 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/README.rst +26 -23
- gitlabber-1.2.0/gitlabber/__init__.py +1 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber/cli.py +26 -1
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber/git.py +19 -11
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber/gitlab_tree.py +25 -7
- gitlabber-1.2.0/gitlabber.egg-info/PKG-INFO +187 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber.egg-info/SOURCES.txt +13 -1
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber.egg-info/entry_points.txt +0 -1
- {gitlabber-1.1.9 → gitlabber-1.2.0}/setup.py +5 -5
- gitlabber-1.2.0/tests/__pycache__/__init__.cpython-311.pyc +0 -0
- gitlabber-1.2.0/tests/__pycache__/gitlab_test_utils.cpython-311.pyc +0 -0
- gitlabber-1.2.0/tests/__pycache__/io_test_util.cpython-311.pyc +0 -0
- gitlabber-1.2.0/tests/__pycache__/test_archive.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.0/tests/__pycache__/test_cli.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.0/tests/__pycache__/test_e2e.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.0/tests/__pycache__/test_format.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.0/tests/__pycache__/test_git.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.0/tests/__pycache__/test_gitlab_tree.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.0/tests/__pycache__/test_integration.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.0/tests/__pycache__/test_method.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.0/tests/__pycache__/test_naming.cpython-311-pytest-8.2.2.pyc +0 -0
- gitlabber-1.2.0/tests/gitlab_test_utils.py +174 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test_cli.py +7 -4
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test_git.py +17 -2
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test_gitlab_tree.py +21 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test_integration.py +0 -1
- gitlabber-1.1.9/PKG-INFO +0 -184
- gitlabber-1.1.9/gitlabber/__init__.py +0 -1
- gitlabber-1.1.9/gitlabber.egg-info/PKG-INFO +0 -184
- gitlabber-1.1.9/tests/gitlab_test_utils.py +0 -133
- {gitlabber-1.1.9 → gitlabber-1.2.0}/LICENSE.txt +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/MANIFEST.in +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber/__main__.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber/archive.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber/format.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber/method.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber/naming.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber/progress.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber.egg-info/dependency_links.txt +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber.egg-info/requires.txt +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/gitlabber.egg-info/top_level.txt +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/setup.cfg +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/__init__.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/io_test_util.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test-input.yaml +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test-output.json +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test-output.tree +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test-output.yaml +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test_archive.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test_e2e.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test_format.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test_method.py +0 -0
- {gitlabber-1.1.9 → gitlabber-1.2.0}/tests/test_naming.py +0 -0
gitlabber-1.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: gitlabber
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: A Gitlab clone/pull utility for backing up or cloning Gitlab groups
|
|
5
|
+
Home-page: https://github.com/ezbz/gitlabber
|
|
6
|
+
Download-URL: https://github.com/ezbz/gitlabber/archive/master.zip
|
|
7
|
+
Author: Erez Mazor
|
|
8
|
+
Author-email: erezmazor@gmail.com
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: gitlab,python,cli
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Utilities
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Requires-Python: >=3
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE.txt
|
|
26
|
+
|
|
27
|
+
.. image:: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml/badge.svg?branch=master
|
|
28
|
+
:target: https://github.com/ezbz/gitlabber/actions/workflows/python-app.yml
|
|
29
|
+
|
|
30
|
+
.. image:: https://codecov.io/gh/ezbz/gitlabber/branch/master/graph/badge.svg
|
|
31
|
+
:target: https://codecov.io/gh/ezbz/gitlabber
|
|
32
|
+
|
|
33
|
+
.. image:: https://badge.fury.io/py/gitlabber.svg
|
|
34
|
+
:target: https://badge.fury.io/py/gitlabber
|
|
35
|
+
|
|
36
|
+
.. image:: https://img.shields.io/pypi/l/gitlabber.svg
|
|
37
|
+
:target: https://pypi.python.org/pypi/gitlabber/
|
|
38
|
+
|
|
39
|
+
.. image:: https://img.shields.io/pypi/pyversions/gitlabber
|
|
40
|
+
:target: https://pypi.python.org/pypi/gitlabber/
|
|
41
|
+
|
|
42
|
+
.. image:: https://readthedocs.org/projects/gitlabber/badge/?version=latest&style=plastic
|
|
43
|
+
:target: https://gitlabber.readthedocs.io/en/latest/README.html
|
|
44
|
+
|
|
45
|
+
Gitlabber
|
|
46
|
+
=========
|
|
47
|
+
|
|
48
|
+
* A utility to clone and pull GitLab groups, subgroups, projects based on path selection
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Purpose
|
|
52
|
+
-------
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
Installation
|
|
59
|
+
------------
|
|
60
|
+
|
|
61
|
+
* You can install Gitlabber from `PyPi <https://pypi.org/project/gitlabber>`_:
|
|
62
|
+
|
|
63
|
+
.. code-block:: bash
|
|
64
|
+
|
|
65
|
+
pip install gitlabber
|
|
66
|
+
|
|
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`
|
|
68
|
+
and `read_api` (or `api`, for GitLab versions <12.0)
|
|
69
|
+
|
|
70
|
+
Usage
|
|
71
|
+
-----
|
|
72
|
+
|
|
73
|
+
* Arguments can be provided via the CLI arguments directly or via environment variables::
|
|
74
|
+
+---------------+---------------+---------------------------+
|
|
75
|
+
| Argument | Flag | Environment Variable |
|
|
76
|
+
+===============+===============+===========================+
|
|
77
|
+
| token | -t | `GITLAB_TOKEN` |
|
|
78
|
+
+---------------+---------------+---------------------------+
|
|
79
|
+
| url | -u | `GITLAB_URL` |
|
|
80
|
+
+---------------+---------------+---------------------------+
|
|
81
|
+
| method | -m | `GITLABBER_CLONE_METHOD` |
|
|
82
|
+
+---------------+---------------+---------------------------+
|
|
83
|
+
| naming | -n | `GITLABBER_FOLDER_NAMING` |
|
|
84
|
+
+---------------+---------------+---------------------------+
|
|
85
|
+
| include | -i | `GITLABBER_INCLUDE` |
|
|
86
|
+
+---------------+---------------+---------------------------+
|
|
87
|
+
| exclude | -x | `GITLABBER_EXCLUDE` |
|
|
88
|
+
+---------------+---------------+---------------------------+
|
|
89
|
+
|
|
90
|
+
* To view the tree run the command with your includes/excludes and the `-p` flag. It will print your tree like so:
|
|
91
|
+
|
|
92
|
+
.. code-block:: bash
|
|
93
|
+
|
|
94
|
+
root [http://gitlab.my.com]
|
|
95
|
+
├── group1 [/group1]
|
|
96
|
+
│ └── subgroup1 [/group1/subgroup1]
|
|
97
|
+
│ └── project1 [/group1/subgroup1/project1]
|
|
98
|
+
└── group2 [/group2]
|
|
99
|
+
├── subgroup1 [/group2/subgroup1]
|
|
100
|
+
│ └── project2 [/group2/subgroup1/project2]
|
|
101
|
+
├── subgroup2 [/group2/subgroup2]
|
|
102
|
+
└── subgroup3 [/group2/subgroup3]
|
|
103
|
+
|
|
104
|
+
* To see how to use glob patterns and regex to filter tree nodes, see the `globre project page <https://pypi.org/project/globre/#details>`_.
|
|
105
|
+
|
|
106
|
+
* 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.
|
|
107
|
+
|
|
108
|
+
* 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
|
|
109
|
+
|
|
110
|
+
* Printed Usage:
|
|
111
|
+
|
|
112
|
+
.. code-block:: bash
|
|
113
|
+
|
|
114
|
+
usage: gitlabber [-h] [-t token] [-u url] [--verbose] [-p] [-d]
|
|
115
|
+
[--print-format {json,yaml,tree}] [-m {ssh,https}] [-i csv]
|
|
116
|
+
[-x csv] [--version] [-g {id,full_path,full_name}]
|
|
117
|
+
[dest]
|
|
118
|
+
|
|
119
|
+
Gitlabber - clones or pulls entire groups/projects tree from GitLab
|
|
120
|
+
|
|
121
|
+
positional arguments:
|
|
122
|
+
dest destination path for the cloned tree (created if doesn't exist)
|
|
123
|
+
|
|
124
|
+
optional arguments:
|
|
125
|
+
-h, --help show this help message and exit
|
|
126
|
+
-t token, --token token
|
|
127
|
+
gitlab personal access token https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html
|
|
128
|
+
-u url, --url url base gitlab url (e.g.: 'http://gitlab.mycompany.com')
|
|
129
|
+
--verbose print more verbose output
|
|
130
|
+
-p, --print print the tree without cloning
|
|
131
|
+
--print-format {json,yaml,tree}
|
|
132
|
+
print format (default: 'tree')
|
|
133
|
+
-n {name,path}, --naming {name,path}
|
|
134
|
+
the folder naming strategy for projects (default: "name")
|
|
135
|
+
-m {ssh,http}, --method {ssh,http}
|
|
136
|
+
the git transport method to use for cloning (default: "ssh")
|
|
137
|
+
-a {include,exclude,only}, --archived {include,exclude,only}
|
|
138
|
+
include archived projects and groups in the results (default: "include")
|
|
139
|
+
-i csv, --include csv
|
|
140
|
+
comma delimited list of glob patterns of paths to projects or groups to clone/pull
|
|
141
|
+
-x csv, --exclude csv
|
|
142
|
+
comma delimited list of glob patterns of paths to projects or groups to exclude from clone/pull
|
|
143
|
+
-r, --recursive clone/pull git submodules recursively
|
|
144
|
+
-T, --hide-token hide the token from the gitlab request URL (for method: http)
|
|
145
|
+
-f, --use-fetch clone/fetch git repository (mirrored repositories)
|
|
146
|
+
-s, --include-shared include shared projects in the results
|
|
147
|
+
--version print the version
|
|
148
|
+
|
|
149
|
+
examples:
|
|
150
|
+
|
|
151
|
+
clone an entire gitlab tree using a base url and a token:
|
|
152
|
+
gitlabber -t <personal access token> -u <gitlab url> .
|
|
153
|
+
|
|
154
|
+
# the following examples assume you provided token/url in environment variables so these arguments are omitted
|
|
155
|
+
only print the gitlab tree:
|
|
156
|
+
gitlabber -p .
|
|
157
|
+
|
|
158
|
+
clone only projects under subgroup 'MySubGroup' to location '~/GitlabRoot':
|
|
159
|
+
gitlabber -i '/MyGroup/MySubGroup**' ~/GitlabRoot
|
|
160
|
+
|
|
161
|
+
clone only projects under group 'MyGroup' excluding any projects under subgroup 'MySubGroup':
|
|
162
|
+
gitlabber -i '/MyGroup**' -x '/MyGroup/MySubGroup**' .
|
|
163
|
+
|
|
164
|
+
clone an entire gitlab tree except projects under groups named 'ArchiveGroup':
|
|
165
|
+
gitlabber -x '/ArchiveGroup**' .
|
|
166
|
+
|
|
167
|
+
clone projects that start with a case insensitive 'w' using a regular expression:
|
|
168
|
+
gitlabber -i '/{[w].*}' .
|
|
169
|
+
|
|
170
|
+
Debugging
|
|
171
|
+
---------
|
|
172
|
+
* You can use the `--verbose` flag to print Gitlabber debug messages
|
|
173
|
+
* 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:
|
|
174
|
+
|
|
175
|
+
.. code-block:: bash
|
|
176
|
+
|
|
177
|
+
export GIT_PYTHON_TRACE='full'
|
|
178
|
+
|
|
179
|
+
Troubleshooting
|
|
180
|
+
---------------
|
|
181
|
+
* `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`)
|
|
182
|
+
|
|
183
|
+
Known Limitations
|
|
184
|
+
-----------------
|
|
185
|
+
* 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.
|
|
186
|
+
* 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).
|
|
187
|
+
* 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>`_.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
.. image:: https://img.shields.io/pypi/l/gitlabber.svg
|
|
11
11
|
:target: https://pypi.python.org/pypi/gitlabber/
|
|
12
12
|
|
|
13
|
-
.. image:: https://img.shields.io/pypi/pyversions/
|
|
13
|
+
.. image:: https://img.shields.io/pypi/pyversions/gitlabber
|
|
14
14
|
:target: https://pypi.python.org/pypi/gitlabber/
|
|
15
15
|
|
|
16
16
|
.. image:: https://readthedocs.org/projects/gitlabber/badge/?version=latest&style=plastic
|
|
@@ -19,27 +19,27 @@
|
|
|
19
19
|
Gitlabber
|
|
20
20
|
=========
|
|
21
21
|
|
|
22
|
-
* A utility to clone and pull
|
|
22
|
+
* A utility to clone and pull GitLab groups, subgroups, projects based on path selection
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
Purpose
|
|
26
26
|
-------
|
|
27
27
|
|
|
28
|
-
Gitlabber clones or pulls all projects under a subset of groups / subgroups by building a tree from the
|
|
28
|
+
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.
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
Installation
|
|
33
33
|
------------
|
|
34
34
|
|
|
35
|
-
* You can install
|
|
35
|
+
* You can install Gitlabber from `PyPi <https://pypi.org/project/gitlabber>`_:
|
|
36
36
|
|
|
37
37
|
.. code-block:: bash
|
|
38
38
|
|
|
39
39
|
pip install gitlabber
|
|
40
40
|
|
|
41
|
-
* You'll need to create an
|
|
42
|
-
and `read_api`
|
|
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`
|
|
42
|
+
and `read_api` (or `api`, for GitLab versions <12.0)
|
|
43
43
|
|
|
44
44
|
Usage
|
|
45
45
|
-----
|
|
@@ -61,7 +61,7 @@ Usage
|
|
|
61
61
|
| exclude | -x | `GITLABBER_EXCLUDE` |
|
|
62
62
|
+---------------+---------------+---------------------------+
|
|
63
63
|
|
|
64
|
-
* To view the tree run the command with your includes/excludes and the `-p` flag
|
|
64
|
+
* To view the tree run the command with your includes/excludes and the `-p` flag. It will print your tree like so:
|
|
65
65
|
|
|
66
66
|
.. code-block:: bash
|
|
67
67
|
|
|
@@ -75,22 +75,22 @@ Usage
|
|
|
75
75
|
├── subgroup2 [/group2/subgroup2]
|
|
76
76
|
└── subgroup3 [/group2/subgroup3]
|
|
77
77
|
|
|
78
|
-
* To see how to use glob patterns and regex to filter tree nodes see `globre project page <https://pypi.org/project/globre
|
|
78
|
+
* To see how to use glob patterns and regex to filter tree nodes, see the `globre project page <https://pypi.org/project/globre/#details>`_.
|
|
79
79
|
|
|
80
|
-
* Cloning vs Pulling: when running Gitlabber consecutively with same parameters it will scan the local tree structure
|
|
80
|
+
* 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.
|
|
81
81
|
|
|
82
|
-
* 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>`
|
|
82
|
+
* 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
|
|
83
83
|
|
|
84
84
|
* Printed Usage:
|
|
85
85
|
|
|
86
86
|
.. code-block:: bash
|
|
87
87
|
|
|
88
|
-
usage: gitlabber [-h] [-t token] [-u url] [--verbose] [-p]
|
|
88
|
+
usage: gitlabber [-h] [-t token] [-u url] [--verbose] [-p] [-d]
|
|
89
89
|
[--print-format {json,yaml,tree}] [-m {ssh,https}] [-i csv]
|
|
90
|
-
[-x csv] [--version]
|
|
90
|
+
[-x csv] [--version] [-g {id,full_path,full_name}]
|
|
91
91
|
[dest]
|
|
92
92
|
|
|
93
|
-
Gitlabber - clones or pulls entire groups/projects tree from
|
|
93
|
+
Gitlabber - clones or pulls entire groups/projects tree from GitLab
|
|
94
94
|
|
|
95
95
|
positional arguments:
|
|
96
96
|
dest destination path for the cloned tree (created if doesn't exist)
|
|
@@ -115,6 +115,9 @@ Usage
|
|
|
115
115
|
-x csv, --exclude csv
|
|
116
116
|
comma delimited list of glob patterns of paths to projects or groups to exclude from clone/pull
|
|
117
117
|
-r, --recursive clone/pull git submodules recursively
|
|
118
|
+
-T, --hide-token hide the token from the gitlab request URL (for method: http)
|
|
119
|
+
-f, --use-fetch clone/fetch git repository (mirrored repositories)
|
|
120
|
+
-s, --include-shared include shared projects in the results
|
|
118
121
|
--version print the version
|
|
119
122
|
|
|
120
123
|
examples:
|
|
@@ -138,21 +141,21 @@ Usage
|
|
|
138
141
|
clone projects that start with a case insensitive 'w' using a regular expression:
|
|
139
142
|
gitlabber -i '/{[w].*}' .
|
|
140
143
|
|
|
141
|
-
Debugging
|
|
144
|
+
Debugging
|
|
142
145
|
---------
|
|
143
|
-
* You can use the `--verbose` flag to
|
|
144
|
-
* For more verbose
|
|
146
|
+
* You can use the `--verbose` flag to print Gitlabber debug messages
|
|
147
|
+
* 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:
|
|
145
148
|
|
|
146
149
|
.. code-block:: bash
|
|
147
150
|
|
|
148
151
|
export GIT_PYTHON_TRACE='full'
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
* `GitlabHttpError: 503`: make sure you provide the base
|
|
153
|
+
Troubleshooting
|
|
154
|
+
---------------
|
|
155
|
+
* `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`)
|
|
153
156
|
|
|
154
157
|
Known Limitations
|
|
155
|
-
-----------------
|
|
156
|
-
*
|
|
157
|
-
* Folder
|
|
158
|
-
*
|
|
158
|
+
-----------------
|
|
159
|
+
* 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 `-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
|
+
* 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.0'
|
|
@@ -38,7 +38,8 @@ def main():
|
|
|
38
38
|
excludes=split(args.exclude)
|
|
39
39
|
|
|
40
40
|
tree = GitlabTree(args.url, args.token, args.method, args.naming, args.archived.api_value, includes,
|
|
41
|
-
excludes, args.file, args.concurrency, args.recursive, args.verbose
|
|
41
|
+
excludes, args.file, args.concurrency, args.recursive, args.verbose,
|
|
42
|
+
args.use_fetch, args.hide_token)
|
|
42
43
|
log.debug("Reading projects tree from gitlab at [%s]", args.url)
|
|
43
44
|
tree.load_tree()
|
|
44
45
|
|
|
@@ -105,6 +106,12 @@ def parse_args(argv=None):
|
|
|
105
106
|
metavar=('token'),
|
|
106
107
|
default=os.environ.get('GITLAB_TOKEN'),
|
|
107
108
|
help='gitlab personal access token https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html')
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
'-T',
|
|
111
|
+
'--hide-token',
|
|
112
|
+
action='store_true',
|
|
113
|
+
default=False,
|
|
114
|
+
help='use an inline URL token (avoids storing the gitlab personal access token in the .git/config)')
|
|
108
115
|
parser.add_argument(
|
|
109
116
|
'-u',
|
|
110
117
|
'--url',
|
|
@@ -177,6 +184,24 @@ def parse_args(argv=None):
|
|
|
177
184
|
action='store_true',
|
|
178
185
|
default=False,
|
|
179
186
|
help='clone/pull git submodules recursively')
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
'-F',
|
|
189
|
+
'--use-fetch',
|
|
190
|
+
action='store_true',
|
|
191
|
+
default=False,
|
|
192
|
+
help='clone/fetch git repository (mirrored repositories)')
|
|
193
|
+
parser.add_argument(
|
|
194
|
+
'-d',
|
|
195
|
+
'--dont-checkout',
|
|
196
|
+
action='store_true',
|
|
197
|
+
default=False,
|
|
198
|
+
help="don't checkout pulled git repository")
|
|
199
|
+
parser.add_argument(
|
|
200
|
+
'-s',
|
|
201
|
+
'--include-shared',
|
|
202
|
+
action='store_true',
|
|
203
|
+
default=True,
|
|
204
|
+
help='include shared projects in the results')
|
|
180
205
|
parser.add_argument(
|
|
181
206
|
'--version',
|
|
182
207
|
action='store_true',
|
|
@@ -12,31 +12,33 @@ progress = ProgressBar('* syncing projects')
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class GitAction:
|
|
15
|
-
def __init__(self, node, path, recursive=False):
|
|
15
|
+
def __init__(self, node, path, recursive=False, use_fetch=False, inline_token=False):
|
|
16
16
|
self.node = node
|
|
17
17
|
self.path = path
|
|
18
18
|
self.recursive = recursive
|
|
19
|
+
self.use_fetch = use_fetch
|
|
20
|
+
self.inline_token = inline_token
|
|
19
21
|
|
|
20
|
-
def sync_tree(root, dest, concurrency=1, disable_progress=False, recursive=False):
|
|
22
|
+
def sync_tree(root, dest, concurrency=1, disable_progress=False, recursive=False, use_fetch=False, inline_token=False):
|
|
21
23
|
if not disable_progress:
|
|
22
24
|
progress.init_progress(len(root.leaves))
|
|
23
|
-
actions = get_git_actions(root, dest, recursive)
|
|
25
|
+
actions = get_git_actions(root, dest, recursive, use_fetch, inline_token)
|
|
24
26
|
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
|
|
25
27
|
executor.map(clone_or_pull_project, actions)
|
|
26
28
|
elapsed = progress.finish_progress()
|
|
27
29
|
log.debug("Syncing projects took [%s]", elapsed)
|
|
28
30
|
|
|
29
31
|
|
|
30
|
-
def get_git_actions(root, dest, recursive):
|
|
32
|
+
def get_git_actions(root, dest, recursive, use_fetch, inline_token):
|
|
31
33
|
actions = []
|
|
32
34
|
for child in root.children:
|
|
33
35
|
path = "%s%s" % (dest, child.root_path)
|
|
34
36
|
if not os.path.exists(path):
|
|
35
37
|
os.makedirs(path)
|
|
36
38
|
if child.is_leaf:
|
|
37
|
-
actions.append(GitAction(child, path, recursive))
|
|
39
|
+
actions.append(GitAction(child, path, recursive, use_fetch, inline_token))
|
|
38
40
|
if not child.is_leaf:
|
|
39
|
-
actions.extend(get_git_actions(child, dest, recursive))
|
|
41
|
+
actions.extend(get_git_actions(child, dest, recursive, use_fetch, inline_token))
|
|
40
42
|
return actions
|
|
41
43
|
|
|
42
44
|
|
|
@@ -57,7 +59,10 @@ def clone_or_pull_project(action):
|
|
|
57
59
|
progress.show_progress(action.node.name, 'pull')
|
|
58
60
|
try:
|
|
59
61
|
repo = git.Repo(action.path)
|
|
60
|
-
|
|
62
|
+
if(not action.use_fetch):
|
|
63
|
+
repo.remotes.origin.pull()
|
|
64
|
+
else:
|
|
65
|
+
repo.remotes.origin.fetch()
|
|
61
66
|
if(action.recursive):
|
|
62
67
|
repo.submodule_update(recursive=True)
|
|
63
68
|
except KeyboardInterrupt:
|
|
@@ -71,11 +76,14 @@ def clone_or_pull_project(action):
|
|
|
71
76
|
'''
|
|
72
77
|
log.debug("cloning new project %s", action.path)
|
|
73
78
|
progress.show_progress(action.node.name, 'clone')
|
|
79
|
+
multi_options = []
|
|
80
|
+
if(action.recursive):
|
|
81
|
+
multi_options.append('--recursive')
|
|
82
|
+
if(action.use_fetch):
|
|
83
|
+
multi_options.append('--mirror')
|
|
74
84
|
try:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
else:
|
|
78
|
-
git.Repo.clone_from(action.node.url, action.path)
|
|
85
|
+
git.Repo.clone_from(action.node.url, action.path, multi_options=multi_options)
|
|
86
|
+
|
|
79
87
|
except KeyboardInterrupt:
|
|
80
88
|
log.fatal("User interrupted")
|
|
81
89
|
sys.exit(0)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
from gitlab import Gitlab
|
|
2
|
+
from gitlab.exceptions import GitlabGetError, GitlabListError
|
|
3
|
+
from gitlab.exceptions import GitlabGetError
|
|
2
4
|
from anytree import Node, RenderTree
|
|
3
5
|
from anytree.exporter import DictExporter, JsonExporter
|
|
4
6
|
from anytree.importer import DictImporter
|
|
@@ -16,7 +18,8 @@ log = logging.getLogger(__name__)
|
|
|
16
18
|
|
|
17
19
|
class GitlabTree:
|
|
18
20
|
|
|
19
|
-
def __init__(self, url, token, method, naming, archived=None, includes=[], excludes=[], in_file=None, concurrency=1, recursive=False, disable_progress=False
|
|
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):
|
|
20
23
|
self.includes = includes
|
|
21
24
|
self.excludes = excludes
|
|
22
25
|
self.url = url
|
|
@@ -31,6 +34,10 @@ class GitlabTree:
|
|
|
31
34
|
self.recursive = recursive
|
|
32
35
|
self.disable_progress = disable_progress
|
|
33
36
|
self.progress = ProgressBar('* loading tree', disable_progress)
|
|
37
|
+
self.token = token
|
|
38
|
+
self.include_shared = include_shared
|
|
39
|
+
self.use_fetch = use_fetch
|
|
40
|
+
self.hide_token = hide_token
|
|
34
41
|
|
|
35
42
|
@staticmethod
|
|
36
43
|
def get_ca_path():
|
|
@@ -57,7 +64,7 @@ class GitlabTree:
|
|
|
57
64
|
|
|
58
65
|
def is_excluded(self, node):
|
|
59
66
|
'''
|
|
60
|
-
returns True if the node should be excluded
|
|
67
|
+
returns True if the node should be excluded
|
|
61
68
|
if the are no exclude patterns then nothing is excluded
|
|
62
69
|
any exclude pattern matching the root path will result in exclusion
|
|
63
70
|
'''
|
|
@@ -96,14 +103,23 @@ class GitlabTree:
|
|
|
96
103
|
for project in projects:
|
|
97
104
|
project_id = project.name if self.naming == FolderNaming.NAME else project.path
|
|
98
105
|
project_url = project.ssh_url_to_repo if self.method is CloneMethod.SSH else project.http_url_to_repo
|
|
106
|
+
if self.token is not None and self.method is CloneMethod.HTTP:
|
|
107
|
+
if (not self.hide_token):
|
|
108
|
+
project_url = project_url.replace('://', '://gitlab-token:%s@' % self.token)
|
|
109
|
+
log.debug("Generated URL: %s", project_url)
|
|
110
|
+
else:
|
|
111
|
+
log.debug("Hiding token from project url: %s", project_url)
|
|
99
112
|
node = self.make_node(project_id, parent,
|
|
100
113
|
url=project_url)
|
|
101
114
|
self.progress.show_progress(node.name, 'project')
|
|
102
115
|
|
|
103
116
|
def get_projects(self, group, parent):
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
try:
|
|
118
|
+
projects = group.projects.list(archived=self.archived, with_shared=self.include_shared, get_all=True)
|
|
119
|
+
self.progress.update_progress_length(len(projects))
|
|
120
|
+
self.add_projects(parent, projects)
|
|
121
|
+
except GitlabListError as error:
|
|
122
|
+
log.error(f"Error getting projects on {group.name} (ID: {group.id}): {error} )")
|
|
107
123
|
|
|
108
124
|
def get_subgroups(self, group, parent):
|
|
109
125
|
subgroups = group.subgroups.list(as_list=False, archived=self.archived)
|
|
@@ -116,6 +132,7 @@ class GitlabTree:
|
|
|
116
132
|
self.get_subgroups(subgroup, node)
|
|
117
133
|
self.get_projects(subgroup, node)
|
|
118
134
|
|
|
135
|
+
|
|
119
136
|
def load_gitlab_tree(self):
|
|
120
137
|
groups = self.gitlab.groups.list(as_list=False, archived=self.archived)
|
|
121
138
|
self.progress.init_progress(len(groups))
|
|
@@ -125,7 +142,7 @@ class GitlabTree:
|
|
|
125
142
|
node = self.make_node(group_id, self.root, url=group.web_url)
|
|
126
143
|
self.progress.show_progress(node.name, 'group')
|
|
127
144
|
self.get_subgroups(group, node)
|
|
128
|
-
self.get_projects(group, node)
|
|
145
|
+
self.get_projects(group, node)
|
|
129
146
|
|
|
130
147
|
elapsed = self.progress.finish_progress()
|
|
131
148
|
log.debug("Loading projects tree from gitlab took [%s]", elapsed)
|
|
@@ -178,7 +195,8 @@ class GitlabTree:
|
|
|
178
195
|
log.debug("Going to clone/pull [%s] groups and [%s] projects" %
|
|
179
196
|
(len(self.root.descendants) - len(self.root.leaves), len(self.root.leaves)))
|
|
180
197
|
sync_tree(self.root, dest, concurrency=self.concurrency,
|
|
181
|
-
disable_progress=self.disable_progress, recursive=self.recursive
|
|
198
|
+
disable_progress=self.disable_progress, recursive=self.recursive,
|
|
199
|
+
use_fetch=self.use_fetch, hide_token=self.hide_token)
|
|
182
200
|
|
|
183
201
|
def is_empty(self):
|
|
184
202
|
return self.root.height < 1
|