GitHubLab.py 19 KB
Newer Older
1
2
#!/usr/bin/python3
# coding: utf8
3

4
# Copyright (c) 2017-present OW2 http://www.ow2.org
Martin Hamant's avatar
Martin Hamant committed
5
#
6
7
8
9
10
11
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
Martin Hamant's avatar
Martin Hamant committed
12
#
13
14
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
Martin Hamant's avatar
Martin Hamant committed
15
#
16
17
18
19
20
21
22
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
Martin Hamant's avatar
Martin Hamant committed
23

24
from . import config
25
import requests
26
import pprint
27
import logging
28
import json
29
pp = pprint.PrettyPrinter()
Martin Hamant's avatar
Martin Hamant committed
30
logger = logging.getLogger(__name__)
31

32

33
34
35
def red(text):
    return('\x1b[1;31;40m' + text + '\x1b[0m')

36

Martin Hamant's avatar
Martin Hamant committed
37
# authentication against gitlab
38
gitlabHeaders = {'private-token':  config.globalCfg['gitlabApiKey']}
39
40
41

sessionGihubApi = requests.Session()
if 'gitHubApiToken' in config.globalCfg:
42
43
    sessionGihubApi.headers.update(
        {'Authorization': 'token {}'.format(config.globalCfg['gitHubApiToken'])})
44

45
# the cache is shared over all functions
46
47
cacheGroup = {}

48
# the orphan file
49
orphanFilePath = f'{config.base_dir}/potentialOrphans.json'
Martin Hamant's avatar
Martin Hamant committed
50

51
52
53
try:
    with open(orphanFilePath, 'w') as orphanFile:
        json.dump({'GitLabOrphan': {}}, orphanFile)
54
except OSError as e:
Martin Hamant's avatar
Martin Hamant committed
55
    logger.error(f'cannot create file: {e}')
56

Martin Hamant's avatar
Martin Hamant committed
57

Martin Hamant's avatar
Martin Hamant committed
58
class GitlabGroupError(Exception):
59
    def __init__(self, message):
Martin Hamant's avatar
Martin Hamant committed
60
        self.message = message
61

62

Martin Hamant's avatar
Martin Hamant committed
63
class GithubError(Exception):
64
    def __init__(self, message):
65
        self.message = message
Martin Hamant's avatar
Martin Hamant committed
66

67

Martin Hamant's avatar
Martin Hamant committed
68
def record_orphan(entry):
69
70
71
72
73
74
75
76
77
    """
    entry : a dict of orphans to record as :
    {'proactive/documentation': 'https://gitlab.ow2.org/api/v4/projects/383',
     'proactive/scheduling': 'https://gitlab.ow2.org/api/v4/projects/382'}

    The output is a JSON file
    the filename is hardcoded in orphanFilePath
    """
    if type(entry) is not dict:
78
79
        return False
    try:
80
        with open(orphanFilePath, 'r+') as orphanFile:
81
            _j = json.load(orphanFile)
82
83
84
85
            orphanFile.seek(0)
            _j['GitLabOrphan'].update(entry)
            json.dump(_j, orphanFile)
    except OSError as e:
Martin Hamant's avatar
Martin Hamant committed
86
        logger.error('cannot update file: {}'.format(e))
87
    except json.decoder.JSONDecodeError:
Martin Hamant's avatar
Martin Hamant committed
88
        logger.error("JSON decode error")
89

90

Martin Hamant's avatar
Martin Hamant committed
91
def cache_gitlab_group(namespace):
92
    """return True when the cache has been updated sucessfully"""
Martin Hamant's avatar
cleanup    
Martin Hamant committed
93

Martin Hamant's avatar
Martin Hamant committed
94
    def get_meta(namespace):
95
96
97
98
99
100
        try:
            gitlabGroupMeta = requests.get(
                config.globalCfg['gitlabApiUrl'] + '/groups/{}?simple=1&with_projects=0'.format(namespace), headers=gitlabHeaders)
            gitlabGroupMeta.raise_for_status()

            cacheGroup['meta'] = gitlabGroupMeta.json()
101
            return True
102
        except requests.exceptions.HTTPError as e:
Martin Hamant's avatar
Martin Hamant committed
103
            logger.error("There was a problem in getting group's metadata : {}".format(e))
104
105
            cacheGroup.clear()
            return False
106

Martin Hamant's avatar
Martin Hamant committed
107
    def get_projects(namespace):
108
109
110
111
112
113
114
115
116
117
118
119
120
        try:
            gitlabGroup = requests.get(
                config.globalCfg['gitlabApiUrl'] + '/groups/{}/projects?simple=1&per_page=100'.format(namespace), headers=gitlabHeaders)
            gitlabGroup.raise_for_status()
            cacheGroup['projects'] = []
            # items from first page
            cacheGroup['projects'].extend([p for p in gitlabGroup.json()])
            while 'next' in gitlabGroup.links:
                # get next page until there is one
                gitlabGroup = requests.get(
                    gitlabGroup.links['next']['url'], headers=gitlabHeaders)
                gitlabGroup.raise_for_status()
                cacheGroup['projects'].extend([p for p in gitlabGroup.json()])
121
            return True
122
        except requests.exceptions.HTTPError as e:
Martin Hamant's avatar
Martin Hamant committed
123
            logger.error("There was a problem in getting group's projects : {}".format(e))
124
125
            cacheGroup.clear()
            return False
126

Martin Hamant's avatar
Martin Hamant committed
127
    def do_update(namespace):
Martin Hamant's avatar
Martin Hamant committed
128
        logger.debug(f'updating group cache for {namespace}')
129
130
        cacheGroup.clear()
        # first we collect the group metadata only
Martin Hamant's avatar
Martin Hamant committed
131
        if not get_meta(namespace):
132
133
            return False
        # then collect group's projects names
Martin Hamant's avatar
Martin Hamant committed
134
        if not get_projects(namespace):
135
            return False
136
        return True
Martin Hamant's avatar
Martin Hamant committed
137

Martin Hamant's avatar
Martin Hamant committed
138
    if 'meta' in cacheGroup:
139
140
141
142
143
144
145
146
147
        # GitLab group's 'path' (in json response) is always lowercase. However, the user maintained YML source
        # definition can have dstgroup with uppercase chars.
        # In addition, gitlab isn't sensitive to the case of groups, ie:
        # (API) https://gitlab.ow2.org/api/v4/groups/PRELUDE is the same as https://gitlab.ow2.org/api/v4/groups/prelude
        # and :
        # (Web) https://gitlab.ow2.org/PRELUDE is redirected to https://gitlab.ow2.org/prelude
        # so we do all the compute in lowercase

        if cacheGroup['meta']['path'].lower() == namespace.lower():
Martin Hamant's avatar
Martin Hamant committed
148
149
150
            # cache is already populated
            return True
        else:
151
            # new project : update needed
Martin Hamant's avatar
Martin Hamant committed
152
            return do_update(namespace)
Martin Hamant's avatar
Martin Hamant committed
153
154
    else:
        # cache miss this group, updating
Martin Hamant's avatar
Martin Hamant committed
155
        return do_update(namespace)
Martin Hamant's avatar
Martin Hamant committed
156
157


Martin Hamant's avatar
Martin Hamant committed
158
def add_project_to_gitlab(namespace, repoName, repoUrl, issuesEnabledFor=False):
Martin Hamant's avatar
Martin Hamant committed
159
160
161
162
163
    """
    Add a mirror project to an *existing* group.

    The destination namespace should exists beforehand
    """
Martin Hamant's avatar
Martin Hamant committed
164
    logger.debug("invoke add project {}/{} ...".format(namespace, repoName))
165
166

    # processing only if the cache is populated with the group data
Martin Hamant's avatar
Martin Hamant committed
167
    if cache_gitlab_group(namespace):
168
169
170

        # we don't process if the project already exists in gitlab
        for project in cacheGroup['projects']:
171
            if project['name'] == repoName:
Martin Hamant's avatar
Martin Hamant committed
172
173
                logger.debug('project "{}" already exists in GitLab group "{}"'.format(
                    repoName, namespace))
174
175
176
                return False
        # unsure if the group is intended for mirroring
        # We don't want to mix up human maintained repo with mirrored ones
177
        if(cacheGroup['meta']['description'] == 'type: mirror'):
Martin Hamant's avatar
Martin Hamant committed
178
179
            logger.info('would create ' + namespace + '/'
                        + repoName + ' and description ' + repoUrl)
180

181
            payload = {
182
                'namespace_id': cacheGroup['meta']['id'],
183
184
                'name': repoName,
                'path': repoName,
185
186
187
188
                'description': 'origin: {}'.format(repoUrl),
                'shared_runners_enabled': False,
                'lfs_enabled': False,
                'request_access_enabled': False,
Martin Hamant's avatar
Martin Hamant committed
189
190
                'builds_access_level': 'disabled',
                'wiki_access_level': 'disabled',
Martin Hamant's avatar
Martin Hamant committed
191
                # doesn't work yet in GitLab API
192
193
                # 'issues_access_level': 'private' if issuesEnabled else 'disabled',
                'issues_enabled': False,
Martin Hamant's avatar
Martin Hamant committed
194
195
196
197
                'snippets_access_level': 'disabled',
                # doesn't seem implemented yet
                # 'merge_requests_access_level': 'disabled',
                'merge_requests_enabled': False,
Martin Hamant's avatar
Martin Hamant committed
198
199
200
201
                # not implemented in GitLab yet
                # 'analytics_access_level': 'disabled',
                # not implemented in GitLab yet
                # 'operations_access_level': 'disabled',
202
                'visibility': 'public'
203
204
            }

Martin Hamant's avatar
Martin Hamant committed
205
            if config.args.dryrun:
Martin Hamant's avatar
Martin Hamant committed
206
207
                logger.info('dryrun. The following actions would be made:')
                logger.info('GitLab POST payload {}'.format(payload))
Martin Hamant's avatar
Martin Hamant committed
208
            else:
Martin Hamant's avatar
Martin Hamant committed
209
                logger.info("creating project {}/{} ...".format(namespace, repoName))
Martin Hamant's avatar
Martin Hamant committed
210
211
                newrepo = requests.post(
                    config.globalCfg['gitlabApiUrl'] + '/projects', params=payload, headers=gitlabHeaders)
Martin Hamant's avatar
Martin Hamant committed
212

Martin Hamant's avatar
Martin Hamant committed
213
                if not newrepo.status_code == 201:  #  201 is "created" in GitLab API
Martin Hamant's avatar
Martin Hamant committed
214
215
216
217
                    logger.debug(pp.pformat(newrepo.json()))
                    logger.error('Error from GitLab (repo "{}/{}"): {}'.format(namespace,
                                                                               repoName, newrepo.json()['message']))
                    logger.error(
Martin Hamant's avatar
Martin Hamant committed
218
                        'in case of error \"namespace is not valid\" check if the gitlab user is a member of the target group')
Martin Hamant's avatar
Martin Hamant committed
219
220

                # tweak issue tracker permissions if needed
Martin Hamant's avatar
Martin Hamant committed
221
                if issuesEnabledFor and (repoName in issuesEnabledFor):
Martin Hamant's avatar
Martin Hamant committed
222
223
224
225
226
                    # this repo should have issue tracker enabled
                    tweakrepo = requests.put(
                        config.globalCfg['gitlabApiUrl'] + f"/projects/{newrepo.json()['id']}",
                        params={'issues_access_level': 'private'}, headers=gitlabHeaders)
                    if not tweakrepo.status_code == 200:
Martin Hamant's avatar
Martin Hamant committed
227
228
                        logger.error('Error from GitLab (repo "{}/{}"): {}'.format(namespace,
                                                                                   repoName, tweakrepo.json()['message']))
229
        else:
Martin Hamant's avatar
Martin Hamant committed
230
            raise GitlabGroupError(
Martin Hamant's avatar
Martin Hamant committed
231
                'group {} description is not configured for mirroring'.format(namespace))
232
    else:
Martin Hamant's avatar
Martin Hamant committed
233
        logger.error("Can't update group cache for {}".format(namespace))
234
235
236


# print(repos.text)
Martin Hamant's avatar
Martin Hamant committed
237
def is_valid_source(repo, validRepoTypes):
238
    """
239
240
    Determine if the specified repo has special attributes against input list.

241
242
243
    if the input list validRepoTypes is ommited and any of the special attributes
    of ghRepoTypesList is True in the GitHub the repository, then that repo is not considered
    """
244
245
246
247
248
    # those are all the possible repo types in GitHub
    ghRepoTypesList = ['archived', 'fork', 'private']

    # from the repo , we extract value for those keys, as a dict
    # ie. { 'archived': False, 'fork': False, 'private': True }
Martin Hamant's avatar
Martin Hamant committed
249
    repoTypes = {k: v for k, v in repo.items() if k in ghRepoTypesList}
250
251

    # list of type that are found True in repo
Martin Hamant's avatar
Martin Hamant committed
252
    trueTypes = [k for k, v in repoTypes.items() if v]
253
254
255
256
257
258
259
    # ie. [ 'private' ]

    if trueTypes:
        # this repo has special attribute type in github (not regular "source")
        if validRepoTypes:
            # config is provided
            if list(set(trueTypes) & set(validRepoTypes)):
Martin Hamant's avatar
Martin Hamant committed
260
                # will sync if there is intersection between the actual repo and config
261
262
263
264
                return True
            else:
                return False
        else:
Martin Hamant's avatar
Martin Hamant committed
265
            # special repo, no config provided : will not sync
266
            return False
267
    else:
268
        # regular repo : will sync
269
270
        return True

271

Martin Hamant's avatar
Martin Hamant committed
272
def print_github_limits(headers):
Martin Hamant's avatar
Martin Hamant committed
273
    if 'X-RateLimit-Remaining' in headers:
274
        if int(headers['X-RateLimit-Remaining']) < 5:
Martin Hamant's avatar
Martin Hamant committed
275
            logger.warning('warning ! reaching GitHub API limits')
276

Martin Hamant's avatar
Martin Hamant committed
277
        logger.warning(
278
279
            'GitHub\'s X-RateLimit-Remaining : {}'.format(headers['X-RateLimit-Remaining']))

280

Martin Hamant's avatar
Martin Hamant committed
281
def prepare_repos(doGetSize=False, **itemSettings):
Martin Hamant's avatar
Martin Hamant committed
282
283
284
    # the dest group is the one read from config if specified otherwise we take
    # github's namespace

Martin Hamant's avatar
Martin Hamant committed
285
    # the destination GitLab group could be specified by user in YML.
286
    # If not, fallback to the github org name
Martin Hamant's avatar
Martin Hamant committed
287
    dstGroup = itemSettings['glDestGroup'] if itemSettings['glDestGroup'] else itemSettings['ghNs']
288
289

    if itemSettings['ghRepo']:
Martin Hamant's avatar
Martin Hamant committed
290
291
        # get a single repo, providing a full name aka namespace/reponame
        # warning : a single repo can be any of type : fork, archive
292
        # Endpoint is like https://api.github.com/repos/shumatech/BOSSA
Martin Hamant's avatar
Martin Hamant committed
293
        logger.info("single repo sync")
294
        # TODO factorize github api calls some day
Martin Hamant's avatar
Martin Hamant committed
295
        _ghRepo = '{}/{}'.format(itemSettings['ghNs'], itemSettings['ghRepo'])
296
        repo = sessionGihubApi.get(
Martin Hamant's avatar
Martin Hamant committed
297
            "https://api.github.com/repos/{}".format(_ghRepo), timeout=5)
298
        if repo.status_code == requests.codes.ok:
Martin Hamant's avatar
Martin Hamant committed
299
            print_github_limits(repo.headers)
300
301

            repo = repo.json()
302

303
304
            if doGetSize:
                return repo['size']
Martin Hamant's avatar
Martin Hamant committed
305
            else:
Martin Hamant's avatar
Martin Hamant committed
306
307
                add_project_to_gitlab(dstGroup, repo['name'], repo['clone_url'],
                                      issuesEnabledFor=itemSettings['issuesEnabledFor'])
308
        elif repo.status_code == requests.codes.not_found:
Martin Hamant's avatar
Martin Hamant committed
309
310
            logger.warning(
                'GitHub repo {} has not been found (404)\nReason: {}'.format(_ghRepo, repo.text))
311
312
313

            # Is it only the repo that got deleted or the whole github org ?
            _ghOrg = sessionGihubApi.get(
Martin Hamant's avatar
Martin Hamant committed
314
                "https://api.github.com/users/{}".format(itemSettings['ghNs']), timeout=5)
315
316
            if _ghOrg.status_code == requests.codes.not_found:
                # the whole org doesn't exist
Martin Hamant's avatar
Martin Hamant committed
317
                raise(GithubError(
Martin Hamant's avatar
Martin Hamant committed
318
                    "The GitHub org {} not found (deleted?)".format(itemSettings['ghNs'])))
319
            elif _ghOrg.status_code == requests.codes.ok:
320
                # only the repo has been deleted
Martin Hamant's avatar
Martin Hamant committed
321
                raise(GithubError("GitHub org '{}' exists but repo '{}' has not been found from it (deleted?) (404)\nReason: {}".format(
Martin Hamant's avatar
Martin Hamant committed
322
                    itemSettings['ghNs'], _ghRepo, repo.text)))
323
324
325

                # TODO : Do something to delete the repos from GitLab, if exists
                # TODO : Because if it exists in GitLab but no more in GitHub, it means its been deleted
326
        else:
Martin Hamant's avatar
Martin Hamant committed
327
            logger.error('unhandled github HTTP return code')
328
329
330

    else:
        # a whole namespace
331
        repos = sessionGihubApi.get(
Martin Hamant's avatar
Martin Hamant committed
332
            "https://api.github.com/users/{}/repos?per_page=100".format(itemSettings['ghNs']), timeout=5)
333
        if repos.status_code == requests.codes.ok:
Martin Hamant's avatar
Martin Hamant committed
334
            print_github_limits(repos.headers)
335
336

            ghRepos = {}
337
            reposSize = 0
338
339
340
341

            # there might be several pages but in any case, we retrieve the item of the first page returned

            for repo in repos.json():
Martin Hamant's avatar
Martin Hamant committed
342
                if is_valid_source(repo, itemSettings['incRepoType']):
343
                    ghRepos[repo['name']] = repo['clone_url']
344
                    if not itemSettings['ghRepoOnlyList'] or (itemSettings['ghRepoOnlyList'] and repo['name'] in itemSettings['ghRepoOnlyList']):
345
                        reposSize += repo['size']
346

347
348
            # next pages if any. Takes advantage of requests's links dict.
            while 'next' in repos.links:
349
350
                repos = sessionGihubApi.get(
                    repos.links['next']['url'], timeout=5)
351

352
                if repos.status_code != requests.codes.ok:
Martin Hamant's avatar
Martin Hamant committed
353
                    logger.critical(f"can't get next page from GitHub API : {repos.text}")
354
                    exit()
355

356
                for repo in repos.json():
Martin Hamant's avatar
Martin Hamant committed
357
                    if is_valid_source(repo, itemSettings['incRepoType']):
358
                        ghRepos[repo['name']] = repo['clone_url']
359
                        if not itemSettings['ghRepoOnlyList'] or (itemSettings['ghRepoOnlyList'] and repo['name'] in itemSettings['ghRepoOnlyList']):
360
                            reposSize += repo['size']
361

Martin Hamant's avatar
Martin Hamant committed
362
            logger.info("Considering {} repositories in this github org".format(
Martin Hamant's avatar
Martin Hamant committed
363
                len(ghRepos)))
364
            if itemSettings['ghRepoOnlyList']:
Martin Hamant's avatar
Martin Hamant committed
365
                logger.info("only {} of them will be sync'ed".format(
366
                    len(itemSettings['ghRepoOnlyList'])))
367

Martin Hamant's avatar
Martin Hamant committed
368
            if cache_gitlab_group(dstGroup):
Martin Hamant's avatar
Martin Hamant committed
369
                pInGitLab = [p['name'] for p in cacheGroup['projects']]
370
371
372
                # we sort out if the configured repos are still in GitHub ('only' list)
                if itemSettings['ghRepoOnlyList']:
                    if set(itemSettings['ghRepoOnlyList']) <= set(pInGitLab):
Martin Hamant's avatar
Martin Hamant committed
373
                        logger.info("all configured repos are in Github")
374

375
376
                    _orphans = set(pInGitLab) - set(itemSettings['ghRepoOnlyList'])
                    if _orphans:
Martin Hamant's avatar
Martin Hamant committed
377
                        logger.warning(
Martin Hamant's avatar
Martin Hamant committed
378
379
380
                            'Warning, "ONLY" list:  some of the existing GitLab repos in this group'
                            ' are not specified in the source configuration. See orphan log.')
                        _pApiUrls = {p['path_with_namespace']: '{}/projects/{}'.format(config.globalCfg['gitlabApiUrl'], p['id']) for p in cacheGroup['projects']
381
                                     for orphan in _orphans if orphan == p['name'].lower()}
Martin Hamant's avatar
Martin Hamant committed
382
                        record_orphan(_pApiUrls)
383
384

                else:
385
386
387
388
                    # If the whole org is configured for sync
                    # we print out repos that are in GitLab but not in GitHub (anymore)
                    _reposNotInGH = list(set(pInGitLab) - set(ghRepos.keys()))
                    if _reposNotInGH:
Martin Hamant's avatar
Martin Hamant committed
389
                        logger.warning(
Martin Hamant's avatar
Martin Hamant committed
390
                            "Warning: some of the GitLab repos doesn't exist or have special"
391
                            "status in GitHub org (archive, fork). See orphan log.")
Martin Hamant's avatar
Martin Hamant committed
392
                        logger.warning("Consider deleting them from GitLab")
Martin Hamant's avatar
Martin Hamant committed
393
                        _pApiUrls = {p['path_with_namespace']: '{}/projects/{}'.format(config.globalCfg['gitlabApiUrl'], p['id']) for p in cacheGroup['projects']
394
                                     for orphan in _reposNotInGH if orphan == p['name']}
Martin Hamant's avatar
Martin Hamant committed
395
                        record_orphan(_pApiUrls)
396

397
            for repoName, repoUrl in ghRepos.items():
398
399
                if doGetSize:
                    return reposSize
400
                # filtering against the 'only' list
401
402
403
                # we add the project to gitlab if either:
                # - there is no 'only' list at all (means we add all repos)
                # - if there is a 'only' list AND the github repo match the one we want to sync
404
                if not itemSettings['ghRepoOnlyList'] or (itemSettings['ghRepoOnlyList'] and repoName in itemSettings['ghRepoOnlyList']):
Martin Hamant's avatar
Martin Hamant committed
405
406
                    add_project_to_gitlab(dstGroup, repoName, repoUrl,
                                          issuesEnabledFor=itemSettings['issuesEnabledFor'])
407
408
409
410
        elif repos.status_code == requests.codes.not_found:
            # This is the case where no repos where found in the GitHub org
            # It could be a typo in the source definition, or, that the GitHub Org has been deleted

Martin Hamant's avatar
Martin Hamant committed
411
            if cache_gitlab_group(dstGroup):
412
                # GitLab group exists
Martin Hamant's avatar
Martin Hamant committed
413
                logger.warning("existing GitLab group '{}' (id: {}) doesn't have a valid matching GitHub org source (currently {})".format(
Martin Hamant's avatar
Martin Hamant committed
414
                    cacheGroup['meta']['name'], cacheGroup['meta']['id'], itemSettings['ghNs']))
415

Martin Hamant's avatar
Martin Hamant committed
416
            raise(GithubError(
Martin Hamant's avatar
Martin Hamant committed
417
                'No repos found in GitHub org {} (404)\nReason: {}'.format(itemSettings['ghNs'], repos.text)))
418
        else:
Martin Hamant's avatar
Martin Hamant committed
419
            logger.warning('unhandled github HTTP return code')
420

421

422
def prepare_repos_from_urls(urls, dstGroup, issuesEnabledProjects):
423
424
    for url in urls:
        repoName = url.split('/').pop().split('.')[0]
Martin Hamant's avatar
Martin Hamant committed
425
        add_project_to_gitlab(dstGroup, repoName, url, issuesEnabledFor=issuesEnabledProjects)