Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

<!--next-version-placeholder-->

## [1.2.6] - 02/07/2024
### Added
- Added ability to provide git options to the GitPython clone/update method to support things like shallow clone (e.g., --depth=1)
### Changed
### Deprecated
### Removed
### Fixed
### Security

## [1.2.5] - 02/07/2024
### Added
- Added ability to clone a user's personal projects
Expand Down
10 changes: 9 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Usage
.. code-block:: bash

usage: gitlabber [-h] [-t token] [-T] [-u url] [--verbose] [-p] [--print-format {json,yaml,tree}] [-n {name,path}] [-m {ssh,http}]
[-a {include,exclude,only}] [-i csv] [-x csv] [-r] [-F] [-d] [-s] [-g term] [-U] [--version]
[-a {include,exclude,only}] [-i csv] [-x csv] [-r] [-F] [-d] [-s] [-g term] [-U] [-o options] [--version]
[dest]

Gitlabber - clones or pulls entire groups/projects tree from gitlab
Expand Down Expand Up @@ -124,6 +124,8 @@ Usage
-g term, --group-search term
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)
-U, --user-projects fetch only user personal projects (skips the group tree altogether, group related parameters are ignored). Clones personal projects to '{gitlab-username}-personal-projects'
-o options, --git-options options
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#
--version print the version

examples:
Expand All @@ -146,6 +148,12 @@ Usage
clone projects that start with a case insensitive 'w' using a regular expression:
gitlabber -i '/{[w].*}' .

clone a user's personal projects to username-personal-projects
gitlabber -U .

perform a shallow clone of the git repositories
gitlabber -o "\-\-depth=1," .



Debugging
Expand Down
2 changes: 1 addition & 1 deletion gitlabber/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.2.5.1'
__version__ = '1.2.6'
15 changes: 14 additions & 1 deletion gitlabber/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def main():

tree = GitlabTree(args.url, args.token, args.method, args.naming, args.archived.api_value, includes,
excludes, args.file, args.concurrency, args.recursive, args.verbose,
args.include_shared, args.use_fetch, args.hide_token, args.user_projects, group_search=args.group_search)
args.include_shared, args.use_fetch, args.hide_token, args.user_projects,
group_search=args.group_search, git_options=args.git_options)
tree.load_tree()

if tree.is_empty():
Expand Down Expand Up @@ -91,6 +92,12 @@ def parse_args(argv=None):

clone projects that start with a case insensitive 'w' using a regular expression:
gitlabber -i '/{[w].*}' .

clone a user's personal projects to username-personal-projects
gitlabber -U .

perform a shallow clone of the git repositories
gitlabber -o "\-\-depth=1," .
'''

parser = ArgumentParser(
Expand Down Expand Up @@ -216,6 +223,12 @@ def parse_args(argv=None):
action='store_true',
default=False,
help='fetch only user personal projects (skips the group tree altogether, group related parameters are ignored). Clones personal projects to \'{gitlab-username}-personal-projects\'')
parser.add_argument(
'-o',
'--git-options',
nargs=1,
metavar=('options'),
help='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#')
parser.add_argument(
'--version',
action='store_true',
Expand Down
9 changes: 6 additions & 3 deletions gitlabber/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@


class GitAction:
def __init__(self, node, path, recursive=False, use_fetch=False, hide_token=False):
def __init__(self, node, path, recursive=False, use_fetch=False, hide_token=False, git_options=None):
self.node = node
self.path = path
self.recursive = recursive
self.use_fetch = use_fetch
self.hide_token = hide_token
self.git_options = git_options

def sync_tree(root, dest, concurrency=1, disable_progress=False, recursive=False, use_fetch=False, hide_token=False):
def sync_tree(root, dest, concurrency=1, disable_progress=False, recursive=False, use_fetch=False, hide_token=False, git_options=None):
if not disable_progress:
progress.init_progress(len(root.leaves))
actions = get_git_actions(root, dest, recursive, use_fetch, hide_token)
Expand Down Expand Up @@ -71,7 +72,7 @@ def clone_or_pull_project(action):
log.fatal("User interrupted")
sys.exit(0)
except Exception as e:
log.debug("Error pulling project %s", action.path, exc_info=True)
log.error("Error pulling project %s", action.path, exc_info=True)
else:
'''
Clone new project
Expand All @@ -86,6 +87,8 @@ def clone_or_pull_project(action):
multi_options.append('--recursive')
if(action.use_fetch):
multi_options.append('--mirror')
if(action.git_options):
multi_options += action.git_options.split(',')
try:
git.Repo.clone_from(action.node.url, action.path, multi_options=multi_options)

Expand Down
3 changes: 2 additions & 1 deletion gitlabber/gitlab_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

class GitlabTree:
def __init__(self, url, token, method, naming=None, archived=None, includes=[], excludes=[], in_file=None, concurrency=1, recursive=False, disable_progress=False,
include_shared=True, use_fetch=False, hide_token=False, user_projects=False, group_search=None):
include_shared=True, use_fetch=False, hide_token=False, user_projects=False, group_search=None, git_options=None):
self.includes = includes
self.excludes = excludes
self.url = url
Expand All @@ -39,6 +39,7 @@ def __init__(self, url, token, method, naming=None, archived=None, includes=[],
self.hide_token = hide_token
self.user_projects = user_projects
self.group_search = group_search
self.git_options = git_options

@staticmethod
def get_ca_path():
Expand Down
10 changes: 6 additions & 4 deletions tests/gitlab_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
from unittest import mock
from gitlabber import gitlab_tree
from gitlabber.method import CloneMethod

URL = "http://gitlab.my.com/"
TOKEN = "MOCK_TOKEN"
Expand All @@ -12,6 +13,7 @@
SUBGROUP_NAME = "subgroup"

PROJECT_URL = "http://gitlab.my.com/group/subgroup/project/project.git"
PROJECT_URL_WITH_TOKEN = "http://gitlab-token:xxx@gitlab.my.com/group/subgroup/project/project.git"
PROJECT_NAME = "project"

YAML_TEST_INPUT_FILE = "tests/test-input.yaml"
Expand All @@ -21,7 +23,7 @@


class MockNode:
def __init__(self, type, id, name, url, subgroups=mock.MagicMock(), projects=mock.MagicMock(), parent_id=None, archived=0, shared=False, group_search=None):
def __init__(self, type, id, name, url, subgroups=mock.MagicMock(), projects=mock.MagicMock(), parent_id=None, archived=0, shared=False, group_search=None, git_options=None):
self.type = type
self.id = id
self.name = name
Expand Down Expand Up @@ -117,10 +119,10 @@ def validate_tree(root):
validate_subgroup(root.children[0].children[0])
validate_project(root.children[0].children[0].children[0])

def create_test_gitlab(monkeypatch, includes=None, excludes=None, in_file=None):
def create_test_gitlab(monkeypatch, includes=None, excludes=None, in_file=None, hide_token=True, method=CloneMethod.SSH):
gl = gitlab_tree.GitlabTree(
URL, TOKEN, "ssh", "name", includes=includes, excludes=excludes, in_file=in_file)
projects = Listable(MockNode("project", 2, PROJECT_NAME, PROJECT_URL))
URL, TOKEN, "ssh", "name", includes=includes, excludes=excludes, in_file=in_file, hide_token=hide_token)
projects = Listable(MockNode("project", 2, PROJECT_NAME, PROJECT_URL if hide_token else PROJECT_URL_WITH_TOKEN))
groups = Listable(
MockNode("group", 2, GROUP_NAME, GROUP_URL, subgroups=Listable(
MockNode("subgroup", 3, SUBGROUP_NAME, SUBGROUP_URL, projects=projects)
Expand Down
10 changes: 5 additions & 5 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_args_version():
def test_args_logging(mock_tree, mock_log, mock_os, mock_sys, mock_logging):
args_mock = mock.Mock()
args_mock.return_value = Node(
type="test", name="test", version=None, verbose=True, include="", exclude="", url="test_url", token="test_token", method=CloneMethod.SSH, naming=FolderNaming.PATH, archived=ArchivedResults.INCLUDE, file=None, concurrency=1, recursive=False, disble_progress=True, print=None, dest=".", include_shared=True, use_fetch=None, hide_token=None, user_projects=None, group_search=None)
type="test", name="test", version=None, verbose=True, include="", exclude="", url="test_url", token="test_token", method=CloneMethod.SSH, naming=FolderNaming.PATH, archived=ArchivedResults.INCLUDE, file=None, concurrency=1, recursive=False, disble_progress=True, print=None, dest=".", include_shared=True, use_fetch=None, hide_token=None, user_projects=None, group_search=None, git_options=None)
cli.parse_args = args_mock

mock_streamhandler = mock.Mock()
Expand All @@ -57,7 +57,7 @@ def test_args_include(mock_tree):
exc_groups = "/exc**,/exc**"
args_mock = mock.Mock()
args_mock.return_value = Node(
type="test", name="test", version=None, debug=None, include=inc_groups, exclude=exc_groups, url="test_url", token="test_token", method=CloneMethod.SSH, naming=FolderNaming.NAME, archived=ArchivedResults.INCLUDE, file=None, concurrency=1, recursive=False, disble_progress=True, print=None, dest=".", use_fetch=None, hide_token=None, user_projects=None, group_search=None)
type="test", name="test", version=None, debug=None, include=inc_groups, exclude=exc_groups, url="test_url", token="test_token", method=CloneMethod.SSH, naming=FolderNaming.NAME, archived=ArchivedResults.INCLUDE, file=None, concurrency=1, recursive=False, disble_progress=True, print=None, dest=".", use_fetch=None, hide_token=None, user_projects=None, group_search=None, git_options=None)
cli.parse_args = args_mock

split_mock = mock.Mock()
Expand All @@ -73,7 +73,7 @@ def test_args_include(mock_tree):
def test_args_include(mock_tree):
args_mock = mock.Mock()
args_mock.return_value = Node(
type="test", name="test", version=None, verbose=None, include="", exclude="", url="test_url", token="test_token", method=CloneMethod.SSH, naming=FolderNaming.NAME, archived=ArchivedResults.INCLUDE, file=None, concurrency=1, recursive=False, disble_progress=True, print=True, dest=".", print_format=PrintFormat.YAML, include_shared=True, use_fetch=None, hide_token=None, user_projects=None, group_search=None)
type="test", name="test", version=None, verbose=None, include="", exclude="", url="test_url", token="test_token", method=CloneMethod.SSH, naming=FolderNaming.NAME, archived=ArchivedResults.INCLUDE, file=None, concurrency=1, recursive=False, disble_progress=True, print=True, dest=".", print_format=PrintFormat.YAML, include_shared=True, use_fetch=None, hide_token=None, user_projects=None, group_search=None, git_options=None)
cli.parse_args = args_mock

print_tree_mock = mock.Mock()
Expand Down Expand Up @@ -116,7 +116,7 @@ def test_missing_url(mock_tree):
def test_empty_tree(mock_tree):
args_mock = mock.Mock()
args_mock.return_value = Node(
type="test", name="test", version=None, verbose=None, include="", exclude="", url="test_url", token="test_token", method=CloneMethod.SSH, naming=FolderNaming.NAME, archived=ArchivedResults.INCLUDE, file=None, concurrency=1, recursive=False, disble_progress=True, print=True, dest=".", include_shared=True, use_fetch=None, hide_token=None, user_projects=None, group_search=None)
type="test", name="test", version=None, verbose=None, include="", exclude="", url="test_url", token="test_token", method=CloneMethod.SSH, naming=FolderNaming.NAME, archived=ArchivedResults.INCLUDE, file=None, concurrency=1, recursive=False, disble_progress=True, print=True, dest=".", include_shared=True, use_fetch=None, hide_token=None, user_projects=None, group_search=None, git_options=None)
cli.parse_args = args_mock

with pytest.raises(SystemExit):
Expand All @@ -127,7 +127,7 @@ def test_empty_tree(mock_tree):
def test_missing_dest(mock_tree, capsys):
args_mock = mock.Mock()
args_mock.return_value = Node(
type="test", name="test", version=None, verbose=None, include="", exclude="", url="test_url", token="test_token", method=CloneMethod.SSH, naming=FolderNaming.NAME, archived=ArchivedResults.INCLUDE, file=None, concurrency=1, recursive=False, disble_progress=True, print=False, dest=None, group_search=None)
type="test", name="test", version=None, verbose=None, include="", exclude="", url="test_url", token="test_token", method=CloneMethod.SSH, naming=FolderNaming.NAME, archived=ArchivedResults.INCLUDE, file=None, concurrency=1, recursive=False, disble_progress=True, print=False, dest=None, group_search=None, git_options=None)
cli.parse_args = args_mock
mock_tree.return_value.is_empty = mock.Mock(return_value=False)

Expand Down
38 changes: 38 additions & 0 deletions tests/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,20 @@ def test_pull_repo_exception(mock_git):

mock_git.Repo.assert_called_once_with("dummy_dir")
repo_instance.remotes.origin.pull.assert_called_once()

@mock.patch('gitlabber.git.git')
def test_clone_repo_exception(mock_git):
mock_repo = mock.Mock()
mock_git.Repo = mock_repo
git.is_git_repo = mock.MagicMock(return_value=False)

repo_instance = mock_git.Repo.return_value
repo_instance.clone_from.side_effect=Exception('clone test exception')

git.clone_or_pull_project(
GitAction(Node(type="project", name="dummy_url", url="dummy_url"), "dummy_dir"))
mock_git.Repo.clone_from.assert_called_once_with('dummy_url', 'dummy_dir', multi_options=[])
mock_git.Repo.clone_from.assert_called_once()

@mock.patch('gitlabber.git.git')
def test_pull_repo_interrupt(mock_git):
Expand Down Expand Up @@ -144,3 +158,27 @@ def test_clone_repo_interrupt(mock_git):
Node(type="project", name="dummy_url", url="dummy_url"), "dummy_dir"))

mock_git.Repo.clone_from.assert_called_once_with("dummy_url", "dummy_dir", multi_options=[])


@mock.patch('gitlabber.git.git')
def test_clone_repo_options_many_options(mock_git):
mock_repo = mock.Mock()
mock_git.Repo = mock_repo
git.is_git_repo = mock.MagicMock(return_value=False)

git.clone_or_pull_project(
GitAction(Node(type="project", name="dummy_url", url="dummy_url"), "dummy_dir", git_options="--opt1=1,--opt2=2"))

mock_git.Repo.clone_from.assert_called_once_with("dummy_url", "dummy_dir", multi_options=['--opt1=1','--opt2=2'])


@mock.patch('gitlabber.git.git')
def test_clone_repo_options_with_recursive(mock_git):
mock_repo = mock.Mock()
mock_git.Repo = mock_repo
git.is_git_repo = mock.MagicMock(return_value=False)

git.clone_or_pull_project(
GitAction(Node(type="project", name="dummy_url", url="dummy_url"), "dummy_dir", recursive=True, git_options="--opt1=1,--opt2=2"))

mock_git.Repo.clone_from.assert_called_once_with("dummy_url", "dummy_dir", multi_options=['--recursive','--opt1=1','--opt2=2'])
10 changes: 9 additions & 1 deletion tests/test_gitlab_tree.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

from gitlabber.method import CloneMethod
import tests.gitlab_test_utils as gitlab_util
import tests.io_test_util as output_util
from gitlabber.archive import ArchivedResults
Expand Down Expand Up @@ -164,4 +165,11 @@ def test_shared_excluded(monkeypatch):
assert len(gl.root.children) == 1
assert len(gl.root.children[0].children) == 1

assert "project" in gl.root.children[0].children[0].name
assert "project" in gl.root.children[0].children[0].name


def test_hide_token_from_project_url(monkeypatch):
gl = gitlab_util.create_test_gitlab(monkeypatch, hide_token=True, method=CloneMethod.HTTP)
gl.load_tree()
gl.print_tree()
assert 'gitlab-token:xxx@' not in gl.root.children[0].children[0].children[0].url