##// END OF EJS Templates
white space cleanup
marcink -
r2207:17ff5693 beta
parent child Browse files
Show More
@@ -1,287 +1,286 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplegit
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 7 It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import re
29 29 import logging
30 30 import traceback
31 31
32 32 from dulwich import server as dulserver
33 33
34 34
35 35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
36 36
37 37 def handle(self):
38 38 write = lambda x: self.proto.write_sideband(1, x)
39 39
40 40 graph_walker = dulserver.ProtocolGraphWalker(self,
41 41 self.repo.object_store,
42 42 self.repo.get_peeled)
43 43 objects_iter = self.repo.fetch_objects(
44 44 graph_walker.determine_wants, graph_walker, self.progress,
45 45 get_tagged=self.get_tagged)
46 46
47 47 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
48 48 # that the client still expects a 0-object pack in most cases.
49 49 if objects_iter is None:
50 50 return
51 51
52 52 self.progress("counting objects: %d, done.\n" % len(objects_iter))
53 53 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
54 54 objects_iter)
55 55 messages = []
56 56 messages.append('thank you for using rhodecode')
57 57
58 58 for msg in messages:
59 59 self.progress(msg + "\n")
60 60 # we are done
61 61 self.proto.write("0000")
62 62
63 63
64 64 dulserver.DEFAULT_HANDLERS = {
65 65 'git-upload-pack': SimpleGitUploadPackHandler,
66 66 'git-receive-pack': dulserver.ReceivePackHandler,
67 67 }
68 68
69 69 from dulwich.repo import Repo
70 70 from dulwich.web import make_wsgi_chain
71 71
72 72 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
73 73
74 74 from rhodecode.lib.utils2 import safe_str
75 75 from rhodecode.lib.base import BaseVCSController
76 76 from rhodecode.lib.auth import get_container_username
77 77 from rhodecode.lib.utils import is_valid_repo, make_ui
78 78 from rhodecode.model.db import User
79 79
80 80 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
81 81
82 82 log = logging.getLogger(__name__)
83 83
84 84
85 85 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
86 86
87 87
88 88 def is_git(environ):
89 89 path_info = environ['PATH_INFO']
90 90 isgit_path = GIT_PROTO_PAT.match(path_info)
91 91 log.debug('pathinfo: %s detected as GIT %s' % (
92 92 path_info, isgit_path != None)
93 93 )
94 94 return isgit_path
95 95
96 96
97 97 class SimpleGit(BaseVCSController):
98 98
99 99 def _handle_request(self, environ, start_response):
100 100
101 101 if not is_git(environ):
102 102 return self.application(environ, start_response)
103 103
104 104 ipaddr = self._get_ip_addr(environ)
105 105 username = None
106 106 self._git_first_op = False
107 107 # skip passing error to error controller
108 108 environ['pylons.status_code_redirect'] = True
109 109
110 110 #======================================================================
111 111 # EXTRACT REPOSITORY NAME FROM ENV
112 112 #======================================================================
113 113 try:
114 114 repo_name = self.__get_repository(environ)
115 115 log.debug('Extracted repo name is %s' % repo_name)
116 116 except:
117 117 return HTTPInternalServerError()(environ, start_response)
118 118
119 119 # quick check if that dir exists...
120 120 if is_valid_repo(repo_name, self.basepath) is False:
121 121 return HTTPNotFound()(environ, start_response)
122 122
123 123 #======================================================================
124 124 # GET ACTION PULL or PUSH
125 125 #======================================================================
126 126 action = self.__get_action(environ)
127 127
128 128 #======================================================================
129 129 # CHECK ANONYMOUS PERMISSION
130 130 #======================================================================
131 131 if action in ['pull', 'push']:
132 132 anonymous_user = self.__get_user('default')
133 133 username = anonymous_user.username
134 134 anonymous_perm = self._check_permission(action, anonymous_user,
135 135 repo_name)
136 136
137 137 if anonymous_perm is not True or anonymous_user.active is False:
138 138 if anonymous_perm is not True:
139 139 log.debug('Not enough credentials to access this '
140 140 'repository as anonymous user')
141 141 if anonymous_user.active is False:
142 142 log.debug('Anonymous access is disabled, running '
143 143 'authentication')
144 144 #==============================================================
145 145 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
146 146 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
147 147 #==============================================================
148 148
149 149 # Attempting to retrieve username from the container
150 150 username = get_container_username(environ, self.config)
151 151
152 152 # If not authenticated by the container, running basic auth
153 153 if not username:
154 154 self.authenticate.realm = \
155 155 safe_str(self.config['rhodecode_realm'])
156 156 result = self.authenticate(environ)
157 157 if isinstance(result, str):
158 158 AUTH_TYPE.update(environ, 'basic')
159 159 REMOTE_USER.update(environ, result)
160 160 username = result
161 161 else:
162 162 return result.wsgi_application(environ, start_response)
163 163
164 164 #==============================================================
165 165 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
166 166 #==============================================================
167 167 if action in ['pull', 'push']:
168 168 try:
169 169 user = self.__get_user(username)
170 170 if user is None or not user.active:
171 171 return HTTPForbidden()(environ, start_response)
172 172 username = user.username
173 173 except:
174 174 log.error(traceback.format_exc())
175 175 return HTTPInternalServerError()(environ,
176 176 start_response)
177 177
178 178 #check permissions for this repository
179 179 perm = self._check_permission(action, user, repo_name)
180 180 if perm is not True:
181 181 return HTTPForbidden()(environ, start_response)
182 182 extras = {
183 183 'ip': ipaddr,
184 184 'username': username,
185 185 'action': action,
186 186 'repository': repo_name,
187 187 'scm': 'git',
188 188 }
189 189
190 190 #===================================================================
191 191 # GIT REQUEST HANDLING
192 192 #===================================================================
193 193 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
194 194 log.debug('Repository path is %s' % repo_path)
195 195
196 196 baseui = make_ui('db')
197 197 for k, v in extras.items():
198 198 baseui.setconfig('rhodecode_extras', k, v)
199 199
200 200 try:
201 201 # invalidate cache on push
202 202 if action == 'push':
203 203 self._invalidate_cache(repo_name)
204 204 self._handle_githooks(action, baseui, environ)
205 205
206 206 log.info('%s action on GIT repo "%s"' % (action, repo_name))
207 207 app = self.__make_app(repo_name, repo_path)
208 208 return app(environ, start_response)
209 209 except Exception:
210 210 log.error(traceback.format_exc())
211 211 return HTTPInternalServerError()(environ, start_response)
212 212
213 213 def __make_app(self, repo_name, repo_path):
214 214 """
215 215 Make an wsgi application using dulserver
216 216
217 217 :param repo_name: name of the repository
218 218 :param repo_path: full path to the repository
219 219 """
220 220 _d = {'/' + repo_name: Repo(repo_path)}
221 221 backend = dulserver.DictBackend(_d)
222 222 gitserve = make_wsgi_chain(backend)
223 223
224 224 return gitserve
225 225
226 226 def __get_repository(self, environ):
227 227 """
228 228 Get's repository name out of PATH_INFO header
229 229
230 230 :param environ: environ where PATH_INFO is stored
231 231 """
232 232 try:
233 233 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
234 234 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
235 235 except:
236 236 log.error(traceback.format_exc())
237 237 raise
238 238
239 239 return repo_name
240 240
241 241 def __get_user(self, username):
242 242 return User.get_by_username(username)
243 243
244 244 def __get_action(self, environ):
245 245 """
246 246 Maps git request commands into a pull or push command.
247 247
248 248 :param environ:
249 249 """
250 250 service = environ['QUERY_STRING'].split('=')
251 251
252 252 if len(service) > 1:
253 253 service_cmd = service[1]
254 254 mapping = {
255 255 'git-receive-pack': 'push',
256 256 'git-upload-pack': 'pull',
257 257 }
258 258 op = mapping[service_cmd]
259 259 self._git_stored_op = op
260 260 return op
261 261 else:
262 262 # try to fallback to stored variable as we don't know if the last
263 263 # operation is pull/push
264 264 op = getattr(self, '_git_stored_op', 'pull')
265 265 return op
266 266
267 267 def _handle_githooks(self, action, baseui, environ):
268 268
269 269 from rhodecode.lib.hooks import log_pull_action, log_push_action
270 270 service = environ['QUERY_STRING'].split('=')
271 271 if len(service) < 2:
272 272 return
273 273
274 274 class cont(object):
275 275 pass
276 276
277 277 repo = cont()
278 278 setattr(repo, 'ui', baseui)
279 279
280 280 push_hook = 'pretxnchangegroup.push_logger'
281 281 pull_hook = 'preoutgoing.pull_logger'
282 282 _hooks = dict(baseui.configitems('hooks')) or {}
283 283 if action == 'push' and _hooks.get(push_hook):
284 284 log_push_action(ui=baseui, repo=repo)
285 285 elif action == 'pull' and _hooks.get(pull_hook):
286 286 log_pull_action(ui=baseui, repo=repo)
287
@@ -1,409 +1,408 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Some simple helper functions
7 7
8 8 :created_on: Jan 5, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import re
27 27 from rhodecode.lib.vcs.utils.lazy import LazyProperty
28 28
29 29
30 30 def __get_lem():
31 31 """
32 32 Get language extension map based on what's inside pygments lexers
33 33 """
34 34 from pygments import lexers
35 35 from string import lower
36 36 from collections import defaultdict
37 37
38 38 d = defaultdict(lambda: [])
39 39
40 40 def __clean(s):
41 41 s = s.lstrip('*')
42 42 s = s.lstrip('.')
43 43
44 44 if s.find('[') != -1:
45 45 exts = []
46 46 start, stop = s.find('['), s.find(']')
47 47
48 48 for suffix in s[start + 1:stop]:
49 49 exts.append(s[:s.find('[')] + suffix)
50 50 return map(lower, exts)
51 51 else:
52 52 return map(lower, [s])
53 53
54 54 for lx, t in sorted(lexers.LEXERS.items()):
55 55 m = map(__clean, t[-2])
56 56 if m:
57 57 m = reduce(lambda x, y: x + y, m)
58 58 for ext in m:
59 59 desc = lx.replace('Lexer', '')
60 60 d[ext].append(desc)
61 61
62 62 return dict(d)
63 63
64 64 def str2bool(_str):
65 65 """
66 66 returs True/False value from given string, it tries to translate the
67 67 string into boolean
68 68
69 69 :param _str: string value to translate into boolean
70 70 :rtype: boolean
71 71 :returns: boolean from given string
72 72 """
73 73 if _str is None:
74 74 return False
75 75 if _str in (True, False):
76 76 return _str
77 77 _str = str(_str).strip().lower()
78 78 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
79 79
80 80
81 81 def convert_line_endings(line, mode):
82 82 """
83 83 Converts a given line "line end" accordingly to given mode
84 84
85 85 Available modes are::
86 86 0 - Unix
87 87 1 - Mac
88 88 2 - DOS
89 89
90 90 :param line: given line to convert
91 91 :param mode: mode to convert to
92 92 :rtype: str
93 93 :return: converted line according to mode
94 94 """
95 95 from string import replace
96 96
97 97 if mode == 0:
98 98 line = replace(line, '\r\n', '\n')
99 99 line = replace(line, '\r', '\n')
100 100 elif mode == 1:
101 101 line = replace(line, '\r\n', '\r')
102 102 line = replace(line, '\n', '\r')
103 103 elif mode == 2:
104 104 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
105 105 return line
106 106
107 107
108 108 def detect_mode(line, default):
109 109 """
110 110 Detects line break for given line, if line break couldn't be found
111 111 given default value is returned
112 112
113 113 :param line: str line
114 114 :param default: default
115 115 :rtype: int
116 116 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
117 117 """
118 118 if line.endswith('\r\n'):
119 119 return 2
120 120 elif line.endswith('\n'):
121 121 return 0
122 122 elif line.endswith('\r'):
123 123 return 1
124 124 else:
125 125 return default
126 126
127 127
128 128 def generate_api_key(username, salt=None):
129 129 """
130 130 Generates unique API key for given username, if salt is not given
131 131 it'll be generated from some random string
132 132
133 133 :param username: username as string
134 134 :param salt: salt to hash generate KEY
135 135 :rtype: str
136 136 :returns: sha1 hash from username+salt
137 137 """
138 138 from tempfile import _RandomNameSequence
139 139 import hashlib
140 140
141 141 if salt is None:
142 142 salt = _RandomNameSequence().next()
143 143
144 144 return hashlib.sha1(username + salt).hexdigest()
145 145
146 146
147 147 def safe_unicode(str_, from_encoding=None):
148 148 """
149 149 safe unicode function. Does few trick to turn str_ into unicode
150 150
151 151 In case of UnicodeDecode error we try to return it with encoding detected
152 152 by chardet library if it fails fallback to unicode with errors replaced
153 153
154 154 :param str_: string to decode
155 155 :rtype: unicode
156 156 :returns: unicode object
157 157 """
158 158 if isinstance(str_, unicode):
159 159 return str_
160 160
161 161 if not from_encoding:
162 162 import rhodecode
163 163 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
164 164 from_encoding = DEFAULT_ENCODING
165 165
166 166 try:
167 167 return unicode(str_)
168 168 except UnicodeDecodeError:
169 169 pass
170 170
171 171 try:
172 172 return unicode(str_, from_encoding)
173 173 except UnicodeDecodeError:
174 174 pass
175 175
176 176 try:
177 177 import chardet
178 178 encoding = chardet.detect(str_)['encoding']
179 179 if encoding is None:
180 180 raise Exception()
181 181 return str_.decode(encoding)
182 182 except (ImportError, UnicodeDecodeError, Exception):
183 183 return unicode(str_, from_encoding, 'replace')
184 184
185 185
186 186 def safe_str(unicode_, to_encoding=None):
187 187 """
188 188 safe str function. Does few trick to turn unicode_ into string
189 189
190 190 In case of UnicodeEncodeError we try to return it with encoding detected
191 191 by chardet library if it fails fallback to string with errors replaced
192 192
193 193 :param unicode_: unicode to encode
194 194 :rtype: str
195 195 :returns: str object
196 196 """
197 197
198 198 # if it's not basestr cast to str
199 199 if not isinstance(unicode_, basestring):
200 200 return str(unicode_)
201 201
202 202 if isinstance(unicode_, str):
203 203 return unicode_
204 204
205 205 if not to_encoding:
206 206 import rhodecode
207 207 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
208 208 to_encoding = DEFAULT_ENCODING
209 209
210 210 try:
211 211 return unicode_.encode(to_encoding)
212 212 except UnicodeEncodeError:
213 213 pass
214 214
215 215 try:
216 216 import chardet
217 217 encoding = chardet.detect(unicode_)['encoding']
218 218 print encoding
219 219 if encoding is None:
220 220 raise UnicodeEncodeError()
221 221
222 222 return unicode_.encode(encoding)
223 223 except (ImportError, UnicodeEncodeError):
224 224 return unicode_.encode(to_encoding, 'replace')
225 225
226 226 return safe_str
227 227
228 228
229 229 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
230 230 """
231 231 Custom engine_from_config functions that makes sure we use NullPool for
232 232 file based sqlite databases. This prevents errors on sqlite. This only
233 233 applies to sqlalchemy versions < 0.7.0
234 234
235 235 """
236 236 import sqlalchemy
237 237 from sqlalchemy import engine_from_config as efc
238 238 import logging
239 239
240 240 if int(sqlalchemy.__version__.split('.')[1]) < 7:
241 241
242 242 # This solution should work for sqlalchemy < 0.7.0, and should use
243 243 # proxy=TimerProxy() for execution time profiling
244 244
245 245 from sqlalchemy.pool import NullPool
246 246 url = configuration[prefix + 'url']
247 247
248 248 if url.startswith('sqlite'):
249 249 kwargs.update({'poolclass': NullPool})
250 250 return efc(configuration, prefix, **kwargs)
251 251 else:
252 252 import time
253 253 from sqlalchemy import event
254 254 from sqlalchemy.engine import Engine
255 255
256 256 log = logging.getLogger('sqlalchemy.engine')
257 257 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
258 258 engine = efc(configuration, prefix, **kwargs)
259 259
260 260 def color_sql(sql):
261 261 COLOR_SEQ = "\033[1;%dm"
262 262 COLOR_SQL = YELLOW
263 263 normal = '\x1b[0m'
264 264 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
265 265
266 266 if configuration['debug']:
267 267 #attach events only for debug configuration
268 268
269 269 def before_cursor_execute(conn, cursor, statement,
270 270 parameters, context, executemany):
271 271 context._query_start_time = time.time()
272 272 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
273 273
274 274
275 275 def after_cursor_execute(conn, cursor, statement,
276 276 parameters, context, executemany):
277 277 total = time.time() - context._query_start_time
278 278 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
279 279
280 280 event.listen(engine, "before_cursor_execute",
281 281 before_cursor_execute)
282 282 event.listen(engine, "after_cursor_execute",
283 283 after_cursor_execute)
284 284
285 285 return engine
286 286
287 287
288 288 def age(curdate):
289 289 """
290 290 turns a datetime into an age string.
291 291
292 292 :param curdate: datetime object
293 293 :rtype: unicode
294 294 :returns: unicode words describing age
295 295 """
296 296
297 297 from datetime import datetime
298 298 from webhelpers.date import time_ago_in_words
299 299
300 300 _ = lambda s: s
301 301
302 302 if not curdate:
303 303 return ''
304 304
305 305 agescales = [(_(u"year"), 3600 * 24 * 365),
306 306 (_(u"month"), 3600 * 24 * 30),
307 307 (_(u"day"), 3600 * 24),
308 308 (_(u"hour"), 3600),
309 309 (_(u"minute"), 60),
310 310 (_(u"second"), 1), ]
311 311
312 312 age = datetime.now() - curdate
313 313 age_seconds = (age.days * agescales[2][1]) + age.seconds
314 314 pos = 1
315 315 for scale in agescales:
316 316 if scale[1] <= age_seconds:
317 317 if pos == 6:
318 318 pos = 5
319 319 return '%s %s' % (time_ago_in_words(curdate,
320 320 agescales[pos][0]), _('ago'))
321 321 pos += 1
322 322
323 323 return _(u'just now')
324 324
325 325
326 326 def uri_filter(uri):
327 327 """
328 328 Removes user:password from given url string
329 329
330 330 :param uri:
331 331 :rtype: unicode
332 332 :returns: filtered list of strings
333 333 """
334 334 if not uri:
335 335 return ''
336 336
337 337 proto = ''
338 338
339 339 for pat in ('https://', 'http://'):
340 340 if uri.startswith(pat):
341 341 uri = uri[len(pat):]
342 342 proto = pat
343 343 break
344 344
345 345 # remove passwords and username
346 346 uri = uri[uri.find('@') + 1:]
347 347
348 348 # get the port
349 349 cred_pos = uri.find(':')
350 350 if cred_pos == -1:
351 351 host, port = uri, None
352 352 else:
353 353 host, port = uri[:cred_pos], uri[cred_pos + 1:]
354 354
355 355 return filter(None, [proto, host, port])
356 356
357 357
358 358 def credentials_filter(uri):
359 359 """
360 360 Returns a url with removed credentials
361 361
362 362 :param uri:
363 363 """
364 364
365 365 uri = uri_filter(uri)
366 366 #check if we have port
367 367 if len(uri) > 2 and uri[2]:
368 368 uri[2] = ':' + uri[2]
369 369
370 370 return ''.join(uri)
371 371
372 372
373 373 def get_changeset_safe(repo, rev):
374 374 """
375 375 Safe version of get_changeset if this changeset doesn't exists for a
376 376 repo it returns a Dummy one instead
377 377
378 378 :param repo:
379 379 :param rev:
380 380 """
381 381 from rhodecode.lib.vcs.backends.base import BaseRepository
382 382 from rhodecode.lib.vcs.exceptions import RepositoryError
383 383 if not isinstance(repo, BaseRepository):
384 384 raise Exception('You must pass an Repository '
385 385 'object as first argument got %s', type(repo))
386 386
387 387 try:
388 388 cs = repo.get_changeset(rev)
389 389 except RepositoryError:
390 390 from rhodecode.lib.utils import EmptyChangeset
391 391 cs = EmptyChangeset(requested_revision=rev)
392 392 return cs
393 393
394 394
395 395 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
396 396
397 397
398 398 def extract_mentioned_users(s):
399 399 """
400 400 Returns unique usernames from given string s that have @mention
401 401
402 402 :param s: string to get mentions
403 403 """
404 404 usrs = set()
405 405 for username in re.findall(MENTIONS_REGEX, s):
406 406 usrs.add(username)
407 407
408 408 return sorted(list(usrs), key=lambda k: k.lower())
409
@@ -1,14 +1,14 b''
1 1 """
2 2 Mercurial libs compatibility
3 3 """
4 4
5 5 from mercurial import archival, merge as hg_merge, patch, ui
6 6 from mercurial.commands import clone, nullid, pull
7 7 from mercurial.context import memctx, memfilectx
8 8 from mercurial.error import RepoError, RepoLookupError, Abort
9 9 from mercurial.hgweb.common import get_contact
10 10 from mercurial.localrepo import localrepository
11 11 from mercurial.match import match
12 12 from mercurial.mdiff import diffopts
13 13 from mercurial.node import hex
14 from mercurial.encoding import tolocal No newline at end of file
14 from mercurial.encoding import tolocal
General Comments 0
You need to be logged in to leave comments. Login now