##// END OF EJS Templates
avoid displaying repr of internal classes in user facing messages...
Mads Kiilerich -
r3574:000653f7 beta
parent child Browse files
Show More
@@ -1,698 +1,698
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.git
3 vcs.backends.git
4 ~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~
5
5
6 Git backend implementation.
6 Git backend implementation.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import os
12 import os
13 import re
13 import re
14 import time
14 import time
15 import posixpath
15 import posixpath
16 import logging
16 import logging
17 import traceback
17 import traceback
18 import urllib
18 import urllib
19 import urllib2
19 import urllib2
20 from dulwich.repo import Repo, NotGitRepository
20 from dulwich.repo import Repo, NotGitRepository
21 from dulwich.objects import Tag
21 from dulwich.objects import Tag
22 from string import Template
22 from string import Template
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.lib.vcs.backends.base import BaseRepository
25 from rhodecode.lib.vcs.backends.base import BaseRepository
26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
29 from rhodecode.lib.vcs.exceptions import RepositoryError
29 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
35 from rhodecode.lib.vcs.utils.paths import abspath
35 from rhodecode.lib.vcs.utils.paths import abspath
36 from rhodecode.lib.vcs.utils.paths import get_user_home
36 from rhodecode.lib.vcs.utils.paths import get_user_home
37 from .workdir import GitWorkdir
37 from .workdir import GitWorkdir
38 from .changeset import GitChangeset
38 from .changeset import GitChangeset
39 from .inmemory import GitInMemoryChangeset
39 from .inmemory import GitInMemoryChangeset
40 from .config import ConfigFile
40 from .config import ConfigFile
41 from rhodecode.lib import subprocessio
41 from rhodecode.lib import subprocessio
42
42
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class GitRepository(BaseRepository):
47 class GitRepository(BaseRepository):
48 """
48 """
49 Git repository backend.
49 Git repository backend.
50 """
50 """
51 DEFAULT_BRANCH_NAME = 'master'
51 DEFAULT_BRANCH_NAME = 'master'
52 scm = 'git'
52 scm = 'git'
53
53
54 def __init__(self, repo_path, create=False, src_url=None,
54 def __init__(self, repo_path, create=False, src_url=None,
55 update_after_clone=False, bare=False):
55 update_after_clone=False, bare=False):
56
56
57 self.path = abspath(repo_path)
57 self.path = abspath(repo_path)
58 repo = self._get_repo(create, src_url, update_after_clone, bare)
58 repo = self._get_repo(create, src_url, update_after_clone, bare)
59 self.bare = repo.bare
59 self.bare = repo.bare
60
60
61 self._config_files = [
61 self._config_files = [
62 bare and abspath(self.path, 'config')
62 bare and abspath(self.path, 'config')
63 or abspath(self.path, '.git', 'config'),
63 or abspath(self.path, '.git', 'config'),
64 abspath(get_user_home(), '.gitconfig'),
64 abspath(get_user_home(), '.gitconfig'),
65 ]
65 ]
66
66
67 @ThreadLocalLazyProperty
67 @ThreadLocalLazyProperty
68 def _repo(self):
68 def _repo(self):
69 repo = Repo(self.path)
69 repo = Repo(self.path)
70 # patch the instance of GitRepo with an "FAKE" ui object to add
70 # patch the instance of GitRepo with an "FAKE" ui object to add
71 # compatibility layer with Mercurial
71 # compatibility layer with Mercurial
72 if not hasattr(repo, 'ui'):
72 if not hasattr(repo, 'ui'):
73 from mercurial.ui import ui
73 from mercurial.ui import ui
74 baseui = ui()
74 baseui = ui()
75 setattr(repo, 'ui', baseui)
75 setattr(repo, 'ui', baseui)
76 return repo
76 return repo
77
77
78 @property
78 @property
79 def head(self):
79 def head(self):
80 try:
80 try:
81 return self._repo.head()
81 return self._repo.head()
82 except KeyError:
82 except KeyError:
83 return None
83 return None
84
84
85 @LazyProperty
85 @LazyProperty
86 def revisions(self):
86 def revisions(self):
87 """
87 """
88 Returns list of revisions' ids, in ascending order. Being lazy
88 Returns list of revisions' ids, in ascending order. Being lazy
89 attribute allows external tools to inject shas from cache.
89 attribute allows external tools to inject shas from cache.
90 """
90 """
91 return self._get_all_revisions()
91 return self._get_all_revisions()
92
92
93 @classmethod
93 @classmethod
94 def _run_git_command(cls, cmd, **opts):
94 def _run_git_command(cls, cmd, **opts):
95 """
95 """
96 Runs given ``cmd`` as git command and returns tuple
96 Runs given ``cmd`` as git command and returns tuple
97 (stdout, stderr).
97 (stdout, stderr).
98
98
99 :param cmd: git command to be executed
99 :param cmd: git command to be executed
100 :param opts: env options to pass into Subprocess command
100 :param opts: env options to pass into Subprocess command
101 """
101 """
102
102
103 if '_bare' in opts:
103 if '_bare' in opts:
104 _copts = []
104 _copts = []
105 del opts['_bare']
105 del opts['_bare']
106 else:
106 else:
107 _copts = ['-c', 'core.quotepath=false', ]
107 _copts = ['-c', 'core.quotepath=false', ]
108 safe_call = False
108 safe_call = False
109 if '_safe' in opts:
109 if '_safe' in opts:
110 #no exc on failure
110 #no exc on failure
111 del opts['_safe']
111 del opts['_safe']
112 safe_call = True
112 safe_call = True
113
113
114 _str_cmd = False
114 _str_cmd = False
115 if isinstance(cmd, basestring):
115 if isinstance(cmd, basestring):
116 cmd = [cmd]
116 cmd = [cmd]
117 _str_cmd = True
117 _str_cmd = True
118
118
119 gitenv = os.environ
119 gitenv = os.environ
120 # need to clean fix GIT_DIR !
120 # need to clean fix GIT_DIR !
121 if 'GIT_DIR' in gitenv:
121 if 'GIT_DIR' in gitenv:
122 del gitenv['GIT_DIR']
122 del gitenv['GIT_DIR']
123 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
123 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
124
124
125 _git_path = rhodecode.CONFIG.get('git_path', 'git')
125 _git_path = rhodecode.CONFIG.get('git_path', 'git')
126 cmd = [_git_path] + _copts + cmd
126 cmd = [_git_path] + _copts + cmd
127 if _str_cmd:
127 if _str_cmd:
128 cmd = ' '.join(cmd)
128 cmd = ' '.join(cmd)
129 try:
129 try:
130 _opts = dict(
130 _opts = dict(
131 env=gitenv,
131 env=gitenv,
132 shell=False,
132 shell=False,
133 )
133 )
134 _opts.update(opts)
134 _opts.update(opts)
135 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
135 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
136 except (EnvironmentError, OSError), err:
136 except (EnvironmentError, OSError), err:
137 tb_err = ("Couldn't run git command (%s).\n"
137 tb_err = ("Couldn't run git command (%s).\n"
138 "Original error was:%s\n" % (cmd, err))
138 "Original error was:%s\n" % (cmd, err))
139 log.error(tb_err)
139 log.error(tb_err)
140 if safe_call:
140 if safe_call:
141 return '', err
141 return '', err
142 else:
142 else:
143 raise RepositoryError(tb_err)
143 raise RepositoryError(tb_err)
144
144
145 return ''.join(p.output), ''.join(p.error)
145 return ''.join(p.output), ''.join(p.error)
146
146
147 def run_git_command(self, cmd):
147 def run_git_command(self, cmd):
148 opts = {}
148 opts = {}
149 if os.path.isdir(self.path):
149 if os.path.isdir(self.path):
150 opts['cwd'] = self.path
150 opts['cwd'] = self.path
151 return self._run_git_command(cmd, **opts)
151 return self._run_git_command(cmd, **opts)
152
152
153 @classmethod
153 @classmethod
154 def _check_url(cls, url):
154 def _check_url(cls, url):
155 """
155 """
156 Functon will check given url and try to verify if it's a valid
156 Functon will check given url and try to verify if it's a valid
157 link. Sometimes it may happened that mercurial will issue basic
157 link. Sometimes it may happened that mercurial will issue basic
158 auth request that can cause whole API to hang when used from python
158 auth request that can cause whole API to hang when used from python
159 or other external calls.
159 or other external calls.
160
160
161 On failures it'll raise urllib2.HTTPError
161 On failures it'll raise urllib2.HTTPError
162 """
162 """
163 from mercurial.util import url as Url
163 from mercurial.util import url as Url
164
164
165 # those authnadlers are patched for python 2.6.5 bug an
165 # those authnadlers are patched for python 2.6.5 bug an
166 # infinit looping when given invalid resources
166 # infinit looping when given invalid resources
167 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
167 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
168
168
169 # check first if it's not an local url
169 # check first if it's not an local url
170 if os.path.isdir(url) or url.startswith('file:'):
170 if os.path.isdir(url) or url.startswith('file:'):
171 return True
171 return True
172
172
173 if('+' in url[:url.find('://')]):
173 if('+' in url[:url.find('://')]):
174 url = url[url.find('+') + 1:]
174 url = url[url.find('+') + 1:]
175
175
176 handlers = []
176 handlers = []
177 test_uri, authinfo = Url(url).authinfo()
177 test_uri, authinfo = Url(url).authinfo()
178 if not test_uri.endswith('info/refs'):
178 if not test_uri.endswith('info/refs'):
179 test_uri = test_uri.rstrip('/') + '/info/refs'
179 test_uri = test_uri.rstrip('/') + '/info/refs'
180 if authinfo:
180 if authinfo:
181 #create a password manager
181 #create a password manager
182 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
182 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
183 passmgr.add_password(*authinfo)
183 passmgr.add_password(*authinfo)
184
184
185 handlers.extend((httpbasicauthhandler(passmgr),
185 handlers.extend((httpbasicauthhandler(passmgr),
186 httpdigestauthhandler(passmgr)))
186 httpdigestauthhandler(passmgr)))
187
187
188 o = urllib2.build_opener(*handlers)
188 o = urllib2.build_opener(*handlers)
189 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
189 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
190
190
191 q = {"service": 'git-upload-pack'}
191 q = {"service": 'git-upload-pack'}
192 qs = '?%s' % urllib.urlencode(q)
192 qs = '?%s' % urllib.urlencode(q)
193 cu = "%s%s" % (test_uri, qs)
193 cu = "%s%s" % (test_uri, qs)
194 req = urllib2.Request(cu, None, {})
194 req = urllib2.Request(cu, None, {})
195
195
196 try:
196 try:
197 resp = o.open(req)
197 resp = o.open(req)
198 return resp.code == 200
198 return resp.code == 200
199 except Exception, e:
199 except Exception, e:
200 # means it cannot be cloned
200 # means it cannot be cloned
201 raise urllib2.URLError("[%s] %s" % (url, e))
201 raise urllib2.URLError("[%s] %s" % (url, e))
202
202
203 def _get_repo(self, create, src_url=None, update_after_clone=False,
203 def _get_repo(self, create, src_url=None, update_after_clone=False,
204 bare=False):
204 bare=False):
205 if create and os.path.exists(self.path):
205 if create and os.path.exists(self.path):
206 raise RepositoryError("Location already exist")
206 raise RepositoryError("Location already exist")
207 if src_url and not create:
207 if src_url and not create:
208 raise RepositoryError("Create should be set to True if src_url is "
208 raise RepositoryError("Create should be set to True if src_url is "
209 "given (clone operation creates repository)")
209 "given (clone operation creates repository)")
210 try:
210 try:
211 if create and src_url:
211 if create and src_url:
212 GitRepository._check_url(src_url)
212 GitRepository._check_url(src_url)
213 self.clone(src_url, update_after_clone, bare)
213 self.clone(src_url, update_after_clone, bare)
214 return Repo(self.path)
214 return Repo(self.path)
215 elif create:
215 elif create:
216 os.mkdir(self.path)
216 os.mkdir(self.path)
217 if bare:
217 if bare:
218 return Repo.init_bare(self.path)
218 return Repo.init_bare(self.path)
219 else:
219 else:
220 return Repo.init(self.path)
220 return Repo.init(self.path)
221 else:
221 else:
222 return self._repo
222 return self._repo
223 except (NotGitRepository, OSError), err:
223 except (NotGitRepository, OSError), err:
224 raise RepositoryError(err)
224 raise RepositoryError(err)
225
225
226 def _get_all_revisions(self):
226 def _get_all_revisions(self):
227 # we must check if this repo is not empty, since later command
227 # we must check if this repo is not empty, since later command
228 # fails if it is. And it's cheaper to ask than throw the subprocess
228 # fails if it is. And it's cheaper to ask than throw the subprocess
229 # errors
229 # errors
230 try:
230 try:
231 self._repo.head()
231 self._repo.head()
232 except KeyError:
232 except KeyError:
233 return []
233 return []
234 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
234 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
235 '--all').strip()
235 '--all').strip()
236 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
236 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
237 try:
237 try:
238 so, se = self.run_git_command(cmd)
238 so, se = self.run_git_command(cmd)
239 except RepositoryError:
239 except RepositoryError:
240 # Can be raised for empty repositories
240 # Can be raised for empty repositories
241 return []
241 return []
242 return so.splitlines()
242 return so.splitlines()
243
243
244 def _get_all_revisions2(self):
244 def _get_all_revisions2(self):
245 #alternate implementation using dulwich
245 #alternate implementation using dulwich
246 includes = [x[1][0] for x in self._parsed_refs.iteritems()
246 includes = [x[1][0] for x in self._parsed_refs.iteritems()
247 if x[1][1] != 'T']
247 if x[1][1] != 'T']
248 return [c.commit.id for c in self._repo.get_walker(include=includes)]
248 return [c.commit.id for c in self._repo.get_walker(include=includes)]
249
249
250 def _get_revision(self, revision):
250 def _get_revision(self, revision):
251 """
251 """
252 For git backend we always return integer here. This way we ensure
252 For git backend we always return integer here. This way we ensure
253 that changset's revision attribute would become integer.
253 that changset's revision attribute would become integer.
254 """
254 """
255 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
255 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
256 is_bstr = lambda o: isinstance(o, (str, unicode))
256 is_bstr = lambda o: isinstance(o, (str, unicode))
257 is_null = lambda o: len(o) == revision.count('0')
257 is_null = lambda o: len(o) == revision.count('0')
258
258
259 if len(self.revisions) == 0:
259 if len(self.revisions) == 0:
260 raise EmptyRepositoryError("There are no changesets yet")
260 raise EmptyRepositoryError("There are no changesets yet")
261
261
262 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
262 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
263 revision = self.revisions[-1]
263 revision = self.revisions[-1]
264
264
265 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
265 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
266 or isinstance(revision, int) or is_null(revision)):
266 or isinstance(revision, int) or is_null(revision)):
267 try:
267 try:
268 revision = self.revisions[int(revision)]
268 revision = self.revisions[int(revision)]
269 except:
269 except:
270 raise ChangesetDoesNotExistError("Revision %r does not exist "
270 raise ChangesetDoesNotExistError("Revision %r does not exist "
271 "for this repository %s" % (revision, self))
271 "for this repository" % (revision))
272
272
273 elif is_bstr(revision):
273 elif is_bstr(revision):
274 # get by branch/tag name
274 # get by branch/tag name
275 _ref_revision = self._parsed_refs.get(revision)
275 _ref_revision = self._parsed_refs.get(revision)
276 _tags_shas = self.tags.values()
276 _tags_shas = self.tags.values()
277 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
277 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
278 return _ref_revision[0]
278 return _ref_revision[0]
279
279
280 # maybe it's a tag ? we don't have them in self.revisions
280 # maybe it's a tag ? we don't have them in self.revisions
281 elif revision in _tags_shas:
281 elif revision in _tags_shas:
282 return _tags_shas[_tags_shas.index(revision)]
282 return _tags_shas[_tags_shas.index(revision)]
283
283
284 elif not pattern.match(revision) or revision not in self.revisions:
284 elif not pattern.match(revision) or revision not in self.revisions:
285 raise ChangesetDoesNotExistError("Revision %r does not exist "
285 raise ChangesetDoesNotExistError("Revision %r does not exist "
286 "for this repository %s" % (revision, self))
286 "for this repository" % (revision))
287
287
288 # Ensure we return full id
288 # Ensure we return full id
289 if not pattern.match(str(revision)):
289 if not pattern.match(str(revision)):
290 raise ChangesetDoesNotExistError("Given revision %r not recognized"
290 raise ChangesetDoesNotExistError("Given revision %r not recognized"
291 % revision)
291 % revision)
292 return revision
292 return revision
293
293
294 def _get_archives(self, archive_name='tip'):
294 def _get_archives(self, archive_name='tip'):
295
295
296 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
296 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
297 yield {"type": i[0], "extension": i[1], "node": archive_name}
297 yield {"type": i[0], "extension": i[1], "node": archive_name}
298
298
299 def _get_url(self, url):
299 def _get_url(self, url):
300 """
300 """
301 Returns normalized url. If schema is not given, would fall to
301 Returns normalized url. If schema is not given, would fall to
302 filesystem (``file:///``) schema.
302 filesystem (``file:///``) schema.
303 """
303 """
304 url = str(url)
304 url = str(url)
305 if url != 'default' and not '://' in url:
305 if url != 'default' and not '://' in url:
306 url = ':///'.join(('file', url))
306 url = ':///'.join(('file', url))
307 return url
307 return url
308
308
309 def get_hook_location(self):
309 def get_hook_location(self):
310 """
310 """
311 returns absolute path to location where hooks are stored
311 returns absolute path to location where hooks are stored
312 """
312 """
313 loc = os.path.join(self.path, 'hooks')
313 loc = os.path.join(self.path, 'hooks')
314 if not self.bare:
314 if not self.bare:
315 loc = os.path.join(self.path, '.git', 'hooks')
315 loc = os.path.join(self.path, '.git', 'hooks')
316 return loc
316 return loc
317
317
318 @LazyProperty
318 @LazyProperty
319 def name(self):
319 def name(self):
320 return os.path.basename(self.path)
320 return os.path.basename(self.path)
321
321
322 @LazyProperty
322 @LazyProperty
323 def last_change(self):
323 def last_change(self):
324 """
324 """
325 Returns last change made on this repository as datetime object
325 Returns last change made on this repository as datetime object
326 """
326 """
327 return date_fromtimestamp(self._get_mtime(), makedate()[1])
327 return date_fromtimestamp(self._get_mtime(), makedate()[1])
328
328
329 def _get_mtime(self):
329 def _get_mtime(self):
330 try:
330 try:
331 return time.mktime(self.get_changeset().date.timetuple())
331 return time.mktime(self.get_changeset().date.timetuple())
332 except RepositoryError:
332 except RepositoryError:
333 idx_loc = '' if self.bare else '.git'
333 idx_loc = '' if self.bare else '.git'
334 # fallback to filesystem
334 # fallback to filesystem
335 in_path = os.path.join(self.path, idx_loc, "index")
335 in_path = os.path.join(self.path, idx_loc, "index")
336 he_path = os.path.join(self.path, idx_loc, "HEAD")
336 he_path = os.path.join(self.path, idx_loc, "HEAD")
337 if os.path.exists(in_path):
337 if os.path.exists(in_path):
338 return os.stat(in_path).st_mtime
338 return os.stat(in_path).st_mtime
339 else:
339 else:
340 return os.stat(he_path).st_mtime
340 return os.stat(he_path).st_mtime
341
341
342 @LazyProperty
342 @LazyProperty
343 def description(self):
343 def description(self):
344 idx_loc = '' if self.bare else '.git'
344 idx_loc = '' if self.bare else '.git'
345 undefined_description = u'unknown'
345 undefined_description = u'unknown'
346 description_path = os.path.join(self.path, idx_loc, 'description')
346 description_path = os.path.join(self.path, idx_loc, 'description')
347 if os.path.isfile(description_path):
347 if os.path.isfile(description_path):
348 return safe_unicode(open(description_path).read())
348 return safe_unicode(open(description_path).read())
349 else:
349 else:
350 return undefined_description
350 return undefined_description
351
351
352 @LazyProperty
352 @LazyProperty
353 def contact(self):
353 def contact(self):
354 undefined_contact = u'Unknown'
354 undefined_contact = u'Unknown'
355 return undefined_contact
355 return undefined_contact
356
356
357 @property
357 @property
358 def branches(self):
358 def branches(self):
359 if not self.revisions:
359 if not self.revisions:
360 return {}
360 return {}
361 sortkey = lambda ctx: ctx[0]
361 sortkey = lambda ctx: ctx[0]
362 _branches = [(x[0], x[1][0])
362 _branches = [(x[0], x[1][0])
363 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
363 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
364 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
364 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
365
365
366 @LazyProperty
366 @LazyProperty
367 def tags(self):
367 def tags(self):
368 return self._get_tags()
368 return self._get_tags()
369
369
370 def _get_tags(self):
370 def _get_tags(self):
371 if not self.revisions:
371 if not self.revisions:
372 return {}
372 return {}
373
373
374 sortkey = lambda ctx: ctx[0]
374 sortkey = lambda ctx: ctx[0]
375 _tags = [(x[0], x[1][0])
375 _tags = [(x[0], x[1][0])
376 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
376 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
377 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
377 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
378
378
379 def tag(self, name, user, revision=None, message=None, date=None,
379 def tag(self, name, user, revision=None, message=None, date=None,
380 **kwargs):
380 **kwargs):
381 """
381 """
382 Creates and returns a tag for the given ``revision``.
382 Creates and returns a tag for the given ``revision``.
383
383
384 :param name: name for new tag
384 :param name: name for new tag
385 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
385 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
386 :param revision: changeset id for which new tag would be created
386 :param revision: changeset id for which new tag would be created
387 :param message: message of the tag's commit
387 :param message: message of the tag's commit
388 :param date: date of tag's commit
388 :param date: date of tag's commit
389
389
390 :raises TagAlreadyExistError: if tag with same name already exists
390 :raises TagAlreadyExistError: if tag with same name already exists
391 """
391 """
392 if name in self.tags:
392 if name in self.tags:
393 raise TagAlreadyExistError("Tag %s already exists" % name)
393 raise TagAlreadyExistError("Tag %s already exists" % name)
394 changeset = self.get_changeset(revision)
394 changeset = self.get_changeset(revision)
395 message = message or "Added tag %s for commit %s" % (name,
395 message = message or "Added tag %s for commit %s" % (name,
396 changeset.raw_id)
396 changeset.raw_id)
397 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
397 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
398
398
399 self._parsed_refs = self._get_parsed_refs()
399 self._parsed_refs = self._get_parsed_refs()
400 self.tags = self._get_tags()
400 self.tags = self._get_tags()
401 return changeset
401 return changeset
402
402
403 def remove_tag(self, name, user, message=None, date=None):
403 def remove_tag(self, name, user, message=None, date=None):
404 """
404 """
405 Removes tag with the given ``name``.
405 Removes tag with the given ``name``.
406
406
407 :param name: name of the tag to be removed
407 :param name: name of the tag to be removed
408 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
408 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
409 :param message: message of the tag's removal commit
409 :param message: message of the tag's removal commit
410 :param date: date of tag's removal commit
410 :param date: date of tag's removal commit
411
411
412 :raises TagDoesNotExistError: if tag with given name does not exists
412 :raises TagDoesNotExistError: if tag with given name does not exists
413 """
413 """
414 if name not in self.tags:
414 if name not in self.tags:
415 raise TagDoesNotExistError("Tag %s does not exist" % name)
415 raise TagDoesNotExistError("Tag %s does not exist" % name)
416 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
416 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
417 try:
417 try:
418 os.remove(tagpath)
418 os.remove(tagpath)
419 self._parsed_refs = self._get_parsed_refs()
419 self._parsed_refs = self._get_parsed_refs()
420 self.tags = self._get_tags()
420 self.tags = self._get_tags()
421 except OSError, e:
421 except OSError, e:
422 raise RepositoryError(e.strerror)
422 raise RepositoryError(e.strerror)
423
423
424 @LazyProperty
424 @LazyProperty
425 def _parsed_refs(self):
425 def _parsed_refs(self):
426 return self._get_parsed_refs()
426 return self._get_parsed_refs()
427
427
428 def _get_parsed_refs(self):
428 def _get_parsed_refs(self):
429 refs = self._repo.get_refs()
429 refs = self._repo.get_refs()
430 keys = [('refs/heads/', 'H'),
430 keys = [('refs/heads/', 'H'),
431 ('refs/remotes/origin/', 'RH'),
431 ('refs/remotes/origin/', 'RH'),
432 ('refs/tags/', 'T')]
432 ('refs/tags/', 'T')]
433 _refs = {}
433 _refs = {}
434 for ref, sha in refs.iteritems():
434 for ref, sha in refs.iteritems():
435 for k, type_ in keys:
435 for k, type_ in keys:
436 if ref.startswith(k):
436 if ref.startswith(k):
437 _key = ref[len(k):]
437 _key = ref[len(k):]
438 if type_ == 'T':
438 if type_ == 'T':
439 obj = self._repo.get_object(sha)
439 obj = self._repo.get_object(sha)
440 if isinstance(obj, Tag):
440 if isinstance(obj, Tag):
441 sha = self._repo.get_object(sha).object[1]
441 sha = self._repo.get_object(sha).object[1]
442 _refs[_key] = [sha, type_]
442 _refs[_key] = [sha, type_]
443 break
443 break
444 return _refs
444 return _refs
445
445
446 def _heads(self, reverse=False):
446 def _heads(self, reverse=False):
447 refs = self._repo.get_refs()
447 refs = self._repo.get_refs()
448 heads = {}
448 heads = {}
449
449
450 for key, val in refs.items():
450 for key, val in refs.items():
451 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
451 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
452 if key.startswith(ref_key):
452 if key.startswith(ref_key):
453 n = key[len(ref_key):]
453 n = key[len(ref_key):]
454 if n not in ['HEAD']:
454 if n not in ['HEAD']:
455 heads[n] = val
455 heads[n] = val
456
456
457 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
457 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
458
458
459 def get_changeset(self, revision=None):
459 def get_changeset(self, revision=None):
460 """
460 """
461 Returns ``GitChangeset`` object representing commit from git repository
461 Returns ``GitChangeset`` object representing commit from git repository
462 at the given revision or head (most recent commit) if None given.
462 at the given revision or head (most recent commit) if None given.
463 """
463 """
464 if isinstance(revision, GitChangeset):
464 if isinstance(revision, GitChangeset):
465 return revision
465 return revision
466 revision = self._get_revision(revision)
466 revision = self._get_revision(revision)
467 changeset = GitChangeset(repository=self, revision=revision)
467 changeset = GitChangeset(repository=self, revision=revision)
468 return changeset
468 return changeset
469
469
470 def get_changesets(self, start=None, end=None, start_date=None,
470 def get_changesets(self, start=None, end=None, start_date=None,
471 end_date=None, branch_name=None, reverse=False):
471 end_date=None, branch_name=None, reverse=False):
472 """
472 """
473 Returns iterator of ``GitChangeset`` objects from start to end (both
473 Returns iterator of ``GitChangeset`` objects from start to end (both
474 are inclusive), in ascending date order (unless ``reverse`` is set).
474 are inclusive), in ascending date order (unless ``reverse`` is set).
475
475
476 :param start: changeset ID, as str; first returned changeset
476 :param start: changeset ID, as str; first returned changeset
477 :param end: changeset ID, as str; last returned changeset
477 :param end: changeset ID, as str; last returned changeset
478 :param start_date: if specified, changesets with commit date less than
478 :param start_date: if specified, changesets with commit date less than
479 ``start_date`` would be filtered out from returned set
479 ``start_date`` would be filtered out from returned set
480 :param end_date: if specified, changesets with commit date greater than
480 :param end_date: if specified, changesets with commit date greater than
481 ``end_date`` would be filtered out from returned set
481 ``end_date`` would be filtered out from returned set
482 :param branch_name: if specified, changesets not reachable from given
482 :param branch_name: if specified, changesets not reachable from given
483 branch would be filtered out from returned set
483 branch would be filtered out from returned set
484 :param reverse: if ``True``, returned generator would be reversed
484 :param reverse: if ``True``, returned generator would be reversed
485 (meaning that returned changesets would have descending date order)
485 (meaning that returned changesets would have descending date order)
486
486
487 :raise BranchDoesNotExistError: If given ``branch_name`` does not
487 :raise BranchDoesNotExistError: If given ``branch_name`` does not
488 exist.
488 exist.
489 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
489 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
490 ``end`` could not be found.
490 ``end`` could not be found.
491
491
492 """
492 """
493 if branch_name and branch_name not in self.branches:
493 if branch_name and branch_name not in self.branches:
494 raise BranchDoesNotExistError("Branch '%s' not found" \
494 raise BranchDoesNotExistError("Branch '%s' not found" \
495 % branch_name)
495 % branch_name)
496 # %H at format means (full) commit hash, initial hashes are retrieved
496 # %H at format means (full) commit hash, initial hashes are retrieved
497 # in ascending date order
497 # in ascending date order
498 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
498 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
499 cmd_params = {}
499 cmd_params = {}
500 if start_date:
500 if start_date:
501 cmd_template += ' --since "$since"'
501 cmd_template += ' --since "$since"'
502 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
502 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
503 if end_date:
503 if end_date:
504 cmd_template += ' --until "$until"'
504 cmd_template += ' --until "$until"'
505 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
505 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
506 if branch_name:
506 if branch_name:
507 cmd_template += ' $branch_name'
507 cmd_template += ' $branch_name'
508 cmd_params['branch_name'] = branch_name
508 cmd_params['branch_name'] = branch_name
509 else:
509 else:
510 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
510 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
511 '--all').strip()
511 '--all').strip()
512 cmd_template += ' %s' % (rev_filter)
512 cmd_template += ' %s' % (rev_filter)
513
513
514 cmd = Template(cmd_template).safe_substitute(**cmd_params)
514 cmd = Template(cmd_template).safe_substitute(**cmd_params)
515 revs = self.run_git_command(cmd)[0].splitlines()
515 revs = self.run_git_command(cmd)[0].splitlines()
516 start_pos = 0
516 start_pos = 0
517 end_pos = len(revs)
517 end_pos = len(revs)
518 if start:
518 if start:
519 _start = self._get_revision(start)
519 _start = self._get_revision(start)
520 try:
520 try:
521 start_pos = revs.index(_start)
521 start_pos = revs.index(_start)
522 except ValueError:
522 except ValueError:
523 pass
523 pass
524
524
525 if end is not None:
525 if end is not None:
526 _end = self._get_revision(end)
526 _end = self._get_revision(end)
527 try:
527 try:
528 end_pos = revs.index(_end)
528 end_pos = revs.index(_end)
529 except ValueError:
529 except ValueError:
530 pass
530 pass
531
531
532 if None not in [start, end] and start_pos > end_pos:
532 if None not in [start, end] and start_pos > end_pos:
533 raise RepositoryError('start cannot be after end')
533 raise RepositoryError('start cannot be after end')
534
534
535 if end_pos is not None:
535 if end_pos is not None:
536 end_pos += 1
536 end_pos += 1
537
537
538 revs = revs[start_pos:end_pos]
538 revs = revs[start_pos:end_pos]
539 if reverse:
539 if reverse:
540 revs = reversed(revs)
540 revs = reversed(revs)
541 for rev in revs:
541 for rev in revs:
542 yield self.get_changeset(rev)
542 yield self.get_changeset(rev)
543
543
544 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
544 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
545 context=3):
545 context=3):
546 """
546 """
547 Returns (git like) *diff*, as plain text. Shows changes introduced by
547 Returns (git like) *diff*, as plain text. Shows changes introduced by
548 ``rev2`` since ``rev1``.
548 ``rev2`` since ``rev1``.
549
549
550 :param rev1: Entry point from which diff is shown. Can be
550 :param rev1: Entry point from which diff is shown. Can be
551 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
551 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
552 the changes since empty state of the repository until ``rev2``
552 the changes since empty state of the repository until ``rev2``
553 :param rev2: Until which revision changes should be shown.
553 :param rev2: Until which revision changes should be shown.
554 :param ignore_whitespace: If set to ``True``, would not show whitespace
554 :param ignore_whitespace: If set to ``True``, would not show whitespace
555 changes. Defaults to ``False``.
555 changes. Defaults to ``False``.
556 :param context: How many lines before/after changed lines should be
556 :param context: How many lines before/after changed lines should be
557 shown. Defaults to ``3``.
557 shown. Defaults to ``3``.
558 """
558 """
559 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
559 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
560 if ignore_whitespace:
560 if ignore_whitespace:
561 flags.append('-w')
561 flags.append('-w')
562
562
563 if hasattr(rev1, 'raw_id'):
563 if hasattr(rev1, 'raw_id'):
564 rev1 = getattr(rev1, 'raw_id')
564 rev1 = getattr(rev1, 'raw_id')
565
565
566 if hasattr(rev2, 'raw_id'):
566 if hasattr(rev2, 'raw_id'):
567 rev2 = getattr(rev2, 'raw_id')
567 rev2 = getattr(rev2, 'raw_id')
568
568
569 if rev1 == self.EMPTY_CHANGESET:
569 if rev1 == self.EMPTY_CHANGESET:
570 rev2 = self.get_changeset(rev2).raw_id
570 rev2 = self.get_changeset(rev2).raw_id
571 cmd = ' '.join(['show'] + flags + [rev2])
571 cmd = ' '.join(['show'] + flags + [rev2])
572 else:
572 else:
573 rev1 = self.get_changeset(rev1).raw_id
573 rev1 = self.get_changeset(rev1).raw_id
574 rev2 = self.get_changeset(rev2).raw_id
574 rev2 = self.get_changeset(rev2).raw_id
575 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
575 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
576
576
577 if path:
577 if path:
578 cmd += ' -- "%s"' % path
578 cmd += ' -- "%s"' % path
579
579
580 stdout, stderr = self.run_git_command(cmd)
580 stdout, stderr = self.run_git_command(cmd)
581 # If we used 'show' command, strip first few lines (until actual diff
581 # If we used 'show' command, strip first few lines (until actual diff
582 # starts)
582 # starts)
583 if rev1 == self.EMPTY_CHANGESET:
583 if rev1 == self.EMPTY_CHANGESET:
584 lines = stdout.splitlines()
584 lines = stdout.splitlines()
585 x = 0
585 x = 0
586 for line in lines:
586 for line in lines:
587 if line.startswith('diff'):
587 if line.startswith('diff'):
588 break
588 break
589 x += 1
589 x += 1
590 # Append new line just like 'diff' command do
590 # Append new line just like 'diff' command do
591 stdout = '\n'.join(lines[x:]) + '\n'
591 stdout = '\n'.join(lines[x:]) + '\n'
592 return stdout
592 return stdout
593
593
594 @LazyProperty
594 @LazyProperty
595 def in_memory_changeset(self):
595 def in_memory_changeset(self):
596 """
596 """
597 Returns ``GitInMemoryChangeset`` object for this repository.
597 Returns ``GitInMemoryChangeset`` object for this repository.
598 """
598 """
599 return GitInMemoryChangeset(self)
599 return GitInMemoryChangeset(self)
600
600
601 def clone(self, url, update_after_clone=True, bare=False):
601 def clone(self, url, update_after_clone=True, bare=False):
602 """
602 """
603 Tries to clone changes from external location.
603 Tries to clone changes from external location.
604
604
605 :param update_after_clone: If set to ``False``, git won't checkout
605 :param update_after_clone: If set to ``False``, git won't checkout
606 working directory
606 working directory
607 :param bare: If set to ``True``, repository would be cloned into
607 :param bare: If set to ``True``, repository would be cloned into
608 *bare* git repository (no working directory at all).
608 *bare* git repository (no working directory at all).
609 """
609 """
610 url = self._get_url(url)
610 url = self._get_url(url)
611 cmd = ['clone']
611 cmd = ['clone']
612 if bare:
612 if bare:
613 cmd.append('--bare')
613 cmd.append('--bare')
614 elif not update_after_clone:
614 elif not update_after_clone:
615 cmd.append('--no-checkout')
615 cmd.append('--no-checkout')
616 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
616 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
617 cmd = ' '.join(cmd)
617 cmd = ' '.join(cmd)
618 # If error occurs run_git_command raises RepositoryError already
618 # If error occurs run_git_command raises RepositoryError already
619 self.run_git_command(cmd)
619 self.run_git_command(cmd)
620
620
621 def pull(self, url):
621 def pull(self, url):
622 """
622 """
623 Tries to pull changes from external location.
623 Tries to pull changes from external location.
624 """
624 """
625 url = self._get_url(url)
625 url = self._get_url(url)
626 cmd = ['pull']
626 cmd = ['pull']
627 cmd.append("--ff-only")
627 cmd.append("--ff-only")
628 cmd.append(url)
628 cmd.append(url)
629 cmd = ' '.join(cmd)
629 cmd = ' '.join(cmd)
630 # If error occurs run_git_command raises RepositoryError already
630 # If error occurs run_git_command raises RepositoryError already
631 self.run_git_command(cmd)
631 self.run_git_command(cmd)
632
632
633 def fetch(self, url):
633 def fetch(self, url):
634 """
634 """
635 Tries to pull changes from external location.
635 Tries to pull changes from external location.
636 """
636 """
637 url = self._get_url(url)
637 url = self._get_url(url)
638 so, se = self.run_git_command('ls-remote -h %s' % url)
638 so, se = self.run_git_command('ls-remote -h %s' % url)
639 refs = []
639 refs = []
640 for line in (x for x in so.splitlines()):
640 for line in (x for x in so.splitlines()):
641 sha, ref = line.split('\t')
641 sha, ref = line.split('\t')
642 refs.append(ref)
642 refs.append(ref)
643 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
643 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
644 cmd = '''fetch %s -- %s''' % (url, refs)
644 cmd = '''fetch %s -- %s''' % (url, refs)
645 self.run_git_command(cmd)
645 self.run_git_command(cmd)
646
646
647 @LazyProperty
647 @LazyProperty
648 def workdir(self):
648 def workdir(self):
649 """
649 """
650 Returns ``Workdir`` instance for this repository.
650 Returns ``Workdir`` instance for this repository.
651 """
651 """
652 return GitWorkdir(self)
652 return GitWorkdir(self)
653
653
654 def get_config_value(self, section, name, config_file=None):
654 def get_config_value(self, section, name, config_file=None):
655 """
655 """
656 Returns configuration value for a given [``section``] and ``name``.
656 Returns configuration value for a given [``section``] and ``name``.
657
657
658 :param section: Section we want to retrieve value from
658 :param section: Section we want to retrieve value from
659 :param name: Name of configuration we want to retrieve
659 :param name: Name of configuration we want to retrieve
660 :param config_file: A path to file which should be used to retrieve
660 :param config_file: A path to file which should be used to retrieve
661 configuration from (might also be a list of file paths)
661 configuration from (might also be a list of file paths)
662 """
662 """
663 if config_file is None:
663 if config_file is None:
664 config_file = []
664 config_file = []
665 elif isinstance(config_file, basestring):
665 elif isinstance(config_file, basestring):
666 config_file = [config_file]
666 config_file = [config_file]
667
667
668 def gen_configs():
668 def gen_configs():
669 for path in config_file + self._config_files:
669 for path in config_file + self._config_files:
670 try:
670 try:
671 yield ConfigFile.from_path(path)
671 yield ConfigFile.from_path(path)
672 except (IOError, OSError, ValueError):
672 except (IOError, OSError, ValueError):
673 continue
673 continue
674
674
675 for config in gen_configs():
675 for config in gen_configs():
676 try:
676 try:
677 return config.get(section, name)
677 return config.get(section, name)
678 except KeyError:
678 except KeyError:
679 continue
679 continue
680 return None
680 return None
681
681
682 def get_user_name(self, config_file=None):
682 def get_user_name(self, config_file=None):
683 """
683 """
684 Returns user's name from global configuration file.
684 Returns user's name from global configuration file.
685
685
686 :param config_file: A path to file which should be used to retrieve
686 :param config_file: A path to file which should be used to retrieve
687 configuration from (might also be a list of file paths)
687 configuration from (might also be a list of file paths)
688 """
688 """
689 return self.get_config_value('user', 'name', config_file)
689 return self.get_config_value('user', 'name', config_file)
690
690
691 def get_user_email(self, config_file=None):
691 def get_user_email(self, config_file=None):
692 """
692 """
693 Returns user's email from global configuration file.
693 Returns user's email from global configuration file.
694
694
695 :param config_file: A path to file which should be used to retrieve
695 :param config_file: A path to file which should be used to retrieve
696 configuration from (might also be a list of file paths)
696 configuration from (might also be a list of file paths)
697 """
697 """
698 return self.get_config_value('user', 'email', config_file)
698 return self.get_config_value('user', 'email', config_file)
@@ -1,549 +1,549
1 import os
1 import os
2 import time
2 import time
3 import datetime
3 import datetime
4 import urllib
4 import urllib
5 import urllib2
5 import urllib2
6
6
7 from rhodecode.lib.vcs.backends.base import BaseRepository
7 from rhodecode.lib.vcs.backends.base import BaseRepository
8 from .workdir import MercurialWorkdir
8 from .workdir import MercurialWorkdir
9 from .changeset import MercurialChangeset
9 from .changeset import MercurialChangeset
10 from .inmemory import MercurialInMemoryChangeset
10 from .inmemory import MercurialInMemoryChangeset
11
11
12 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \
12 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \
13 ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \
13 ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \
14 VCSError, TagAlreadyExistError, TagDoesNotExistError
14 VCSError, TagAlreadyExistError, TagDoesNotExistError
15 from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \
15 from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \
16 makedate, safe_unicode
16 makedate, safe_unicode
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
18 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
19 from rhodecode.lib.vcs.utils.paths import abspath
19 from rhodecode.lib.vcs.utils.paths import abspath
20
20
21 from rhodecode.lib.vcs.utils.hgcompat import ui, nullid, match, patch, diffopts, clone, \
21 from rhodecode.lib.vcs.utils.hgcompat import ui, nullid, match, patch, diffopts, clone, \
22 get_contact, pull, localrepository, RepoLookupError, Abort, RepoError, hex
22 get_contact, pull, localrepository, RepoLookupError, Abort, RepoError, hex
23
23
24
24
25 class MercurialRepository(BaseRepository):
25 class MercurialRepository(BaseRepository):
26 """
26 """
27 Mercurial repository backend
27 Mercurial repository backend
28 """
28 """
29 DEFAULT_BRANCH_NAME = 'default'
29 DEFAULT_BRANCH_NAME = 'default'
30 scm = 'hg'
30 scm = 'hg'
31
31
32 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
32 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
33 update_after_clone=False):
33 update_after_clone=False):
34 """
34 """
35 Raises RepositoryError if repository could not be find at the given
35 Raises RepositoryError if repository could not be find at the given
36 ``repo_path``.
36 ``repo_path``.
37
37
38 :param repo_path: local path of the repository
38 :param repo_path: local path of the repository
39 :param create=False: if set to True, would try to create repository if
39 :param create=False: if set to True, would try to create repository if
40 it does not exist rather than raising exception
40 it does not exist rather than raising exception
41 :param baseui=None: user data
41 :param baseui=None: user data
42 :param src_url=None: would try to clone repository from given location
42 :param src_url=None: would try to clone repository from given location
43 :param update_after_clone=False: sets update of working copy after
43 :param update_after_clone=False: sets update of working copy after
44 making a clone
44 making a clone
45 """
45 """
46
46
47 if not isinstance(repo_path, str):
47 if not isinstance(repo_path, str):
48 raise VCSError('Mercurial backend requires repository path to '
48 raise VCSError('Mercurial backend requires repository path to '
49 'be instance of <str> got %s instead' %
49 'be instance of <str> got %s instead' %
50 type(repo_path))
50 type(repo_path))
51
51
52 self.path = abspath(repo_path)
52 self.path = abspath(repo_path)
53 self.baseui = baseui or ui.ui()
53 self.baseui = baseui or ui.ui()
54 # We've set path and ui, now we can set _repo itself
54 # We've set path and ui, now we can set _repo itself
55 self._repo = self._get_repo(create, src_url, update_after_clone)
55 self._repo = self._get_repo(create, src_url, update_after_clone)
56
56
57 @property
57 @property
58 def _empty(self):
58 def _empty(self):
59 """
59 """
60 Checks if repository is empty without any changesets
60 Checks if repository is empty without any changesets
61 """
61 """
62 # TODO: Following raises errors when using InMemoryChangeset...
62 # TODO: Following raises errors when using InMemoryChangeset...
63 # return len(self._repo.changelog) == 0
63 # return len(self._repo.changelog) == 0
64 return len(self.revisions) == 0
64 return len(self.revisions) == 0
65
65
66 @LazyProperty
66 @LazyProperty
67 def revisions(self):
67 def revisions(self):
68 """
68 """
69 Returns list of revisions' ids, in ascending order. Being lazy
69 Returns list of revisions' ids, in ascending order. Being lazy
70 attribute allows external tools to inject shas from cache.
70 attribute allows external tools to inject shas from cache.
71 """
71 """
72 return self._get_all_revisions()
72 return self._get_all_revisions()
73
73
74 @LazyProperty
74 @LazyProperty
75 def name(self):
75 def name(self):
76 return os.path.basename(self.path)
76 return os.path.basename(self.path)
77
77
78 @LazyProperty
78 @LazyProperty
79 def branches(self):
79 def branches(self):
80 return self._get_branches()
80 return self._get_branches()
81
81
82 @LazyProperty
82 @LazyProperty
83 def allbranches(self):
83 def allbranches(self):
84 """
84 """
85 List all branches, including closed branches.
85 List all branches, including closed branches.
86 """
86 """
87 return self._get_branches(closed=True)
87 return self._get_branches(closed=True)
88
88
89 def _get_branches(self, closed=False):
89 def _get_branches(self, closed=False):
90 """
90 """
91 Get's branches for this repository
91 Get's branches for this repository
92 Returns only not closed branches by default
92 Returns only not closed branches by default
93
93
94 :param closed: return also closed branches for mercurial
94 :param closed: return also closed branches for mercurial
95 """
95 """
96
96
97 if self._empty:
97 if self._empty:
98 return {}
98 return {}
99
99
100 def _branchtags(localrepo):
100 def _branchtags(localrepo):
101 """
101 """
102 Patched version of mercurial branchtags to not return the closed
102 Patched version of mercurial branchtags to not return the closed
103 branches
103 branches
104
104
105 :param localrepo: locarepository instance
105 :param localrepo: locarepository instance
106 """
106 """
107
107
108 bt = {}
108 bt = {}
109 bt_closed = {}
109 bt_closed = {}
110 for bn, heads in localrepo.branchmap().iteritems():
110 for bn, heads in localrepo.branchmap().iteritems():
111 tip = heads[-1]
111 tip = heads[-1]
112 if 'close' in localrepo.changelog.read(tip)[5]:
112 if 'close' in localrepo.changelog.read(tip)[5]:
113 bt_closed[bn] = tip
113 bt_closed[bn] = tip
114 else:
114 else:
115 bt[bn] = tip
115 bt[bn] = tip
116
116
117 if closed:
117 if closed:
118 bt.update(bt_closed)
118 bt.update(bt_closed)
119 return bt
119 return bt
120
120
121 sortkey = lambda ctx: ctx[0] # sort by name
121 sortkey = lambda ctx: ctx[0] # sort by name
122 _branches = [(safe_unicode(n), hex(h),) for n, h in
122 _branches = [(safe_unicode(n), hex(h),) for n, h in
123 _branchtags(self._repo).items()]
123 _branchtags(self._repo).items()]
124
124
125 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
125 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
126
126
127 @LazyProperty
127 @LazyProperty
128 def tags(self):
128 def tags(self):
129 """
129 """
130 Get's tags for this repository
130 Get's tags for this repository
131 """
131 """
132 return self._get_tags()
132 return self._get_tags()
133
133
134 def _get_tags(self):
134 def _get_tags(self):
135 if self._empty:
135 if self._empty:
136 return {}
136 return {}
137
137
138 sortkey = lambda ctx: ctx[0] # sort by name
138 sortkey = lambda ctx: ctx[0] # sort by name
139 _tags = [(safe_unicode(n), hex(h),) for n, h in
139 _tags = [(safe_unicode(n), hex(h),) for n, h in
140 self._repo.tags().items()]
140 self._repo.tags().items()]
141
141
142 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
142 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
143
143
144 def tag(self, name, user, revision=None, message=None, date=None,
144 def tag(self, name, user, revision=None, message=None, date=None,
145 **kwargs):
145 **kwargs):
146 """
146 """
147 Creates and returns a tag for the given ``revision``.
147 Creates and returns a tag for the given ``revision``.
148
148
149 :param name: name for new tag
149 :param name: name for new tag
150 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
150 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
151 :param revision: changeset id for which new tag would be created
151 :param revision: changeset id for which new tag would be created
152 :param message: message of the tag's commit
152 :param message: message of the tag's commit
153 :param date: date of tag's commit
153 :param date: date of tag's commit
154
154
155 :raises TagAlreadyExistError: if tag with same name already exists
155 :raises TagAlreadyExistError: if tag with same name already exists
156 """
156 """
157 if name in self.tags:
157 if name in self.tags:
158 raise TagAlreadyExistError("Tag %s already exists" % name)
158 raise TagAlreadyExistError("Tag %s already exists" % name)
159 changeset = self.get_changeset(revision)
159 changeset = self.get_changeset(revision)
160 local = kwargs.setdefault('local', False)
160 local = kwargs.setdefault('local', False)
161
161
162 if message is None:
162 if message is None:
163 message = "Added tag %s for changeset %s" % (name,
163 message = "Added tag %s for changeset %s" % (name,
164 changeset.short_id)
164 changeset.short_id)
165
165
166 if date is None:
166 if date is None:
167 date = datetime.datetime.now().ctime()
167 date = datetime.datetime.now().ctime()
168
168
169 try:
169 try:
170 self._repo.tag(name, changeset._ctx.node(), message, local, user,
170 self._repo.tag(name, changeset._ctx.node(), message, local, user,
171 date)
171 date)
172 except Abort, e:
172 except Abort, e:
173 raise RepositoryError(e.message)
173 raise RepositoryError(e.message)
174
174
175 # Reinitialize tags
175 # Reinitialize tags
176 self.tags = self._get_tags()
176 self.tags = self._get_tags()
177 tag_id = self.tags[name]
177 tag_id = self.tags[name]
178
178
179 return self.get_changeset(revision=tag_id)
179 return self.get_changeset(revision=tag_id)
180
180
181 def remove_tag(self, name, user, message=None, date=None):
181 def remove_tag(self, name, user, message=None, date=None):
182 """
182 """
183 Removes tag with the given ``name``.
183 Removes tag with the given ``name``.
184
184
185 :param name: name of the tag to be removed
185 :param name: name of the tag to be removed
186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 :param message: message of the tag's removal commit
187 :param message: message of the tag's removal commit
188 :param date: date of tag's removal commit
188 :param date: date of tag's removal commit
189
189
190 :raises TagDoesNotExistError: if tag with given name does not exists
190 :raises TagDoesNotExistError: if tag with given name does not exists
191 """
191 """
192 if name not in self.tags:
192 if name not in self.tags:
193 raise TagDoesNotExistError("Tag %s does not exist" % name)
193 raise TagDoesNotExistError("Tag %s does not exist" % name)
194 if message is None:
194 if message is None:
195 message = "Removed tag %s" % name
195 message = "Removed tag %s" % name
196 if date is None:
196 if date is None:
197 date = datetime.datetime.now().ctime()
197 date = datetime.datetime.now().ctime()
198 local = False
198 local = False
199
199
200 try:
200 try:
201 self._repo.tag(name, nullid, message, local, user, date)
201 self._repo.tag(name, nullid, message, local, user, date)
202 self.tags = self._get_tags()
202 self.tags = self._get_tags()
203 except Abort, e:
203 except Abort, e:
204 raise RepositoryError(e.message)
204 raise RepositoryError(e.message)
205
205
206 @LazyProperty
206 @LazyProperty
207 def bookmarks(self):
207 def bookmarks(self):
208 """
208 """
209 Get's bookmarks for this repository
209 Get's bookmarks for this repository
210 """
210 """
211 return self._get_bookmarks()
211 return self._get_bookmarks()
212
212
213 def _get_bookmarks(self):
213 def _get_bookmarks(self):
214 if self._empty:
214 if self._empty:
215 return {}
215 return {}
216
216
217 sortkey = lambda ctx: ctx[0] # sort by name
217 sortkey = lambda ctx: ctx[0] # sort by name
218 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
218 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
219 self._repo._bookmarks.items()]
219 self._repo._bookmarks.items()]
220 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
220 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
221
221
222 def _get_all_revisions(self):
222 def _get_all_revisions(self):
223
223
224 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
224 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
225
225
226 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
226 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
227 context=3):
227 context=3):
228 """
228 """
229 Returns (git like) *diff*, as plain text. Shows changes introduced by
229 Returns (git like) *diff*, as plain text. Shows changes introduced by
230 ``rev2`` since ``rev1``.
230 ``rev2`` since ``rev1``.
231
231
232 :param rev1: Entry point from which diff is shown. Can be
232 :param rev1: Entry point from which diff is shown. Can be
233 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
233 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
234 the changes since empty state of the repository until ``rev2``
234 the changes since empty state of the repository until ``rev2``
235 :param rev2: Until which revision changes should be shown.
235 :param rev2: Until which revision changes should be shown.
236 :param ignore_whitespace: If set to ``True``, would not show whitespace
236 :param ignore_whitespace: If set to ``True``, would not show whitespace
237 changes. Defaults to ``False``.
237 changes. Defaults to ``False``.
238 :param context: How many lines before/after changed lines should be
238 :param context: How many lines before/after changed lines should be
239 shown. Defaults to ``3``.
239 shown. Defaults to ``3``.
240 """
240 """
241 if hasattr(rev1, 'raw_id'):
241 if hasattr(rev1, 'raw_id'):
242 rev1 = getattr(rev1, 'raw_id')
242 rev1 = getattr(rev1, 'raw_id')
243
243
244 if hasattr(rev2, 'raw_id'):
244 if hasattr(rev2, 'raw_id'):
245 rev2 = getattr(rev2, 'raw_id')
245 rev2 = getattr(rev2, 'raw_id')
246
246
247 # Check if given revisions are present at repository (may raise
247 # Check if given revisions are present at repository (may raise
248 # ChangesetDoesNotExistError)
248 # ChangesetDoesNotExistError)
249 if rev1 != self.EMPTY_CHANGESET:
249 if rev1 != self.EMPTY_CHANGESET:
250 self.get_changeset(rev1)
250 self.get_changeset(rev1)
251 self.get_changeset(rev2)
251 self.get_changeset(rev2)
252 if path:
252 if path:
253 file_filter = match(self.path, '', [path])
253 file_filter = match(self.path, '', [path])
254 else:
254 else:
255 file_filter = None
255 file_filter = None
256
256
257 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
257 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
258 opts=diffopts(git=True,
258 opts=diffopts(git=True,
259 ignorews=ignore_whitespace,
259 ignorews=ignore_whitespace,
260 context=context)))
260 context=context)))
261
261
262 @classmethod
262 @classmethod
263 def _check_url(cls, url):
263 def _check_url(cls, url):
264 """
264 """
265 Function will check given url and try to verify if it's a valid
265 Function will check given url and try to verify if it's a valid
266 link. Sometimes it may happened that mercurial will issue basic
266 link. Sometimes it may happened that mercurial will issue basic
267 auth request that can cause whole API to hang when used from python
267 auth request that can cause whole API to hang when used from python
268 or other external calls.
268 or other external calls.
269
269
270 On failures it'll raise urllib2.HTTPError, return code 200 if url
270 On failures it'll raise urllib2.HTTPError, return code 200 if url
271 is valid or True if it's a local path
271 is valid or True if it's a local path
272 """
272 """
273
273
274 from mercurial.util import url as Url
274 from mercurial.util import url as Url
275
275
276 # those authnadlers are patched for python 2.6.5 bug an
276 # those authnadlers are patched for python 2.6.5 bug an
277 # infinit looping when given invalid resources
277 # infinit looping when given invalid resources
278 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
278 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
279
279
280 # check first if it's not an local url
280 # check first if it's not an local url
281 if os.path.isdir(url) or url.startswith('file:'):
281 if os.path.isdir(url) or url.startswith('file:'):
282 return True
282 return True
283
283
284 if('+' in url[:url.find('://')]):
284 if('+' in url[:url.find('://')]):
285 url = url[url.find('+') + 1:]
285 url = url[url.find('+') + 1:]
286
286
287 handlers = []
287 handlers = []
288 test_uri, authinfo = Url(url).authinfo()
288 test_uri, authinfo = Url(url).authinfo()
289
289
290 if authinfo:
290 if authinfo:
291 #create a password manager
291 #create a password manager
292 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
292 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
293 passmgr.add_password(*authinfo)
293 passmgr.add_password(*authinfo)
294
294
295 handlers.extend((httpbasicauthhandler(passmgr),
295 handlers.extend((httpbasicauthhandler(passmgr),
296 httpdigestauthhandler(passmgr)))
296 httpdigestauthhandler(passmgr)))
297
297
298 o = urllib2.build_opener(*handlers)
298 o = urllib2.build_opener(*handlers)
299 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
299 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
300 ('Accept', 'application/mercurial-0.1')]
300 ('Accept', 'application/mercurial-0.1')]
301
301
302 q = {"cmd": 'between'}
302 q = {"cmd": 'between'}
303 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
303 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
304 qs = '?%s' % urllib.urlencode(q)
304 qs = '?%s' % urllib.urlencode(q)
305 cu = "%s%s" % (test_uri, qs)
305 cu = "%s%s" % (test_uri, qs)
306 req = urllib2.Request(cu, None, {})
306 req = urllib2.Request(cu, None, {})
307
307
308 try:
308 try:
309 resp = o.open(req)
309 resp = o.open(req)
310 return resp.code == 200
310 return resp.code == 200
311 except Exception, e:
311 except Exception, e:
312 # means it cannot be cloned
312 # means it cannot be cloned
313 raise urllib2.URLError("[%s] %s" % (url, e))
313 raise urllib2.URLError("[%s] %s" % (url, e))
314
314
315 def _get_repo(self, create, src_url=None, update_after_clone=False):
315 def _get_repo(self, create, src_url=None, update_after_clone=False):
316 """
316 """
317 Function will check for mercurial repository in given path and return
317 Function will check for mercurial repository in given path and return
318 a localrepo object. If there is no repository in that path it will
318 a localrepo object. If there is no repository in that path it will
319 raise an exception unless ``create`` parameter is set to True - in
319 raise an exception unless ``create`` parameter is set to True - in
320 that case repository would be created and returned.
320 that case repository would be created and returned.
321 If ``src_url`` is given, would try to clone repository from the
321 If ``src_url`` is given, would try to clone repository from the
322 location at given clone_point. Additionally it'll make update to
322 location at given clone_point. Additionally it'll make update to
323 working copy accordingly to ``update_after_clone`` flag
323 working copy accordingly to ``update_after_clone`` flag
324 """
324 """
325
325
326 try:
326 try:
327 if src_url:
327 if src_url:
328 url = str(self._get_url(src_url))
328 url = str(self._get_url(src_url))
329 opts = {}
329 opts = {}
330 if not update_after_clone:
330 if not update_after_clone:
331 opts.update({'noupdate': True})
331 opts.update({'noupdate': True})
332 try:
332 try:
333 MercurialRepository._check_url(url)
333 MercurialRepository._check_url(url)
334 clone(self.baseui, url, self.path, **opts)
334 clone(self.baseui, url, self.path, **opts)
335 # except urllib2.URLError:
335 # except urllib2.URLError:
336 # raise Abort("Got HTTP 404 error")
336 # raise Abort("Got HTTP 404 error")
337 except Exception:
337 except Exception:
338 raise
338 raise
339
339
340 # Don't try to create if we've already cloned repo
340 # Don't try to create if we've already cloned repo
341 create = False
341 create = False
342 return localrepository(self.baseui, self.path, create=create)
342 return localrepository(self.baseui, self.path, create=create)
343 except (Abort, RepoError), err:
343 except (Abort, RepoError), err:
344 if create:
344 if create:
345 msg = "Cannot create repository at %s. Original error was %s"\
345 msg = "Cannot create repository at %s. Original error was %s"\
346 % (self.path, err)
346 % (self.path, err)
347 else:
347 else:
348 msg = "Not valid repository at %s. Original error was %s"\
348 msg = "Not valid repository at %s. Original error was %s"\
349 % (self.path, err)
349 % (self.path, err)
350 raise RepositoryError(msg)
350 raise RepositoryError(msg)
351
351
352 @LazyProperty
352 @LazyProperty
353 def in_memory_changeset(self):
353 def in_memory_changeset(self):
354 return MercurialInMemoryChangeset(self)
354 return MercurialInMemoryChangeset(self)
355
355
356 @LazyProperty
356 @LazyProperty
357 def description(self):
357 def description(self):
358 undefined_description = u'unknown'
358 undefined_description = u'unknown'
359 return safe_unicode(self._repo.ui.config('web', 'description',
359 return safe_unicode(self._repo.ui.config('web', 'description',
360 undefined_description, untrusted=True))
360 undefined_description, untrusted=True))
361
361
362 @LazyProperty
362 @LazyProperty
363 def contact(self):
363 def contact(self):
364 undefined_contact = u'Unknown'
364 undefined_contact = u'Unknown'
365 return safe_unicode(get_contact(self._repo.ui.config)
365 return safe_unicode(get_contact(self._repo.ui.config)
366 or undefined_contact)
366 or undefined_contact)
367
367
368 @LazyProperty
368 @LazyProperty
369 def last_change(self):
369 def last_change(self):
370 """
370 """
371 Returns last change made on this repository as datetime object
371 Returns last change made on this repository as datetime object
372 """
372 """
373 return date_fromtimestamp(self._get_mtime(), makedate()[1])
373 return date_fromtimestamp(self._get_mtime(), makedate()[1])
374
374
375 def _get_mtime(self):
375 def _get_mtime(self):
376 try:
376 try:
377 return time.mktime(self.get_changeset().date.timetuple())
377 return time.mktime(self.get_changeset().date.timetuple())
378 except RepositoryError:
378 except RepositoryError:
379 #fallback to filesystem
379 #fallback to filesystem
380 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
380 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
381 st_path = os.path.join(self.path, '.hg', "store")
381 st_path = os.path.join(self.path, '.hg', "store")
382 if os.path.exists(cl_path):
382 if os.path.exists(cl_path):
383 return os.stat(cl_path).st_mtime
383 return os.stat(cl_path).st_mtime
384 else:
384 else:
385 return os.stat(st_path).st_mtime
385 return os.stat(st_path).st_mtime
386
386
387 def _get_hidden(self):
387 def _get_hidden(self):
388 return self._repo.ui.configbool("web", "hidden", untrusted=True)
388 return self._repo.ui.configbool("web", "hidden", untrusted=True)
389
389
390 def _get_revision(self, revision):
390 def _get_revision(self, revision):
391 """
391 """
392 Get's an ID revision given as str. This will always return a fill
392 Get's an ID revision given as str. This will always return a fill
393 40 char revision number
393 40 char revision number
394
394
395 :param revision: str or int or None
395 :param revision: str or int or None
396 """
396 """
397
397
398 if self._empty:
398 if self._empty:
399 raise EmptyRepositoryError("There are no changesets yet")
399 raise EmptyRepositoryError("There are no changesets yet")
400
400
401 if revision in [-1, 'tip', None]:
401 if revision in [-1, 'tip', None]:
402 revision = 'tip'
402 revision = 'tip'
403
403
404 try:
404 try:
405 revision = hex(self._repo.lookup(revision))
405 revision = hex(self._repo.lookup(revision))
406 except (IndexError, ValueError, RepoLookupError, TypeError):
406 except (IndexError, ValueError, RepoLookupError, TypeError):
407 raise ChangesetDoesNotExistError("Revision %r does not "
407 raise ChangesetDoesNotExistError("Revision %r does not "
408 "exist for this repository %s" \
408 "exist for this repository"
409 % (revision, self))
409 % (revision))
410 return revision
410 return revision
411
411
412 def _get_archives(self, archive_name='tip'):
412 def _get_archives(self, archive_name='tip'):
413 allowed = self.baseui.configlist("web", "allow_archive",
413 allowed = self.baseui.configlist("web", "allow_archive",
414 untrusted=True)
414 untrusted=True)
415 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
415 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
416 if i[0] in allowed or self._repo.ui.configbool("web",
416 if i[0] in allowed or self._repo.ui.configbool("web",
417 "allow" + i[0],
417 "allow" + i[0],
418 untrusted=True):
418 untrusted=True):
419 yield {"type": i[0], "extension": i[1], "node": archive_name}
419 yield {"type": i[0], "extension": i[1], "node": archive_name}
420
420
421 def _get_url(self, url):
421 def _get_url(self, url):
422 """
422 """
423 Returns normalized url. If schema is not given, would fall
423 Returns normalized url. If schema is not given, would fall
424 to filesystem
424 to filesystem
425 (``file:///``) schema.
425 (``file:///``) schema.
426 """
426 """
427 url = str(url)
427 url = str(url)
428 if url != 'default' and not '://' in url:
428 if url != 'default' and not '://' in url:
429 url = "file:" + urllib.pathname2url(url)
429 url = "file:" + urllib.pathname2url(url)
430 return url
430 return url
431
431
432 def get_hook_location(self):
432 def get_hook_location(self):
433 """
433 """
434 returns absolute path to location where hooks are stored
434 returns absolute path to location where hooks are stored
435 """
435 """
436 return os.path.join(self.path, '.hg', '.hgrc')
436 return os.path.join(self.path, '.hg', '.hgrc')
437
437
438 def get_changeset(self, revision=None):
438 def get_changeset(self, revision=None):
439 """
439 """
440 Returns ``MercurialChangeset`` object representing repository's
440 Returns ``MercurialChangeset`` object representing repository's
441 changeset at the given ``revision``.
441 changeset at the given ``revision``.
442 """
442 """
443 revision = self._get_revision(revision)
443 revision = self._get_revision(revision)
444 changeset = MercurialChangeset(repository=self, revision=revision)
444 changeset = MercurialChangeset(repository=self, revision=revision)
445 return changeset
445 return changeset
446
446
447 def get_changesets(self, start=None, end=None, start_date=None,
447 def get_changesets(self, start=None, end=None, start_date=None,
448 end_date=None, branch_name=None, reverse=False):
448 end_date=None, branch_name=None, reverse=False):
449 """
449 """
450 Returns iterator of ``MercurialChangeset`` objects from start to end
450 Returns iterator of ``MercurialChangeset`` objects from start to end
451 (both are inclusive)
451 (both are inclusive)
452
452
453 :param start: None, str, int or mercurial lookup format
453 :param start: None, str, int or mercurial lookup format
454 :param end: None, str, int or mercurial lookup format
454 :param end: None, str, int or mercurial lookup format
455 :param start_date:
455 :param start_date:
456 :param end_date:
456 :param end_date:
457 :param branch_name:
457 :param branch_name:
458 :param reversed: return changesets in reversed order
458 :param reversed: return changesets in reversed order
459 """
459 """
460
460
461 start_raw_id = self._get_revision(start)
461 start_raw_id = self._get_revision(start)
462 start_pos = self.revisions.index(start_raw_id) if start else None
462 start_pos = self.revisions.index(start_raw_id) if start else None
463 end_raw_id = self._get_revision(end)
463 end_raw_id = self._get_revision(end)
464 end_pos = self.revisions.index(end_raw_id) if end else None
464 end_pos = self.revisions.index(end_raw_id) if end else None
465
465
466 if None not in [start, end] and start_pos > end_pos:
466 if None not in [start, end] and start_pos > end_pos:
467 raise RepositoryError("Start revision '%s' cannot be "
467 raise RepositoryError("Start revision '%s' cannot be "
468 "after end revision '%s'" % (start, end))
468 "after end revision '%s'" % (start, end))
469
469
470 if branch_name and branch_name not in self.allbranches.keys():
470 if branch_name and branch_name not in self.allbranches.keys():
471 raise BranchDoesNotExistError('Branch %s not found in'
471 raise BranchDoesNotExistError('Branch %s not found in'
472 ' this repository' % branch_name)
472 ' this repository' % branch_name)
473 if end_pos is not None:
473 if end_pos is not None:
474 end_pos += 1
474 end_pos += 1
475
475
476 slice_ = reversed(self.revisions[start_pos:end_pos]) if reverse else \
476 slice_ = reversed(self.revisions[start_pos:end_pos]) if reverse else \
477 self.revisions[start_pos:end_pos]
477 self.revisions[start_pos:end_pos]
478
478
479 for id_ in slice_:
479 for id_ in slice_:
480 cs = self.get_changeset(id_)
480 cs = self.get_changeset(id_)
481 if branch_name and cs.branch != branch_name:
481 if branch_name and cs.branch != branch_name:
482 continue
482 continue
483 if start_date and cs.date < start_date:
483 if start_date and cs.date < start_date:
484 continue
484 continue
485 if end_date and cs.date > end_date:
485 if end_date and cs.date > end_date:
486 continue
486 continue
487
487
488 yield cs
488 yield cs
489
489
490 def pull(self, url):
490 def pull(self, url):
491 """
491 """
492 Tries to pull changes from external location.
492 Tries to pull changes from external location.
493 """
493 """
494 url = self._get_url(url)
494 url = self._get_url(url)
495 try:
495 try:
496 pull(self.baseui, self._repo, url)
496 pull(self.baseui, self._repo, url)
497 except Abort, err:
497 except Abort, err:
498 # Propagate error but with vcs's type
498 # Propagate error but with vcs's type
499 raise RepositoryError(str(err))
499 raise RepositoryError(str(err))
500
500
501 @LazyProperty
501 @LazyProperty
502 def workdir(self):
502 def workdir(self):
503 """
503 """
504 Returns ``Workdir`` instance for this repository.
504 Returns ``Workdir`` instance for this repository.
505 """
505 """
506 return MercurialWorkdir(self)
506 return MercurialWorkdir(self)
507
507
508 def get_config_value(self, section, name=None, config_file=None):
508 def get_config_value(self, section, name=None, config_file=None):
509 """
509 """
510 Returns configuration value for a given [``section``] and ``name``.
510 Returns configuration value for a given [``section``] and ``name``.
511
511
512 :param section: Section we want to retrieve value from
512 :param section: Section we want to retrieve value from
513 :param name: Name of configuration we want to retrieve
513 :param name: Name of configuration we want to retrieve
514 :param config_file: A path to file which should be used to retrieve
514 :param config_file: A path to file which should be used to retrieve
515 configuration from (might also be a list of file paths)
515 configuration from (might also be a list of file paths)
516 """
516 """
517 if config_file is None:
517 if config_file is None:
518 config_file = []
518 config_file = []
519 elif isinstance(config_file, basestring):
519 elif isinstance(config_file, basestring):
520 config_file = [config_file]
520 config_file = [config_file]
521
521
522 config = self._repo.ui
522 config = self._repo.ui
523 for path in config_file:
523 for path in config_file:
524 config.readconfig(path)
524 config.readconfig(path)
525 return config.config(section, name)
525 return config.config(section, name)
526
526
527 def get_user_name(self, config_file=None):
527 def get_user_name(self, config_file=None):
528 """
528 """
529 Returns user's name from global configuration file.
529 Returns user's name from global configuration file.
530
530
531 :param config_file: A path to file which should be used to retrieve
531 :param config_file: A path to file which should be used to retrieve
532 configuration from (might also be a list of file paths)
532 configuration from (might also be a list of file paths)
533 """
533 """
534 username = self.get_config_value('ui', 'username')
534 username = self.get_config_value('ui', 'username')
535 if username:
535 if username:
536 return author_name(username)
536 return author_name(username)
537 return None
537 return None
538
538
539 def get_user_email(self, config_file=None):
539 def get_user_email(self, config_file=None):
540 """
540 """
541 Returns user's email from global configuration file.
541 Returns user's email from global configuration file.
542
542
543 :param config_file: A path to file which should be used to retrieve
543 :param config_file: A path to file which should be used to retrieve
544 configuration from (might also be a list of file paths)
544 configuration from (might also be a list of file paths)
545 """
545 """
546 username = self.get_config_value('ui', 'username')
546 username = self.get_config_value('ui', 'username')
547 if username:
547 if username:
548 return author_email(username)
548 return author_email(username)
549 return None
549 return None
@@ -1,357 +1,355
1 from rhodecode.tests import *
1 from rhodecode.tests import *
2 from rhodecode.model.db import Repository
2 from rhodecode.model.db import Repository
3 from rhodecode.model.meta import Session
3 from rhodecode.model.meta import Session
4
4
5 ARCHIVE_SPECS = {
5 ARCHIVE_SPECS = {
6 '.tar.bz2': ('application/x-bzip2', 'tbz2', ''),
6 '.tar.bz2': ('application/x-bzip2', 'tbz2', ''),
7 '.tar.gz': ('application/x-gzip', 'tgz', ''),
7 '.tar.gz': ('application/x-gzip', 'tgz', ''),
8 '.zip': ('application/zip', 'zip', ''),
8 '.zip': ('application/zip', 'zip', ''),
9 }
9 }
10
10
11
11
12 def _set_downloads(repo_name, set_to):
12 def _set_downloads(repo_name, set_to):
13 repo = Repository.get_by_repo_name(repo_name)
13 repo = Repository.get_by_repo_name(repo_name)
14 repo.enable_downloads = set_to
14 repo.enable_downloads = set_to
15 Session().add(repo)
15 Session().add(repo)
16 Session().commit()
16 Session().commit()
17
17
18
18
19 class TestFilesController(TestController):
19 class TestFilesController(TestController):
20
20
21 def test_index(self):
21 def test_index(self):
22 self.log_user()
22 self.log_user()
23 response = self.app.get(url(controller='files', action='index',
23 response = self.app.get(url(controller='files', action='index',
24 repo_name=HG_REPO,
24 repo_name=HG_REPO,
25 revision='tip',
25 revision='tip',
26 f_path='/'))
26 f_path='/'))
27 # Test response...
27 # Test response...
28 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/docs">docs</a>')
28 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/docs">docs</a>')
29 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/tests">tests</a>')
29 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/tests">tests</a>')
30 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/vcs">vcs</a>')
30 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/vcs">vcs</a>')
31 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/.hgignore">.hgignore</a>')
31 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/.hgignore">.hgignore</a>')
32 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/MANIFEST.in">MANIFEST.in</a>')
32 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/MANIFEST.in">MANIFEST.in</a>')
33
33
34 def test_index_revision(self):
34 def test_index_revision(self):
35 self.log_user()
35 self.log_user()
36
36
37 response = self.app.get(
37 response = self.app.get(
38 url(controller='files', action='index',
38 url(controller='files', action='index',
39 repo_name=HG_REPO,
39 repo_name=HG_REPO,
40 revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
40 revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
41 f_path='/')
41 f_path='/')
42 )
42 )
43
43
44 #Test response...
44 #Test response...
45
45
46 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs">docs</a>')
46 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs">docs</a>')
47 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests">tests</a>')
47 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests">tests</a>')
48 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst">README.rst</a>')
48 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst">README.rst</a>')
49 response.mustcontain('1.1 KiB')
49 response.mustcontain('1.1 KiB')
50 response.mustcontain('text/x-python')
50 response.mustcontain('text/x-python')
51
51
52 def test_index_different_branch(self):
52 def test_index_different_branch(self):
53 self.log_user()
53 self.log_user()
54
54
55 response = self.app.get(url(controller='files', action='index',
55 response = self.app.get(url(controller='files', action='index',
56 repo_name=HG_REPO,
56 repo_name=HG_REPO,
57 revision='97e8b885c04894463c51898e14387d80c30ed1ee',
57 revision='97e8b885c04894463c51898e14387d80c30ed1ee',
58 f_path='/'))
58 f_path='/'))
59
59
60 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: git</a></span>""")
60 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: git</a></span>""")
61
61
62 def test_index_paging(self):
62 def test_index_paging(self):
63 self.log_user()
63 self.log_user()
64
64
65 for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'),
65 for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'),
66 (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'),
66 (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'),
67 (109, '75feb4c33e81186c87eac740cee2447330288412'),
67 (109, '75feb4c33e81186c87eac740cee2447330288412'),
68 (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
68 (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
69 (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
69 (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
70
70
71 response = self.app.get(url(controller='files', action='index',
71 response = self.app.get(url(controller='files', action='index',
72 repo_name=HG_REPO,
72 repo_name=HG_REPO,
73 revision=r[1],
73 revision=r[1],
74 f_path='/'))
74 f_path='/'))
75
75
76 response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12]))
76 response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12]))
77
77
78 def test_file_source(self):
78 def test_file_source(self):
79 self.log_user()
79 self.log_user()
80 response = self.app.get(url(controller='files', action='index',
80 response = self.app.get(url(controller='files', action='index',
81 repo_name=HG_REPO,
81 repo_name=HG_REPO,
82 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
82 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
83 f_path='vcs/nodes.py'))
83 f_path='vcs/nodes.py'))
84
84
85 response.mustcontain("""<div class="commit">Partially implemented <a class="issue-tracker-link" href="https://myissueserver.com/vcs_test_hg/issue/16">#16</a>. filecontent/commit message/author/node name are safe_unicode now.
85 response.mustcontain("""<div class="commit">Partially implemented <a class="issue-tracker-link" href="https://myissueserver.com/vcs_test_hg/issue/16">#16</a>. filecontent/commit message/author/node name are safe_unicode now.
86 In addition some other __str__ are unicode as well
86 In addition some other __str__ are unicode as well
87 Added test for unicode
87 Added test for unicode
88 Improved test to clone into uniq repository.
88 Improved test to clone into uniq repository.
89 removed extra unicode conversion in diff.</div>
89 removed extra unicode conversion in diff.</div>
90 """)
90 """)
91
91
92 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
92 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
93
93
94 def test_file_source_history(self):
94 def test_file_source_history(self):
95 self.log_user()
95 self.log_user()
96 response = self.app.get(url(controller='files', action='history',
96 response = self.app.get(url(controller='files', action='history',
97 repo_name=HG_REPO,
97 repo_name=HG_REPO,
98 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
98 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
99 f_path='vcs/nodes.py'),
99 f_path='vcs/nodes.py'),
100 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
100 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
101 #test or history
101 #test or history
102 response.mustcontain("""<optgroup label="Changesets">
102 response.mustcontain("""<optgroup label="Changesets">
103 <option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
103 <option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
104 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
104 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
105 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
105 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
106 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
106 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
107 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
107 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
108 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
108 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
109 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
109 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
110 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
110 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
111 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
111 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
112 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
112 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
113 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
113 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
114 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
114 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
115 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
115 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
116 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
116 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
117 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
117 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
118 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
118 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
119 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
119 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
120 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
120 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
121 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
121 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
122 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
122 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
123 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
123 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
124 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
124 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
125 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
125 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
126 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
126 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
127 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
127 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
128 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
128 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
129 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
129 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
130 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
130 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
131 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
131 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
132 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
132 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
133 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
133 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
134 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
134 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
135 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
135 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
136 </optgroup>
136 </optgroup>
137 <optgroup label="Branches">
137 <optgroup label="Branches">
138 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
138 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
139 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
139 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
140 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
140 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
141 </optgroup>
141 </optgroup>
142 <optgroup label="Tags">
142 <optgroup label="Tags">
143 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
143 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
144 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
144 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
145 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
145 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
146 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
146 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
147 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
147 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
148 </optgroup>
148 </optgroup>
149 """)
149 """)
150
150
151 def test_file_annotation(self):
151 def test_file_annotation(self):
152 self.log_user()
152 self.log_user()
153 response = self.app.get(url(controller='files', action='index',
153 response = self.app.get(url(controller='files', action='index',
154 repo_name=HG_REPO,
154 repo_name=HG_REPO,
155 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
155 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
156 f_path='vcs/nodes.py',
156 f_path='vcs/nodes.py',
157 annotate=True))
157 annotate=True))
158
158
159 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
159 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
160
160
161 def test_file_annotation_history(self):
161 def test_file_annotation_history(self):
162 self.log_user()
162 self.log_user()
163 response = self.app.get(url(controller='files', action='history',
163 response = self.app.get(url(controller='files', action='history',
164 repo_name=HG_REPO,
164 repo_name=HG_REPO,
165 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
165 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
166 f_path='vcs/nodes.py',
166 f_path='vcs/nodes.py',
167 annotate=True),
167 annotate=True),
168 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
168 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
169
169
170 response.mustcontain("""<optgroup label="Changesets">
170 response.mustcontain("""<optgroup label="Changesets">
171 <option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
171 <option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
172 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
172 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
173 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
173 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
174 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
174 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
175 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
175 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
176 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
176 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
177 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
177 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
178 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
178 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
179 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
179 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
180 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
180 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
181 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
181 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
182 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
182 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
183 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
183 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
184 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
184 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
185 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
185 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
186 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
186 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
187 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
187 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
188 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
188 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
189 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
189 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
190 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
190 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
191 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
191 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
192 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
192 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
193 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
193 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
194 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
194 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
195 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
195 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
196 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
196 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
197 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
197 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
198 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
198 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
199 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
199 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
200 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
200 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
201 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
201 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
202 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
202 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
203 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
203 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
204 </optgroup>
204 </optgroup>
205 <optgroup label="Branches">
205 <optgroup label="Branches">
206 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
206 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
207 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
207 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
208 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
208 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
209 </optgroup>
209 </optgroup>
210 <optgroup label="Tags">
210 <optgroup label="Tags">
211 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
211 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
212 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
212 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
213 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
213 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
214 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
214 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
215 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
215 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
216 </optgroup>""")
216 </optgroup>""")
217
217
218 def test_file_annotation_git(self):
218 def test_file_annotation_git(self):
219 self.log_user()
219 self.log_user()
220 response = self.app.get(url(controller='files', action='index',
220 response = self.app.get(url(controller='files', action='index',
221 repo_name=GIT_REPO,
221 repo_name=GIT_REPO,
222 revision='master',
222 revision='master',
223 f_path='vcs/nodes.py',
223 f_path='vcs/nodes.py',
224 annotate=True))
224 annotate=True))
225
225
226 def test_archival(self):
226 def test_archival(self):
227 self.log_user()
227 self.log_user()
228 _set_downloads(HG_REPO, set_to=True)
228 _set_downloads(HG_REPO, set_to=True)
229 for arch_ext, info in ARCHIVE_SPECS.items():
229 for arch_ext, info in ARCHIVE_SPECS.items():
230 short = '27cd5cce30c9%s' % arch_ext
230 short = '27cd5cce30c9%s' % arch_ext
231 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
231 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
232 filename = '%s-%s' % (HG_REPO, short)
232 filename = '%s-%s' % (HG_REPO, short)
233 response = self.app.get(url(controller='files',
233 response = self.app.get(url(controller='files',
234 action='archivefile',
234 action='archivefile',
235 repo_name=HG_REPO,
235 repo_name=HG_REPO,
236 fname=fname))
236 fname=fname))
237
237
238 self.assertEqual(response.status, '200 OK')
238 self.assertEqual(response.status, '200 OK')
239 heads = [
239 heads = [
240 ('Pragma', 'no-cache'),
240 ('Pragma', 'no-cache'),
241 ('Cache-Control', 'no-cache'),
241 ('Cache-Control', 'no-cache'),
242 ('Content-Disposition', 'attachment; filename=%s' % filename),
242 ('Content-Disposition', 'attachment; filename=%s' % filename),
243 ('Content-Type', '%s; charset=utf-8' % info[0]),
243 ('Content-Type', '%s; charset=utf-8' % info[0]),
244 ]
244 ]
245 self.assertEqual(response.response._headers.items(), heads)
245 self.assertEqual(response.response._headers.items(), heads)
246
246
247 def test_archival_wrong_ext(self):
247 def test_archival_wrong_ext(self):
248 self.log_user()
248 self.log_user()
249 _set_downloads(HG_REPO, set_to=True)
249 _set_downloads(HG_REPO, set_to=True)
250 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
250 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
251 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
251 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
252
252
253 response = self.app.get(url(controller='files',
253 response = self.app.get(url(controller='files',
254 action='archivefile',
254 action='archivefile',
255 repo_name=HG_REPO,
255 repo_name=HG_REPO,
256 fname=fname))
256 fname=fname))
257 response.mustcontain('Unknown archive type')
257 response.mustcontain('Unknown archive type')
258
258
259 def test_archival_wrong_revision(self):
259 def test_archival_wrong_revision(self):
260 self.log_user()
260 self.log_user()
261 _set_downloads(HG_REPO, set_to=True)
261 _set_downloads(HG_REPO, set_to=True)
262 for rev in ['00x000000', 'tar', 'wrong', '@##$@$42413232', '232dffcd']:
262 for rev in ['00x000000', 'tar', 'wrong', '@##$@$42413232', '232dffcd']:
263 fname = '%s.zip' % rev
263 fname = '%s.zip' % rev
264
264
265 response = self.app.get(url(controller='files',
265 response = self.app.get(url(controller='files',
266 action='archivefile',
266 action='archivefile',
267 repo_name=HG_REPO,
267 repo_name=HG_REPO,
268 fname=fname))
268 fname=fname))
269 response.mustcontain('Unknown revision')
269 response.mustcontain('Unknown revision')
270
270
271 #==========================================================================
271 #==========================================================================
272 # RAW FILE
272 # RAW FILE
273 #==========================================================================
273 #==========================================================================
274 def test_raw_file_ok(self):
274 def test_raw_file_ok(self):
275 self.log_user()
275 self.log_user()
276 response = self.app.get(url(controller='files', action='rawfile',
276 response = self.app.get(url(controller='files', action='rawfile',
277 repo_name=HG_REPO,
277 repo_name=HG_REPO,
278 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
278 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
279 f_path='vcs/nodes.py'))
279 f_path='vcs/nodes.py'))
280
280
281 self.assertEqual(response.content_disposition, "attachment; filename=nodes.py")
281 self.assertEqual(response.content_disposition, "attachment; filename=nodes.py")
282 self.assertEqual(response.content_type, "text/x-python")
282 self.assertEqual(response.content_type, "text/x-python")
283
283
284 def test_raw_file_wrong_cs(self):
284 def test_raw_file_wrong_cs(self):
285 self.log_user()
285 self.log_user()
286 rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
286 rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
287 f_path = 'vcs/nodes.py'
287 f_path = 'vcs/nodes.py'
288
288
289 response = self.app.get(url(controller='files', action='rawfile',
289 response = self.app.get(url(controller='files', action='rawfile',
290 repo_name=HG_REPO,
290 repo_name=HG_REPO,
291 revision=rev,
291 revision=rev,
292 f_path=f_path))
292 f_path=f_path))
293
293
294 msg = """Revision %r does not exist for this repository""" % (rev)
294 msg = """Revision %r does not exist for this repository""" % (rev)
295 self.checkSessionFlash(response, msg)
295 self.checkSessionFlash(response, msg)
296
296
297 msg = """%s""" % (HG_REPO)
297 self.assertEqual('http://localhost/%s/files/tip/' % HG_REPO, response.headers['location'])
298 self.checkSessionFlash(response, msg)
299
298
300 def test_raw_file_wrong_f_path(self):
299 def test_raw_file_wrong_f_path(self):
301 self.log_user()
300 self.log_user()
302 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
301 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
303 f_path = 'vcs/ERRORnodes.py'
302 f_path = 'vcs/ERRORnodes.py'
304 response = self.app.get(url(controller='files', action='rawfile',
303 response = self.app.get(url(controller='files', action='rawfile',
305 repo_name=HG_REPO,
304 repo_name=HG_REPO,
306 revision=rev,
305 revision=rev,
307 f_path=f_path))
306 f_path=f_path))
308
307
309 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
308 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
310 self.checkSessionFlash(response, msg)
309 self.checkSessionFlash(response, msg)
311
310
312 #==========================================================================
311 #==========================================================================
313 # RAW RESPONSE - PLAIN
312 # RAW RESPONSE - PLAIN
314 #==========================================================================
313 #==========================================================================
315 def test_raw_ok(self):
314 def test_raw_ok(self):
316 self.log_user()
315 self.log_user()
317 response = self.app.get(url(controller='files', action='raw',
316 response = self.app.get(url(controller='files', action='raw',
318 repo_name=HG_REPO,
317 repo_name=HG_REPO,
319 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
318 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
320 f_path='vcs/nodes.py'))
319 f_path='vcs/nodes.py'))
321
320
322 self.assertEqual(response.content_type, "text/plain")
321 self.assertEqual(response.content_type, "text/plain")
323
322
324 def test_raw_wrong_cs(self):
323 def test_raw_wrong_cs(self):
325 self.log_user()
324 self.log_user()
326 rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
325 rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
327 f_path = 'vcs/nodes.py'
326 f_path = 'vcs/nodes.py'
328
327
329 response = self.app.get(url(controller='files', action='raw',
328 response = self.app.get(url(controller='files', action='raw',
330 repo_name=HG_REPO,
329 repo_name=HG_REPO,
331 revision=rev,
330 revision=rev,
332 f_path=f_path))
331 f_path=f_path))
333 msg = """Revision %r does not exist for this repository""" % (rev)
332 msg = """Revision %r does not exist for this repository""" % (rev)
334 self.checkSessionFlash(response, msg)
333 self.checkSessionFlash(response, msg)
335
334
336 msg = """%s""" % (HG_REPO)
335 self.assertEqual('http://localhost/%s/files/tip/' % HG_REPO, response.headers['location'])
337 self.checkSessionFlash(response, msg)
338
336
339 def test_raw_wrong_f_path(self):
337 def test_raw_wrong_f_path(self):
340 self.log_user()
338 self.log_user()
341 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
339 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
342 f_path = 'vcs/ERRORnodes.py'
340 f_path = 'vcs/ERRORnodes.py'
343 response = self.app.get(url(controller='files', action='raw',
341 response = self.app.get(url(controller='files', action='raw',
344 repo_name=HG_REPO,
342 repo_name=HG_REPO,
345 revision=rev,
343 revision=rev,
346 f_path=f_path))
344 f_path=f_path))
347 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
345 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
348 self.checkSessionFlash(response, msg)
346 self.checkSessionFlash(response, msg)
349
347
350 def test_ajaxed_files_list(self):
348 def test_ajaxed_files_list(self):
351 self.log_user()
349 self.log_user()
352 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
350 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
353 response = self.app.get(
351 response = self.app.get(
354 url('files_nodelist_home', repo_name=HG_REPO,f_path='/',revision=rev),
352 url('files_nodelist_home', repo_name=HG_REPO,f_path='/',revision=rev),
355 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
353 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
356 )
354 )
357 response.mustcontain("vcs/web/simplevcs/views/repository.py")
355 response.mustcontain("vcs/web/simplevcs/views/repository.py")
General Comments 0
You need to be logged in to leave comments. Login now