##// END OF EJS Templates
Added support for repository located in subdirectories.
marcink -
r248:fb7f0661 default
parent child Browse files
Show More
@@ -1,67 +1,91
1 1 """Routes configuration
2 2
3 3 The more specific and detailed routes should be defined first so they
4 4 may take precedent over the more generic routes. For more information
5 5 refer to the routes manual at http://routes.groovie.org/docs/
6 6 """
7 7 from routes import Mapper
8 8
9 9 def make_map(config):
10 10 """Create, configure and return the routes Mapper"""
11 11 map = Mapper(directory=config['pylons.paths']['controllers'],
12 12 always_scan=config['debug'])
13 13 map.minimization = False
14 14 map.explicit = False
15 15
16 16 # The ErrorController route (handles 404/500 error pages); it should
17 17 # likely stay at the top, ensuring it can always be resolved
18 18 map.connect('/error/{action}', controller='error')
19 19 map.connect('/error/{action}/{id}', controller='error')
20 20
21 21 # CUSTOM ROUTES HERE
22 22 map.connect('hg_home', '/', controller='hg', action='index')
23 23
24 24
25 #REST controllers
26 map.resource('repo', 'repos', path_prefix='/_admin')
25 #REST routes
26 with map.submapper(path_prefix='/_admin', controller='repos') as m:
27 m.connect("repos", "/repos",
28 action="create", conditions=dict(method=["POST"]))
29 m.connect("repos", "/repos",
30 action="index", conditions=dict(method=["GET"]))
31 m.connect("formatted_repos", "/repos.{format}",
32 action="index",
33 conditions=dict(method=["GET"]))
34 m.connect("new_repo", "/repos/new",
35 action="new", conditions=dict(method=["GET"]))
36 m.connect("formatted_new_repo", "/repos/new.{format}",
37 action="new", conditions=dict(method=["GET"]))
38 m.connect("/repos/{id:.*}",
39 action="update", conditions=dict(method=["PUT"]))
40 m.connect("/repos/{id:.*}",
41 action="delete", conditions=dict(method=["DELETE"]))
42 m.connect("edit_repo", "/repos/{id:.*}/edit",
43 action="edit", conditions=dict(method=["GET"]))
44 m.connect("formatted_edit_repo", "/repos/{id:.*}.{format}/edit",
45 action="edit", conditions=dict(method=["GET"]))
46 m.connect("repo", "/repos/{id:.*}",
47 action="show", conditions=dict(method=["GET"]))
48 m.connect("formatted_repo", "/repos/{id:.*}.{format}",
49 action="show", conditions=dict(method=["GET"]))
50
27 51 map.resource('user', 'users', path_prefix='/_admin')
28 52 map.resource('permission', 'permissions', path_prefix='/_admin')
29 53
30 54 #ADMIN
31 55 with map.submapper(path_prefix='/_admin', controller='admin') as m:
32 56 m.connect('admin_home', '/', action='index')#main page
33 57 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
34 58 action='add_repo')
35 59
36 60 #FEEDS
37 map.connect('rss_feed_home', '/{repo_name}/feed/rss',
61 map.connect('rss_feed_home', '/{repo_name:.*}/feed/rss',
38 62 controller='feed', action='rss')
39 map.connect('atom_feed_home', '/{repo_name}/feed/atom',
63 map.connect('atom_feed_home', '/{repo_name:.*}/feed/atom',
40 64 controller='feed', action='atom')
41 65
42 66 map.connect('login_home', '/login', controller='login')
43 67 map.connect('logout_home', '/logout', controller='login', action='logout')
44 68
45 map.connect('changeset_home', '/{repo_name}/changeset/{revision}',
69 map.connect('changeset_home', '/{repo_name:.*}/changeset/{revision}',
46 70 controller='changeset', revision='tip')
47 map.connect('summary_home', '/{repo_name}/summary',
71 map.connect('summary_home', '/{repo_name:.*}/summary',
48 72 controller='summary')
49 map.connect('shortlog_home', '/{repo_name}/shortlog',
73 map.connect('shortlog_home', '/{repo_name:.*}/shortlog',
50 74 controller='shortlog')
51 map.connect('branches_home', '/{repo_name}/branches',
75 map.connect('branches_home', '/{repo_name:.*}/branches',
52 76 controller='branches')
53 map.connect('tags_home', '/{repo_name}/tags',
77 map.connect('tags_home', '/{repo_name:.*}/tags',
54 78 controller='tags')
55 map.connect('changelog_home', '/{repo_name}/changelog',
79 map.connect('changelog_home', '/{repo_name:.*}/changelog',
56 80 controller='changelog')
57 map.connect('files_home', '/{repo_name}/files/{revision}/{f_path:.*}',
81 map.connect('files_home', '/{repo_name:.*}/files/{revision}/{f_path:.*}',
58 82 controller='files', revision='tip', f_path='')
59 map.connect('files_diff_home', '/{repo_name}/diff/{f_path:.*}',
83 map.connect('files_diff_home', '/{repo_name:.*}/diff/{f_path:.*}',
60 84 controller='files', action='diff', revision='tip', f_path='')
61 map.connect('files_raw_home', '/{repo_name}/rawfile/{revision}/{f_path:.*}',
85 map.connect('files_raw_home', '/{repo_name:.*}/rawfile/{revision}/{f_path:.*}',
62 86 controller='files', action='rawfile', revision='tip', f_path='')
63 map.connect('files_annotate_home', '/{repo_name}/annotate/{revision}/{f_path:.*}',
87 map.connect('files_annotate_home', '/{repo_name:.*}/annotate/{revision}/{f_path:.*}',
64 88 controller='files', action='annotate', revision='tip', f_path='')
65 map.connect('files_archive_home', '/{repo_name}/archive/{revision}/{fileformat}',
89 map.connect('files_archive_home', '/{repo_name:.*}/archive/{revision}/{fileformat}',
66 90 controller='files', action='archivefile', revision='tip')
67 91 return map
@@ -1,100 +1,103
1 1 from pylons import request, response, session, tmpl_context as c, url, \
2 2 app_globals as g
3 3 from pylons.controllers.util import abort, redirect
4 4 from pylons_app.lib.auth import LoginRequired
5 from pylons.i18n.translation import _
6 from pylons_app.lib import helpers as h
5 7 from pylons_app.lib.base import BaseController, render
6 8 from pylons_app.lib.filters import clean_repo
7 9 from pylons_app.lib.utils import check_repo, invalidate_cache
8 10 from pylons_app.model.hg_model import HgModel
9 11 import logging
10 12 import os
11 13 import shutil
12 14 from operator import itemgetter
13 15 log = logging.getLogger(__name__)
14 16
15 17 class ReposController(BaseController):
16 18 """REST Controller styled on the Atom Publishing Protocol"""
17 19 # To properly map this controller, ensure your config/routing.py
18 20 # file has a resource setup:
19 21 # map.resource('repo', 'repos')
20 22 @LoginRequired()
21 23 def __before__(self):
22 24 c.admin_user = session.get('admin_user')
23 25 c.admin_username = session.get('admin_username')
24 26 super(ReposController, self).__before__()
25 27
26 28 def index(self, format='html'):
27 29 """GET /repos: All items in the collection"""
28 30 # url('repos')
29 31 cached_repo_list = HgModel().get_repos()
30 32 c.repos_list = sorted(cached_repo_list, key=itemgetter('name_sort'))
31 33 return render('admin/repos/repos.html')
32 34
33 35 def create(self):
34 36 """POST /repos: Create a new item"""
35 37 # url('repos')
36 38 name = request.POST.get('name')
37 39
38 40 try:
39 41 self._create_repo(name)
40 42 #clear our cached list for refresh with new repo
41 43 invalidate_cache('cached_repo_list')
44 h.flash(_('created repository %s') % name, category='success')
42 45 except Exception as e:
43 46 log.error(e)
44 47
45 48 return redirect('repos')
46 49
47 50 def _create_repo(self, repo_name):
48 51 repo_path = os.path.join(g.base_path, repo_name)
49 52 if check_repo(repo_name, g.base_path):
50 53 log.info('creating repo %s in %s', repo_name, repo_path)
51 54 from vcs.backends.hg import MercurialRepository
52 55 MercurialRepository(repo_path, create=True)
53 56
54 57
55 58 def new(self, format='html'):
56 59 """GET /repos/new: Form to create a new item"""
57 60 new_repo = request.GET.get('repo', '')
58 61 c.new_repo = clean_repo(new_repo)
59 62
60 63 return render('admin/repos/repo_add.html')
61 64
62 65 def update(self, id):
63 66 """PUT /repos/id: Update an existing item"""
64 67 # Forms posted to this method should contain a hidden field:
65 68 # <input type="hidden" name="_method" value="PUT" />
66 69 # Or using helpers:
67 70 # h.form(url('repo', id=ID),
68 71 # method='put')
69 72 # url('repo', id=ID)
70 73
71 74 def delete(self, id):
72 75 """DELETE /repos/id: Delete an existing item"""
73 76 # Forms posted to this method should contain a hidden field:
74 77 # <input type="hidden" name="_method" value="DELETE" />
75 78 # Or using helpers:
76 79 # h.form(url('repo', id=ID),
77 80 # method='delete')
78 81 # url('repo', id=ID)
79 82 from datetime import datetime
80 83 path = g.paths[0][1].replace('*', '')
81 84 rm_path = os.path.join(path, id)
82 85 log.info("Removing %s", rm_path)
83 86 shutil.move(os.path.join(rm_path, '.hg'), os.path.join(rm_path, 'rm__.hg'))
84 87 shutil.move(rm_path, os.path.join(path, 'rm__%s-%s' % (datetime.today(), id)))
85 88
86 89 #clear our cached list for refresh with new repo
87 90 invalidate_cache('cached_repo_list')
88
91 h.flash(_('deleted repository %s') % rm_path, category='success')
89 92 return redirect(url('repos'))
90 93
91 94
92 95 def show(self, id, format='html'):
93 96 """GET /repos/id: Show a specific item"""
94 97 # url('repo', id=ID)
95 98
96 99 def edit(self, id, format='html'):
97 100 """GET /repos/id/edit: Form to edit an existing item"""
98 101 # url('edit_repo', id=ID)
99 102 c.new_repo = id
100 103 return render('admin/repos/repo_edit.html')
@@ -1,136 +1,138
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 #
4 4 # Copyright (c) 2010 marcink. All rights reserved.
5 5 #
6 6 """
7 7 Created on 2010-04-28
8 8
9 9 @author: marcink
10 10 SimpleHG middleware for handling mercurial protocol request (push/clone etc.)
11 11 It's implemented with basic auth function
12 12 """
13 13 from datetime import datetime
14 14 from mercurial.hgweb import hgweb
15 15 from mercurial.hgweb.request import wsgiapplication
16 16 from paste.auth.basic import AuthBasicAuthenticator
17 17 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
18 18 from pylons_app.lib.auth import authfunc
19 19 from pylons_app.lib.utils import is_mercurial, make_ui, invalidate_cache
20 20 from pylons_app.model import meta
21 21 from pylons_app.model.db import UserLog, User
22 22 from webob.exc import HTTPNotFound
23 23 import logging
24 24 import os
25 25 log = logging.getLogger(__name__)
26 26
27 27 class SimpleHg(object):
28 28
29 29 def __init__(self, application, config):
30 30 self.application = application
31 31 self.config = config
32 32 #authenticate this mercurial request using
33 33 realm = '%s %s' % (config['hg_app_name'], 'mercurial repository')
34 34 self.authenticate = AuthBasicAuthenticator(realm, authfunc)
35 35
36 36 def __call__(self, environ, start_response):
37 37 if not is_mercurial(environ):
38 38 return self.application(environ, start_response)
39 39 else:
40 40 #===================================================================
41 41 # AUTHENTICATE THIS MERCURIAL REQUEST
42 42 #===================================================================
43 43 username = REMOTE_USER(environ)
44 44 if not username:
45 45 result = self.authenticate(environ)
46 46 if isinstance(result, str):
47 47 AUTH_TYPE.update(environ, 'basic')
48 48 REMOTE_USER.update(environ, result)
49 49 else:
50 50 return result.wsgi_application(environ, start_response)
51 51
52 52 try:
53 repo_name = environ['PATH_INFO'].split('/')[1]
54 except:
53 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
54 except Exception as e:
55 log.error(e)
55 56 return HTTPNotFound()(environ, start_response)
56 57
57 58 #since we wrap into hgweb, just reset the path
58 59 environ['PATH_INFO'] = '/'
59 60 self.baseui = make_ui(self.config['hg_app_repo_conf'])
60 61 self.basepath = self.baseui.configitems('paths')[0][1]\
61 62 .replace('*', '')
62 63 self.repo_path = os.path.join(self.basepath, repo_name)
63 64 try:
64 65 app = wsgiapplication(self.__make_app)
65 66 except Exception as e:
67 log.error(e)
66 68 return HTTPNotFound()(environ, start_response)
67 69 action = self.__get_action(environ)
68 70 #invalidate cache on push
69 71 if action == 'push':
70 72 self.__invalidate_cache(repo_name)
71 73
72 74 if action:
73 75 username = self.__get_environ_user(environ)
74 76 self.__log_user_action(username, action, repo_name)
75 77
76 78 return app(environ, start_response)
77 79
78 80 def __make_app(self):
79 81 hgserve = hgweb(self.repo_path)
80 82 return self.__load_web_settings(hgserve)
81 83
82 84 def __get_environ_user(self, environ):
83 85 return environ.get('REMOTE_USER')
84 86
85 87 def __get_action(self, environ):
86 88 """
87 89 Maps mercurial request commands into a pull or push command.
88 90 @param environ:
89 91 """
90 92 mapping = {
91 93 'changegroup': 'pull',
92 94 'changegroupsubset': 'pull',
93 95 'unbundle': 'push',
94 96 'stream_out': 'pull',
95 97 }
96 98 for qry in environ['QUERY_STRING'].split('&'):
97 99 if qry.startswith('cmd'):
98 100 cmd = qry.split('=')[-1]
99 101 if mapping.has_key(cmd):
100 102 return mapping[cmd]
101 103
102 104 def __log_user_action(self, username, action, repo):
103 105 sa = meta.Session
104 106 try:
105 107 user = sa.query(User).filter(User.username == username).one()
106 108 user_log = UserLog()
107 109 user_log.user_id = user.user_id
108 110 user_log.action = action
109 111 user_log.repository = repo.replace('/', '')
110 112 user_log.action_date = datetime.now()
111 113 sa.add(user_log)
112 114 sa.commit()
113 115 log.info('Adding user %s, action %s on %s',
114 116 username, action, repo)
115 117 except Exception as e:
116 118 sa.rollback()
117 119 log.error('could not log user action:%s', str(e))
118 120
119 121 def __invalidate_cache(self, repo_name):
120 122 """we know that some change was made to repositories and we should
121 123 invalidate the cache to see the changes right away but only for
122 124 push requests"""
123 125 invalidate_cache('cached_repo_list')
124 126 invalidate_cache('full_changelog', repo_name)
125 127
126 128
127 129 def __load_web_settings(self, hgserve):
128 130 repoui = make_ui(os.path.join(self.repo_path, '.hg', 'hgrc'), False)
129 131 #set the global ui for hgserve
130 132 hgserve.repo.ui = self.baseui
131 133
132 134 if repoui:
133 135 #set the repository based config
134 136 hgserve.repo.ui = repoui
135 137
136 138 return hgserve
@@ -1,144 +1,134
1 1 import os
2 2 import logging
3 3 from mercurial import ui, config, hg
4 4 from mercurial.error import RepoError
5 5 log = logging.getLogger(__name__)
6 6
7 7
8 8 def get_repo_slug(request):
9 path_info = request.environ.get('PATH_INFO')
10 uri_lst = path_info.split('/')
11 repo_name = uri_lst[1]
12 return repo_name
9 return request.environ['pylons.routes_dict'].get('repo_name')
13 10
14 11 def is_mercurial(environ):
15 12 """
16 13 Returns True if request's target is mercurial server - header
17 14 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
18 15 """
19 16 http_accept = environ.get('HTTP_ACCEPT')
20 17 if http_accept and http_accept.startswith('application/mercurial'):
21 18 return True
22 19 return False
23 20
24 21 def check_repo_dir(paths):
25 22 repos_path = paths[0][1].split('/')
26 23 if repos_path[-1] in ['*', '**']:
27 24 repos_path = repos_path[:-1]
28 25 if repos_path[0] != '/':
29 26 repos_path[0] = '/'
30 27 if not os.path.isdir(os.path.join(*repos_path)):
31 28 raise Exception('Not a valid repository in %s' % paths[0][1])
32 29
33 30 def check_repo(repo_name, base_path):
34 31
35 32 repo_path = os.path.join(base_path, repo_name)
36 33
37 34 try:
38 35 r = hg.repository(ui.ui(), repo_path)
39 36 hg.verify(r)
40 37 #here we hnow that repo exists it was verified
41 38 log.info('%s repo is already created', repo_name)
42 39 return False
43 40 #raise Exception('Repo exists')
44 41 except RepoError:
45 42 log.info('%s repo is free for creation', repo_name)
46 43 #it means that there is no valid repo there...
47 44 return True
48 45
49 46 def make_ui(path=None, checkpaths=True):
50 47 """
51 48 A funcion that will read python rc files and make an ui from read options
52 49
53 50 @param path: path to mercurial config file
54 51 """
55 52 if not path:
56 53 log.error('repos config path is empty !')
57 54
58 55 if not os.path.isfile(path):
59 56 log.warning('Unable to read config file %s' % path)
60 57 return False
61 58 #propagated from mercurial documentation
62 59 sections = [
63 60 'alias',
64 61 'auth',
65 62 'decode/encode',
66 63 'defaults',
67 64 'diff',
68 65 'email',
69 66 'extensions',
70 67 'format',
71 68 'merge-patterns',
72 69 'merge-tools',
73 70 'hooks',
74 71 'http_proxy',
75 72 'smtp',
76 73 'patch',
77 74 'paths',
78 75 'profiling',
79 76 'server',
80 77 'trusted',
81 78 'ui',
82 79 'web',
83 80 ]
84 81
85 82 baseui = ui.ui()
86 83 cfg = config.config()
87 84 cfg.read(path)
88 85 if checkpaths:check_repo_dir(cfg.items('paths'))
89 86
90 87 for section in sections:
91 88 for k, v in cfg.items(section):
92 89 baseui.setconfig(section, k, v)
93 90
94 91 return baseui
95 92
96 93 def invalidate_cache(name, *args):
97 94 """Invalidates given name cache"""
98 95
99 96 from beaker.cache import region_invalidate
100 97 log.info('INVALIDATING CACHE FOR %s', name)
101 98
102 99 """propagate our arguments to make sure invalidation works. First
103 100 argument has to be the name of cached func name give to cache decorator
104 101 without that the invalidation would not work"""
105 102 tmp = [name]
106 103 tmp.extend(args)
107 104 args = tuple(tmp)
108 105
109 106 if name == 'cached_repo_list':
110 107 from pylons_app.model.hg_model import _get_repos_cached
111 108 region_invalidate(_get_repos_cached, None, *args)
112 109
113 110 if name == 'full_changelog':
114 111 from pylons_app.model.hg_model import _full_changelog_cached
115 112 region_invalidate(_full_changelog_cached, None, *args)
116 113
117 114 from vcs.backends.base import BaseChangeset
118 115 from vcs.utils.lazy import LazyProperty
119 116 class EmptyChangeset(BaseChangeset):
120 117
121 118 revision = -1
122 119
123 120 @LazyProperty
124 121 def raw_id(self):
125 122 """
126 123 Returns raw string identifing this changeset, useful for web
127 124 representation.
128 125 """
129 126 return '0' * 12
130 127
131 128
132 129 def repo2db_mapper():
133 130 """
134 put !
131 scann all dirs for .hgdbid
132 if some dir doesn't have one generate one.
135 133 """
136 pass
137 #scann all dirs for .hgdbid
138 #if some dir doesn't have one generate one.
139 #
140
141
142
143
144
134 pass No newline at end of file
@@ -1,120 +1,126
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 #
4 4 # Copyright (c) 2010 marcink. All rights reserved.
5 5 #
6 6 '''
7 7 Created on Apr 9, 2010
8 8
9 9 @author: marcink
10 10 '''
11 11
12 12 from beaker.cache import cache_region
13 13 from mercurial import ui
14 14 from mercurial.hgweb.hgwebdir_mod import findrepos
15 15 from pylons import app_globals as g
16 16 from vcs.exceptions import RepositoryError, VCSError
17 17 import logging
18 18 import os
19 19 import sys
20 20 log = logging.getLogger(__name__)
21 21
22 22 try:
23 23 from vcs.backends.hg import MercurialRepository
24 24 except ImportError:
25 25 sys.stderr.write('You have to import vcs module')
26 26 raise Exception('Unable to import vcs')
27 27
28 28
29 29 @cache_region('long_term', 'cached_repo_list')
30 30 def _get_repos_cached():
31 31 """
32 32 return cached dict with repos
33 33 """
34 34 return HgModel.repo_scan(g.paths[0][0], g.paths[0][1], g.baseui)
35 35
36 36 @cache_region('long_term', 'full_changelog')
37 37 def _full_changelog_cached(repo_name):
38 38 log.info('getting full changelog for %s', repo_name)
39 39 return list(reversed(list(HgModel().get_repo(repo_name))))
40 40
41 41 class HgModel(object):
42 42 """
43 43 Mercurial Model
44 44 """
45 45
46 46 def __init__(self):
47 47 """
48 48 Constructor
49 49 """
50 50 pass
51 51
52 52 @staticmethod
53 53 def repo_scan(repos_prefix, repos_path, baseui):
54 54 """
55 55 Listing of repositories in given path. This path should not be a
56 56 repository itself. Return a dictionary of repository objects
57 57 :param repos_path: path to directory it could take syntax with
58 58 * or ** for deep recursive displaying repositories
59 59 """
60 60 def check_repo_dir(path):
61 61 """
62 62 Checks the repository
63 63 :param path:
64 64 """
65 65 repos_path = path.split('/')
66 66 if repos_path[-1] in ['*', '**']:
67 67 repos_path = repos_path[:-1]
68 68 if repos_path[0] != '/':
69 69 repos_path[0] = '/'
70 70 if not os.path.isdir(os.path.join(*repos_path)):
71 71 raise RepositoryError('Not a valid repository in %s' % path[0][1])
72 72 if not repos_path.endswith('*'):
73 73 raise VCSError('You need to specify * or ** at the end of path '
74 74 'for recursive scanning')
75 75
76 76 check_repo_dir(repos_path)
77 77 log.info('scanning for repositories in %s', repos_path)
78 78 repos = findrepos([(repos_prefix, repos_path)])
79 79 if not isinstance(baseui, ui.ui):
80 80 baseui = ui.ui()
81 81
82 82 repos_list = {}
83 83 for name, path in repos:
84 84 try:
85 repos_list[name] = MercurialRepository(path, baseui=baseui)
85 #name = name.split('/')[-1]
86 if repos_list.has_key(name):
87 raise RepositoryError('Duplicate repository name %s found in'
88 ' %s' % (name, path))
89 else:
90 repos_list[name] = MercurialRepository(path, baseui=baseui)
91 repos_list[name].name = name
86 92 except OSError:
87 93 continue
88 94 return repos_list
89 95
90 96 def get_repos(self):
91 97 for name, repo in _get_repos_cached().items():
92 98 if repo._get_hidden():
93 99 #skip hidden web repository
94 100 continue
95 101
96 102 last_change = repo.last_change
97 103 try:
98 104 tip = repo.get_changeset('tip')
99 105 except RepositoryError:
100 106 from pylons_app.lib.utils import EmptyChangeset
101 107 tip = EmptyChangeset()
102 108
103 109 tmp_d = {}
104 110 tmp_d['name'] = repo.name
105 111 tmp_d['name_sort'] = tmp_d['name'].lower()
106 112 tmp_d['description'] = repo.description
107 113 tmp_d['description_sort'] = tmp_d['description']
108 114 tmp_d['last_change'] = last_change
109 115 tmp_d['last_change_sort'] = last_change[1] - last_change[0]
110 116 tmp_d['tip'] = tip.raw_id
111 117 tmp_d['tip_sort'] = tip.revision
112 118 tmp_d['rev'] = tip.revision
113 119 tmp_d['contact'] = repo.contact
114 120 tmp_d['contact_sort'] = tmp_d['contact']
115 121 tmp_d['repo_archives'] = list(repo._get_archives())
116 122
117 123 yield tmp_d
118 124
119 125 def get_repo(self, repo_name):
120 126 return _get_repos_cached()[repo_name]
General Comments 0
You need to be logged in to leave comments. Login now