##// END OF EJS Templates
Added diff option into git and hg changeset objects, representing git formated patch against parent1
marcink -
r2384:5563af83 beta
parent child Browse files
Show More
@@ -1,454 +1,459 b''
1 import re
1 import re
2 from itertools import chain
2 from itertools import chain
3 from dulwich import objects
3 from dulwich import objects
4 from subprocess import Popen, PIPE
4 from subprocess import Popen, PIPE
5 from rhodecode.lib.vcs.conf import settings
5 from rhodecode.lib.vcs.conf import settings
6 from rhodecode.lib.vcs.exceptions import RepositoryError
6 from rhodecode.lib.vcs.exceptions import RepositoryError
7 from rhodecode.lib.vcs.exceptions import ChangesetError
7 from rhodecode.lib.vcs.exceptions import ChangesetError
8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
9 from rhodecode.lib.vcs.exceptions import VCSError
9 from rhodecode.lib.vcs.exceptions import VCSError
10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
12 from rhodecode.lib.vcs.backends.base import BaseChangeset
12 from rhodecode.lib.vcs.backends.base import BaseChangeset
13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
14 RemovedFileNode, SubModuleNode
14 RemovedFileNode, SubModuleNode
15 from rhodecode.lib.vcs.utils import safe_unicode
15 from rhodecode.lib.vcs.utils import safe_unicode
16 from rhodecode.lib.vcs.utils import date_fromtimestamp
16 from rhodecode.lib.vcs.utils import date_fromtimestamp
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18
18
19
19
20 class GitChangeset(BaseChangeset):
20 class GitChangeset(BaseChangeset):
21 """
21 """
22 Represents state of the repository at single revision.
22 Represents state of the repository at single revision.
23 """
23 """
24
24
25 def __init__(self, repository, revision):
25 def __init__(self, repository, revision):
26 self._stat_modes = {}
26 self._stat_modes = {}
27 self.repository = repository
27 self.repository = repository
28 self.raw_id = revision
28 self.raw_id = revision
29 self.revision = repository.revisions.index(revision)
29 self.revision = repository.revisions.index(revision)
30
30
31 self.short_id = self.raw_id[:12]
31 self.short_id = self.raw_id[:12]
32 self.id = self.raw_id
32 self.id = self.raw_id
33 try:
33 try:
34 commit = self.repository._repo.get_object(self.raw_id)
34 commit = self.repository._repo.get_object(self.raw_id)
35 except KeyError:
35 except KeyError:
36 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
36 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
37 self._commit = commit
37 self._commit = commit
38 self._tree_id = commit.tree
38 self._tree_id = commit.tree
39
39
40 self.message = safe_unicode(commit.message)
40 self.message = safe_unicode(commit.message)
41 #self.branch = None
41 #self.branch = None
42 self.tags = []
42 self.tags = []
43 self.nodes = {}
43 self.nodes = {}
44 self._paths = {}
44 self._paths = {}
45
45
46 @LazyProperty
46 @LazyProperty
47 def author(self):
47 def author(self):
48 return safe_unicode(self._commit.committer)
48 return safe_unicode(self._commit.committer)
49
49
50 @LazyProperty
50 @LazyProperty
51 def date(self):
51 def date(self):
52 return date_fromtimestamp(self._commit.commit_time,
52 return date_fromtimestamp(self._commit.commit_time,
53 self._commit.commit_timezone)
53 self._commit.commit_timezone)
54
54
55 @LazyProperty
55 @LazyProperty
56 def status(self):
56 def status(self):
57 """
57 """
58 Returns modified, added, removed, deleted files for current changeset
58 Returns modified, added, removed, deleted files for current changeset
59 """
59 """
60 return self.changed, self.added, self.removed
60 return self.changed, self.added, self.removed
61
61
62 @LazyProperty
62 @LazyProperty
63 def branch(self):
63 def branch(self):
64
64
65 heads = self.repository._heads(reverse=False)
65 heads = self.repository._heads(reverse=False)
66
66
67 ref = heads.get(self.raw_id)
67 ref = heads.get(self.raw_id)
68 if ref:
68 if ref:
69 return safe_unicode(ref)
69 return safe_unicode(ref)
70
70
71 def _fix_path(self, path):
71 def _fix_path(self, path):
72 """
72 """
73 Paths are stored without trailing slash so we need to get rid off it if
73 Paths are stored without trailing slash so we need to get rid off it if
74 needed.
74 needed.
75 """
75 """
76 if path.endswith('/'):
76 if path.endswith('/'):
77 path = path.rstrip('/')
77 path = path.rstrip('/')
78 return path
78 return path
79
79
80 def _get_id_for_path(self, path):
80 def _get_id_for_path(self, path):
81
81
82 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
82 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
83 if not path in self._paths:
83 if not path in self._paths:
84 path = path.strip('/')
84 path = path.strip('/')
85 # set root tree
85 # set root tree
86 tree = self.repository._repo[self._commit.tree]
86 tree = self.repository._repo[self._commit.tree]
87 if path == '':
87 if path == '':
88 self._paths[''] = tree.id
88 self._paths[''] = tree.id
89 return tree.id
89 return tree.id
90 splitted = path.split('/')
90 splitted = path.split('/')
91 dirs, name = splitted[:-1], splitted[-1]
91 dirs, name = splitted[:-1], splitted[-1]
92 curdir = ''
92 curdir = ''
93
93
94 # initially extract things from root dir
94 # initially extract things from root dir
95 for item, stat, id in tree.iteritems():
95 for item, stat, id in tree.iteritems():
96 if curdir:
96 if curdir:
97 name = '/'.join((curdir, item))
97 name = '/'.join((curdir, item))
98 else:
98 else:
99 name = item
99 name = item
100 self._paths[name] = id
100 self._paths[name] = id
101 self._stat_modes[name] = stat
101 self._stat_modes[name] = stat
102
102
103 for dir in dirs:
103 for dir in dirs:
104 if curdir:
104 if curdir:
105 curdir = '/'.join((curdir, dir))
105 curdir = '/'.join((curdir, dir))
106 else:
106 else:
107 curdir = dir
107 curdir = dir
108 dir_id = None
108 dir_id = None
109 for item, stat, id in tree.iteritems():
109 for item, stat, id in tree.iteritems():
110 if dir == item:
110 if dir == item:
111 dir_id = id
111 dir_id = id
112 if dir_id:
112 if dir_id:
113 # Update tree
113 # Update tree
114 tree = self.repository._repo[dir_id]
114 tree = self.repository._repo[dir_id]
115 if not isinstance(tree, objects.Tree):
115 if not isinstance(tree, objects.Tree):
116 raise ChangesetError('%s is not a directory' % curdir)
116 raise ChangesetError('%s is not a directory' % curdir)
117 else:
117 else:
118 raise ChangesetError('%s have not been found' % curdir)
118 raise ChangesetError('%s have not been found' % curdir)
119
119
120 # cache all items from the given traversed tree
120 # cache all items from the given traversed tree
121 for item, stat, id in tree.iteritems():
121 for item, stat, id in tree.iteritems():
122 if curdir:
122 if curdir:
123 name = '/'.join((curdir, item))
123 name = '/'.join((curdir, item))
124 else:
124 else:
125 name = item
125 name = item
126 self._paths[name] = id
126 self._paths[name] = id
127 self._stat_modes[name] = stat
127 self._stat_modes[name] = stat
128 if not path in self._paths:
128 if not path in self._paths:
129 raise NodeDoesNotExistError("There is no file nor directory "
129 raise NodeDoesNotExistError("There is no file nor directory "
130 "at the given path %r at revision %r"
130 "at the given path %r at revision %r"
131 % (path, self.short_id))
131 % (path, self.short_id))
132 return self._paths[path]
132 return self._paths[path]
133
133
134 def _get_kind(self, path):
134 def _get_kind(self, path):
135 id = self._get_id_for_path(path)
135 id = self._get_id_for_path(path)
136 obj = self.repository._repo[id]
136 obj = self.repository._repo[id]
137 if isinstance(obj, objects.Blob):
137 if isinstance(obj, objects.Blob):
138 return NodeKind.FILE
138 return NodeKind.FILE
139 elif isinstance(obj, objects.Tree):
139 elif isinstance(obj, objects.Tree):
140 return NodeKind.DIR
140 return NodeKind.DIR
141
141
142 def _get_file_nodes(self):
142 def _get_file_nodes(self):
143 return chain(*(t[2] for t in self.walk()))
143 return chain(*(t[2] for t in self.walk()))
144
144
145 @LazyProperty
145 @LazyProperty
146 def parents(self):
146 def parents(self):
147 """
147 """
148 Returns list of parents changesets.
148 Returns list of parents changesets.
149 """
149 """
150 return [self.repository.get_changeset(parent)
150 return [self.repository.get_changeset(parent)
151 for parent in self._commit.parents]
151 for parent in self._commit.parents]
152
152
153 def next(self, branch=None):
153 def next(self, branch=None):
154
154
155 if branch and self.branch != branch:
155 if branch and self.branch != branch:
156 raise VCSError('Branch option used on changeset not belonging '
156 raise VCSError('Branch option used on changeset not belonging '
157 'to that branch')
157 'to that branch')
158
158
159 def _next(changeset, branch):
159 def _next(changeset, branch):
160 try:
160 try:
161 next_ = changeset.revision + 1
161 next_ = changeset.revision + 1
162 next_rev = changeset.repository.revisions[next_]
162 next_rev = changeset.repository.revisions[next_]
163 except IndexError:
163 except IndexError:
164 raise ChangesetDoesNotExistError
164 raise ChangesetDoesNotExistError
165 cs = changeset.repository.get_changeset(next_rev)
165 cs = changeset.repository.get_changeset(next_rev)
166
166
167 if branch and branch != cs.branch:
167 if branch and branch != cs.branch:
168 return _next(cs, branch)
168 return _next(cs, branch)
169
169
170 return cs
170 return cs
171
171
172 return _next(self, branch)
172 return _next(self, branch)
173
173
174 def prev(self, branch=None):
174 def prev(self, branch=None):
175 if branch and self.branch != branch:
175 if branch and self.branch != branch:
176 raise VCSError('Branch option used on changeset not belonging '
176 raise VCSError('Branch option used on changeset not belonging '
177 'to that branch')
177 'to that branch')
178
178
179 def _prev(changeset, branch):
179 def _prev(changeset, branch):
180 try:
180 try:
181 prev_ = changeset.revision - 1
181 prev_ = changeset.revision - 1
182 if prev_ < 0:
182 if prev_ < 0:
183 raise IndexError
183 raise IndexError
184 prev_rev = changeset.repository.revisions[prev_]
184 prev_rev = changeset.repository.revisions[prev_]
185 except IndexError:
185 except IndexError:
186 raise ChangesetDoesNotExistError
186 raise ChangesetDoesNotExistError
187
187
188 cs = changeset.repository.get_changeset(prev_rev)
188 cs = changeset.repository.get_changeset(prev_rev)
189
189
190 if branch and branch != cs.branch:
190 if branch and branch != cs.branch:
191 return _prev(cs, branch)
191 return _prev(cs, branch)
192
192
193 return cs
193 return cs
194
194
195 return _prev(self, branch)
195 return _prev(self, branch)
196
196
197 def diff(self, ignore_whitespace=True, context=3):
198 return ''.join(self.repository.get_diff(self, self.parents[0],
199 ignore_whitespace=ignore_whitespace,
200 context=context))
201
197 def get_file_mode(self, path):
202 def get_file_mode(self, path):
198 """
203 """
199 Returns stat mode of the file at the given ``path``.
204 Returns stat mode of the file at the given ``path``.
200 """
205 """
201 # ensure path is traversed
206 # ensure path is traversed
202 self._get_id_for_path(path)
207 self._get_id_for_path(path)
203 return self._stat_modes[path]
208 return self._stat_modes[path]
204
209
205 def get_file_content(self, path):
210 def get_file_content(self, path):
206 """
211 """
207 Returns content of the file at given ``path``.
212 Returns content of the file at given ``path``.
208 """
213 """
209 id = self._get_id_for_path(path)
214 id = self._get_id_for_path(path)
210 blob = self.repository._repo[id]
215 blob = self.repository._repo[id]
211 return blob.as_pretty_string()
216 return blob.as_pretty_string()
212
217
213 def get_file_size(self, path):
218 def get_file_size(self, path):
214 """
219 """
215 Returns size of the file at given ``path``.
220 Returns size of the file at given ``path``.
216 """
221 """
217 id = self._get_id_for_path(path)
222 id = self._get_id_for_path(path)
218 blob = self.repository._repo[id]
223 blob = self.repository._repo[id]
219 return blob.raw_length()
224 return blob.raw_length()
220
225
221 def get_file_changeset(self, path):
226 def get_file_changeset(self, path):
222 """
227 """
223 Returns last commit of the file at the given ``path``.
228 Returns last commit of the file at the given ``path``.
224 """
229 """
225 node = self.get_node(path)
230 node = self.get_node(path)
226 return node.history[0]
231 return node.history[0]
227
232
228 def get_file_history(self, path):
233 def get_file_history(self, path):
229 """
234 """
230 Returns history of file as reversed list of ``Changeset`` objects for
235 Returns history of file as reversed list of ``Changeset`` objects for
231 which file at given ``path`` has been modified.
236 which file at given ``path`` has been modified.
232
237
233 TODO: This function now uses os underlying 'git' and 'grep' commands
238 TODO: This function now uses os underlying 'git' and 'grep' commands
234 which is generally not good. Should be replaced with algorithm
239 which is generally not good. Should be replaced with algorithm
235 iterating commits.
240 iterating commits.
236 """
241 """
237 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
242 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
238 self.id, path
243 self.id, path
239 )
244 )
240 so, se = self.repository.run_git_command(cmd)
245 so, se = self.repository.run_git_command(cmd)
241 ids = re.findall(r'[0-9a-fA-F]{40}', so)
246 ids = re.findall(r'[0-9a-fA-F]{40}', so)
242 return [self.repository.get_changeset(id) for id in ids]
247 return [self.repository.get_changeset(id) for id in ids]
243
248
244 def get_file_annotate(self, path):
249 def get_file_annotate(self, path):
245 """
250 """
246 Returns a list of three element tuples with lineno,changeset and line
251 Returns a list of three element tuples with lineno,changeset and line
247
252
248 TODO: This function now uses os underlying 'git' command which is
253 TODO: This function now uses os underlying 'git' command which is
249 generally not good. Should be replaced with algorithm iterating
254 generally not good. Should be replaced with algorithm iterating
250 commits.
255 commits.
251 """
256 """
252 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
257 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
253 # -l ==> outputs long shas (and we need all 40 characters)
258 # -l ==> outputs long shas (and we need all 40 characters)
254 # --root ==> doesn't put '^' character for bounderies
259 # --root ==> doesn't put '^' character for bounderies
255 # -r sha ==> blames for the given revision
260 # -r sha ==> blames for the given revision
256 so, se = self.repository.run_git_command(cmd)
261 so, se = self.repository.run_git_command(cmd)
257 annotate = []
262 annotate = []
258 for i, blame_line in enumerate(so.split('\n')[:-1]):
263 for i, blame_line in enumerate(so.split('\n')[:-1]):
259 ln_no = i + 1
264 ln_no = i + 1
260 id, line = re.split(r' \(.+?\) ', blame_line, 1)
265 id, line = re.split(r' \(.+?\) ', blame_line, 1)
261 annotate.append((ln_no, self.repository.get_changeset(id), line))
266 annotate.append((ln_no, self.repository.get_changeset(id), line))
262 return annotate
267 return annotate
263
268
264 def fill_archive(self, stream=None, kind='tgz', prefix=None,
269 def fill_archive(self, stream=None, kind='tgz', prefix=None,
265 subrepos=False):
270 subrepos=False):
266 """
271 """
267 Fills up given stream.
272 Fills up given stream.
268
273
269 :param stream: file like object.
274 :param stream: file like object.
270 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
275 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
271 Default: ``tgz``.
276 Default: ``tgz``.
272 :param prefix: name of root directory in archive.
277 :param prefix: name of root directory in archive.
273 Default is repository name and changeset's raw_id joined with dash
278 Default is repository name and changeset's raw_id joined with dash
274 (``repo-tip.<KIND>``).
279 (``repo-tip.<KIND>``).
275 :param subrepos: include subrepos in this archive.
280 :param subrepos: include subrepos in this archive.
276
281
277 :raise ImproperArchiveTypeError: If given kind is wrong.
282 :raise ImproperArchiveTypeError: If given kind is wrong.
278 :raise VcsError: If given stream is None
283 :raise VcsError: If given stream is None
279
284
280 """
285 """
281 allowed_kinds = settings.ARCHIVE_SPECS.keys()
286 allowed_kinds = settings.ARCHIVE_SPECS.keys()
282 if kind not in allowed_kinds:
287 if kind not in allowed_kinds:
283 raise ImproperArchiveTypeError('Archive kind not supported use one'
288 raise ImproperArchiveTypeError('Archive kind not supported use one'
284 'of %s', allowed_kinds)
289 'of %s', allowed_kinds)
285
290
286 if prefix is None:
291 if prefix is None:
287 prefix = '%s-%s' % (self.repository.name, self.short_id)
292 prefix = '%s-%s' % (self.repository.name, self.short_id)
288 elif prefix.startswith('/'):
293 elif prefix.startswith('/'):
289 raise VCSError("Prefix cannot start with leading slash")
294 raise VCSError("Prefix cannot start with leading slash")
290 elif prefix.strip() == '':
295 elif prefix.strip() == '':
291 raise VCSError("Prefix cannot be empty")
296 raise VCSError("Prefix cannot be empty")
292
297
293 if kind == 'zip':
298 if kind == 'zip':
294 frmt = 'zip'
299 frmt = 'zip'
295 else:
300 else:
296 frmt = 'tar'
301 frmt = 'tar'
297 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
302 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
298 self.raw_id)
303 self.raw_id)
299 if kind == 'tgz':
304 if kind == 'tgz':
300 cmd += ' | gzip -9'
305 cmd += ' | gzip -9'
301 elif kind == 'tbz2':
306 elif kind == 'tbz2':
302 cmd += ' | bzip2 -9'
307 cmd += ' | bzip2 -9'
303
308
304 if stream is None:
309 if stream is None:
305 raise VCSError('You need to pass in a valid stream for filling'
310 raise VCSError('You need to pass in a valid stream for filling'
306 ' with archival data')
311 ' with archival data')
307 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
312 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
308 cwd=self.repository.path)
313 cwd=self.repository.path)
309
314
310 buffer_size = 1024 * 8
315 buffer_size = 1024 * 8
311 chunk = popen.stdout.read(buffer_size)
316 chunk = popen.stdout.read(buffer_size)
312 while chunk:
317 while chunk:
313 stream.write(chunk)
318 stream.write(chunk)
314 chunk = popen.stdout.read(buffer_size)
319 chunk = popen.stdout.read(buffer_size)
315 # Make sure all descriptors would be read
320 # Make sure all descriptors would be read
316 popen.communicate()
321 popen.communicate()
317
322
318 def get_nodes(self, path):
323 def get_nodes(self, path):
319 if self._get_kind(path) != NodeKind.DIR:
324 if self._get_kind(path) != NodeKind.DIR:
320 raise ChangesetError("Directory does not exist for revision %r at "
325 raise ChangesetError("Directory does not exist for revision %r at "
321 " %r" % (self.revision, path))
326 " %r" % (self.revision, path))
322 path = self._fix_path(path)
327 path = self._fix_path(path)
323 id = self._get_id_for_path(path)
328 id = self._get_id_for_path(path)
324 tree = self.repository._repo[id]
329 tree = self.repository._repo[id]
325 dirnodes = []
330 dirnodes = []
326 filenodes = []
331 filenodes = []
327 als = self.repository.alias
332 als = self.repository.alias
328 for name, stat, id in tree.iteritems():
333 for name, stat, id in tree.iteritems():
329 if objects.S_ISGITLINK(stat):
334 if objects.S_ISGITLINK(stat):
330 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
335 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
331 alias=als))
336 alias=als))
332 continue
337 continue
333
338
334 obj = self.repository._repo.get_object(id)
339 obj = self.repository._repo.get_object(id)
335 if path != '':
340 if path != '':
336 obj_path = '/'.join((path, name))
341 obj_path = '/'.join((path, name))
337 else:
342 else:
338 obj_path = name
343 obj_path = name
339 if obj_path not in self._stat_modes:
344 if obj_path not in self._stat_modes:
340 self._stat_modes[obj_path] = stat
345 self._stat_modes[obj_path] = stat
341 if isinstance(obj, objects.Tree):
346 if isinstance(obj, objects.Tree):
342 dirnodes.append(DirNode(obj_path, changeset=self))
347 dirnodes.append(DirNode(obj_path, changeset=self))
343 elif isinstance(obj, objects.Blob):
348 elif isinstance(obj, objects.Blob):
344 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
349 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
345 else:
350 else:
346 raise ChangesetError("Requested object should be Tree "
351 raise ChangesetError("Requested object should be Tree "
347 "or Blob, is %r" % type(obj))
352 "or Blob, is %r" % type(obj))
348 nodes = dirnodes + filenodes
353 nodes = dirnodes + filenodes
349 for node in nodes:
354 for node in nodes:
350 if not node.path in self.nodes:
355 if not node.path in self.nodes:
351 self.nodes[node.path] = node
356 self.nodes[node.path] = node
352 nodes.sort()
357 nodes.sort()
353 return nodes
358 return nodes
354
359
355 def get_node(self, path):
360 def get_node(self, path):
356 if isinstance(path, unicode):
361 if isinstance(path, unicode):
357 path = path.encode('utf-8')
362 path = path.encode('utf-8')
358 path = self._fix_path(path)
363 path = self._fix_path(path)
359 if not path in self.nodes:
364 if not path in self.nodes:
360 try:
365 try:
361 id_ = self._get_id_for_path(path)
366 id_ = self._get_id_for_path(path)
362 except ChangesetError:
367 except ChangesetError:
363 raise NodeDoesNotExistError("Cannot find one of parents' "
368 raise NodeDoesNotExistError("Cannot find one of parents' "
364 "directories for a given path: %s" % path)
369 "directories for a given path: %s" % path)
365
370
366 als = self.repository.alias
371 als = self.repository.alias
367 _GL = lambda m: m and objects.S_ISGITLINK(m)
372 _GL = lambda m: m and objects.S_ISGITLINK(m)
368 if _GL(self._stat_modes.get(path)):
373 if _GL(self._stat_modes.get(path)):
369 node = SubModuleNode(path, url=None, changeset=id_, alias=als)
374 node = SubModuleNode(path, url=None, changeset=id_, alias=als)
370 else:
375 else:
371 obj = self.repository._repo.get_object(id_)
376 obj = self.repository._repo.get_object(id_)
372
377
373 if isinstance(obj, objects.Tree):
378 if isinstance(obj, objects.Tree):
374 if path == '':
379 if path == '':
375 node = RootNode(changeset=self)
380 node = RootNode(changeset=self)
376 else:
381 else:
377 node = DirNode(path, changeset=self)
382 node = DirNode(path, changeset=self)
378 node._tree = obj
383 node._tree = obj
379 elif isinstance(obj, objects.Blob):
384 elif isinstance(obj, objects.Blob):
380 node = FileNode(path, changeset=self)
385 node = FileNode(path, changeset=self)
381 node._blob = obj
386 node._blob = obj
382 else:
387 else:
383 raise NodeDoesNotExistError("There is no file nor directory "
388 raise NodeDoesNotExistError("There is no file nor directory "
384 "at the given path %r at revision %r"
389 "at the given path %r at revision %r"
385 % (path, self.short_id))
390 % (path, self.short_id))
386 # cache node
391 # cache node
387 self.nodes[path] = node
392 self.nodes[path] = node
388 return self.nodes[path]
393 return self.nodes[path]
389
394
390 @LazyProperty
395 @LazyProperty
391 def affected_files(self):
396 def affected_files(self):
392 """
397 """
393 Get's a fast accessible file changes for given changeset
398 Get's a fast accessible file changes for given changeset
394 """
399 """
395
400
396 return self.added + self.changed
401 return self.added + self.changed
397
402
398 @LazyProperty
403 @LazyProperty
399 def _diff_name_status(self):
404 def _diff_name_status(self):
400 output = []
405 output = []
401 for parent in self.parents:
406 for parent in self.parents:
402 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
407 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
403 so, se = self.repository.run_git_command(cmd)
408 so, se = self.repository.run_git_command(cmd)
404 output.append(so.strip())
409 output.append(so.strip())
405 return '\n'.join(output)
410 return '\n'.join(output)
406
411
407 def _get_paths_for_status(self, status):
412 def _get_paths_for_status(self, status):
408 """
413 """
409 Returns sorted list of paths for given ``status``.
414 Returns sorted list of paths for given ``status``.
410
415
411 :param status: one of: *added*, *modified* or *deleted*
416 :param status: one of: *added*, *modified* or *deleted*
412 """
417 """
413 paths = set()
418 paths = set()
414 char = status[0].upper()
419 char = status[0].upper()
415 for line in self._diff_name_status.splitlines():
420 for line in self._diff_name_status.splitlines():
416 if not line:
421 if not line:
417 continue
422 continue
418
423
419 if line.startswith(char):
424 if line.startswith(char):
420 splitted = line.split(char, 1)
425 splitted = line.split(char, 1)
421 if not len(splitted) == 2:
426 if not len(splitted) == 2:
422 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
427 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
423 "particularly that line: %s" % (self._diff_name_status,
428 "particularly that line: %s" % (self._diff_name_status,
424 line))
429 line))
425 _path = splitted[1].strip()
430 _path = splitted[1].strip()
426 paths.add(_path)
431 paths.add(_path)
427 return sorted(paths)
432 return sorted(paths)
428
433
429 @LazyProperty
434 @LazyProperty
430 def added(self):
435 def added(self):
431 """
436 """
432 Returns list of added ``FileNode`` objects.
437 Returns list of added ``FileNode`` objects.
433 """
438 """
434 if not self.parents:
439 if not self.parents:
435 return list(self._get_file_nodes())
440 return list(self._get_file_nodes())
436 return [self.get_node(path) for path in self._get_paths_for_status('added')]
441 return [self.get_node(path) for path in self._get_paths_for_status('added')]
437
442
438 @LazyProperty
443 @LazyProperty
439 def changed(self):
444 def changed(self):
440 """
445 """
441 Returns list of modified ``FileNode`` objects.
446 Returns list of modified ``FileNode`` objects.
442 """
447 """
443 if not self.parents:
448 if not self.parents:
444 return []
449 return []
445 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
450 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
446
451
447 @LazyProperty
452 @LazyProperty
448 def removed(self):
453 def removed(self):
449 """
454 """
450 Returns list of removed ``FileNode`` objects.
455 Returns list of removed ``FileNode`` objects.
451 """
456 """
452 if not self.parents:
457 if not self.parents:
453 return []
458 return []
454 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
459 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -1,565 +1,571 b''
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 from dulwich.repo import Repo, NotGitRepository
16 from dulwich.repo import Repo, NotGitRepository
17 #from dulwich.config import ConfigFile
17 #from dulwich.config import ConfigFile
18 from string import Template
18 from string import Template
19 from subprocess import Popen, PIPE
19 from subprocess import Popen, PIPE
20 from rhodecode.lib.vcs.backends.base import BaseRepository
20 from rhodecode.lib.vcs.backends.base import BaseRepository
21 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
21 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
22 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
22 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
23 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
23 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
24 from rhodecode.lib.vcs.exceptions import RepositoryError
24 from rhodecode.lib.vcs.exceptions import RepositoryError
25 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
25 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
26 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
26 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
27 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
27 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
29 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
30 from rhodecode.lib.vcs.utils.paths import abspath
30 from rhodecode.lib.vcs.utils.paths import abspath
31 from rhodecode.lib.vcs.utils.paths import get_user_home
31 from rhodecode.lib.vcs.utils.paths import get_user_home
32 from .workdir import GitWorkdir
32 from .workdir import GitWorkdir
33 from .changeset import GitChangeset
33 from .changeset import GitChangeset
34 from .inmemory import GitInMemoryChangeset
34 from .inmemory import GitInMemoryChangeset
35 from .config import ConfigFile
35 from .config import ConfigFile
36
36
37
37
38 class GitRepository(BaseRepository):
38 class GitRepository(BaseRepository):
39 """
39 """
40 Git repository backend.
40 Git repository backend.
41 """
41 """
42 DEFAULT_BRANCH_NAME = 'master'
42 DEFAULT_BRANCH_NAME = 'master'
43 scm = 'git'
43 scm = 'git'
44
44
45 def __init__(self, repo_path, create=False, src_url=None,
45 def __init__(self, repo_path, create=False, src_url=None,
46 update_after_clone=False, bare=False):
46 update_after_clone=False, bare=False):
47
47
48 self.path = abspath(repo_path)
48 self.path = abspath(repo_path)
49 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
49 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
50 #temporary set that to now at later we will move it to constructor
50 #temporary set that to now at later we will move it to constructor
51 baseui = None
51 baseui = None
52 if baseui is None:
52 if baseui is None:
53 from mercurial.ui import ui
53 from mercurial.ui import ui
54 baseui = ui()
54 baseui = ui()
55 # patch the instance of GitRepo with an "FAKE" ui object to add
55 # patch the instance of GitRepo with an "FAKE" ui object to add
56 # compatibility layer with Mercurial
56 # compatibility layer with Mercurial
57 setattr(self._repo, 'ui', baseui)
57 setattr(self._repo, 'ui', baseui)
58
58
59 try:
59 try:
60 self.head = self._repo.head()
60 self.head = self._repo.head()
61 except KeyError:
61 except KeyError:
62 self.head = None
62 self.head = None
63
63
64 self._config_files = [
64 self._config_files = [
65 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
65 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
66 'config'),
66 'config'),
67 abspath(get_user_home(), '.gitconfig'),
67 abspath(get_user_home(), '.gitconfig'),
68 ]
68 ]
69 self.bare = self._repo.bare
69 self.bare = self._repo.bare
70
70
71 @LazyProperty
71 @LazyProperty
72 def revisions(self):
72 def revisions(self):
73 """
73 """
74 Returns list of revisions' ids, in ascending order. Being lazy
74 Returns list of revisions' ids, in ascending order. Being lazy
75 attribute allows external tools to inject shas from cache.
75 attribute allows external tools to inject shas from cache.
76 """
76 """
77 return self._get_all_revisions()
77 return self._get_all_revisions()
78
78
79 def run_git_command(self, cmd):
79 def run_git_command(self, cmd):
80 """
80 """
81 Runs given ``cmd`` as git command and returns tuple
81 Runs given ``cmd`` as git command and returns tuple
82 (returncode, stdout, stderr).
82 (returncode, stdout, stderr).
83
83
84 .. note::
84 .. note::
85 This method exists only until log/blame functionality is implemented
85 This method exists only until log/blame functionality is implemented
86 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
86 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
87 os command's output is road to hell...
87 os command's output is road to hell...
88
88
89 :param cmd: git command to be executed
89 :param cmd: git command to be executed
90 """
90 """
91
91
92 _copts = ['-c', 'core.quotepath=false', ]
92 _copts = ['-c', 'core.quotepath=false', ]
93 _str_cmd = False
93 _str_cmd = False
94 if isinstance(cmd, basestring):
94 if isinstance(cmd, basestring):
95 cmd = [cmd]
95 cmd = [cmd]
96 _str_cmd = True
96 _str_cmd = True
97
97
98 gitenv = os.environ
98 gitenv = os.environ
99 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
99 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
100
100
101 cmd = ['git'] + _copts + cmd
101 cmd = ['git'] + _copts + cmd
102 if _str_cmd:
102 if _str_cmd:
103 cmd = ' '.join(cmd)
103 cmd = ' '.join(cmd)
104 try:
104 try:
105 opts = dict(
105 opts = dict(
106 shell=isinstance(cmd, basestring),
106 shell=isinstance(cmd, basestring),
107 stdout=PIPE,
107 stdout=PIPE,
108 stderr=PIPE,
108 stderr=PIPE,
109 env=gitenv,
109 env=gitenv,
110 )
110 )
111 if os.path.isdir(self.path):
111 if os.path.isdir(self.path):
112 opts['cwd'] = self.path
112 opts['cwd'] = self.path
113 p = Popen(cmd, **opts)
113 p = Popen(cmd, **opts)
114 except OSError, err:
114 except OSError, err:
115 raise RepositoryError("Couldn't run git command (%s).\n"
115 raise RepositoryError("Couldn't run git command (%s).\n"
116 "Original error was:%s" % (cmd, err))
116 "Original error was:%s" % (cmd, err))
117 so, se = p.communicate()
117 so, se = p.communicate()
118 if not se.startswith("fatal: bad default revision 'HEAD'") and \
118 if not se.startswith("fatal: bad default revision 'HEAD'") and \
119 p.returncode != 0:
119 p.returncode != 0:
120 raise RepositoryError("Couldn't run git command (%s).\n"
120 raise RepositoryError("Couldn't run git command (%s).\n"
121 "stderr:\n%s" % (cmd, se))
121 "stderr:\n%s" % (cmd, se))
122 return so, se
122 return so, se
123
123
124 def _check_url(self, url):
124 def _check_url(self, url):
125 """
125 """
126 Functon will check given url and try to verify if it's a valid
126 Functon will check given url and try to verify if it's a valid
127 link. Sometimes it may happened that mercurial will issue basic
127 link. Sometimes it may happened that mercurial will issue basic
128 auth request that can cause whole API to hang when used from python
128 auth request that can cause whole API to hang when used from python
129 or other external calls.
129 or other external calls.
130
130
131 On failures it'll raise urllib2.HTTPError
131 On failures it'll raise urllib2.HTTPError
132 """
132 """
133
133
134 #TODO: implement this
134 #TODO: implement this
135 pass
135 pass
136
136
137 def _get_repo(self, create, src_url=None, update_after_clone=False,
137 def _get_repo(self, create, src_url=None, update_after_clone=False,
138 bare=False):
138 bare=False):
139 if create and os.path.exists(self.path):
139 if create and os.path.exists(self.path):
140 raise RepositoryError("Location already exist")
140 raise RepositoryError("Location already exist")
141 if src_url and not create:
141 if src_url and not create:
142 raise RepositoryError("Create should be set to True if src_url is "
142 raise RepositoryError("Create should be set to True if src_url is "
143 "given (clone operation creates repository)")
143 "given (clone operation creates repository)")
144 try:
144 try:
145 if create and src_url:
145 if create and src_url:
146 self._check_url(src_url)
146 self._check_url(src_url)
147 self.clone(src_url, update_after_clone, bare)
147 self.clone(src_url, update_after_clone, bare)
148 return Repo(self.path)
148 return Repo(self.path)
149 elif create:
149 elif create:
150 os.mkdir(self.path)
150 os.mkdir(self.path)
151 if bare:
151 if bare:
152 return Repo.init_bare(self.path)
152 return Repo.init_bare(self.path)
153 else:
153 else:
154 return Repo.init(self.path)
154 return Repo.init(self.path)
155 else:
155 else:
156 return Repo(self.path)
156 return Repo(self.path)
157 except (NotGitRepository, OSError), err:
157 except (NotGitRepository, OSError), err:
158 raise RepositoryError(err)
158 raise RepositoryError(err)
159
159
160 def _get_all_revisions(self):
160 def _get_all_revisions(self):
161 cmd = 'rev-list --all --date-order'
161 cmd = 'rev-list --all --date-order'
162 try:
162 try:
163 so, se = self.run_git_command(cmd)
163 so, se = self.run_git_command(cmd)
164 except RepositoryError:
164 except RepositoryError:
165 # Can be raised for empty repositories
165 # Can be raised for empty repositories
166 return []
166 return []
167 revisions = so.splitlines()
167 revisions = so.splitlines()
168 revisions.reverse()
168 revisions.reverse()
169 return revisions
169 return revisions
170
170
171 def _get_revision(self, revision):
171 def _get_revision(self, revision):
172 """
172 """
173 For git backend we always return integer here. This way we ensure
173 For git backend we always return integer here. This way we ensure
174 that changset's revision attribute would become integer.
174 that changset's revision attribute would become integer.
175 """
175 """
176 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
176 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
177 is_bstr = lambda o: isinstance(o, (str, unicode))
177 is_bstr = lambda o: isinstance(o, (str, unicode))
178 is_null = lambda o: len(o) == revision.count('0')
178 is_null = lambda o: len(o) == revision.count('0')
179
179
180 if len(self.revisions) == 0:
180 if len(self.revisions) == 0:
181 raise EmptyRepositoryError("There are no changesets yet")
181 raise EmptyRepositoryError("There are no changesets yet")
182
182
183 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
183 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
184 revision = self.revisions[-1]
184 revision = self.revisions[-1]
185
185
186 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
186 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
187 or isinstance(revision, int) or is_null(revision)):
187 or isinstance(revision, int) or is_null(revision)):
188 try:
188 try:
189 revision = self.revisions[int(revision)]
189 revision = self.revisions[int(revision)]
190 except:
190 except:
191 raise ChangesetDoesNotExistError("Revision %r does not exist "
191 raise ChangesetDoesNotExistError("Revision %r does not exist "
192 "for this repository %s" % (revision, self))
192 "for this repository %s" % (revision, self))
193
193
194 elif is_bstr(revision):
194 elif is_bstr(revision):
195 if not pattern.match(revision) or revision not in self.revisions:
195 if not pattern.match(revision) or revision not in self.revisions:
196 raise ChangesetDoesNotExistError("Revision %r does not exist "
196 raise ChangesetDoesNotExistError("Revision %r does not exist "
197 "for this repository %s" % (revision, self))
197 "for this repository %s" % (revision, self))
198
198
199 # Ensure we return full id
199 # Ensure we return full id
200 if not pattern.match(str(revision)):
200 if not pattern.match(str(revision)):
201 raise ChangesetDoesNotExistError("Given revision %r not recognized"
201 raise ChangesetDoesNotExistError("Given revision %r not recognized"
202 % revision)
202 % revision)
203 return revision
203 return revision
204
204
205 def _get_archives(self, archive_name='tip'):
205 def _get_archives(self, archive_name='tip'):
206
206
207 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
207 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
208 yield {"type": i[0], "extension": i[1], "node": archive_name}
208 yield {"type": i[0], "extension": i[1], "node": archive_name}
209
209
210 def _get_url(self, url):
210 def _get_url(self, url):
211 """
211 """
212 Returns normalized url. If schema is not given, would fall to
212 Returns normalized url. If schema is not given, would fall to
213 filesystem (``file:///``) schema.
213 filesystem (``file:///``) schema.
214 """
214 """
215 url = str(url)
215 url = str(url)
216 if url != 'default' and not '://' in url:
216 if url != 'default' and not '://' in url:
217 url = ':///'.join(('file', url))
217 url = ':///'.join(('file', url))
218 return url
218 return url
219
219
220 @LazyProperty
220 @LazyProperty
221 def name(self):
221 def name(self):
222 return os.path.basename(self.path)
222 return os.path.basename(self.path)
223
223
224 @LazyProperty
224 @LazyProperty
225 def last_change(self):
225 def last_change(self):
226 """
226 """
227 Returns last change made on this repository as datetime object
227 Returns last change made on this repository as datetime object
228 """
228 """
229 return date_fromtimestamp(self._get_mtime(), makedate()[1])
229 return date_fromtimestamp(self._get_mtime(), makedate()[1])
230
230
231 def _get_mtime(self):
231 def _get_mtime(self):
232 try:
232 try:
233 return time.mktime(self.get_changeset().date.timetuple())
233 return time.mktime(self.get_changeset().date.timetuple())
234 except RepositoryError:
234 except RepositoryError:
235 idx_loc = '' if self.bare else '.git'
235 idx_loc = '' if self.bare else '.git'
236 # fallback to filesystem
236 # fallback to filesystem
237 in_path = os.path.join(self.path, idx_loc, "index")
237 in_path = os.path.join(self.path, idx_loc, "index")
238 he_path = os.path.join(self.path, idx_loc, "HEAD")
238 he_path = os.path.join(self.path, idx_loc, "HEAD")
239 if os.path.exists(in_path):
239 if os.path.exists(in_path):
240 return os.stat(in_path).st_mtime
240 return os.stat(in_path).st_mtime
241 else:
241 else:
242 return os.stat(he_path).st_mtime
242 return os.stat(he_path).st_mtime
243
243
244 @LazyProperty
244 @LazyProperty
245 def description(self):
245 def description(self):
246 idx_loc = '' if self.bare else '.git'
246 idx_loc = '' if self.bare else '.git'
247 undefined_description = u'unknown'
247 undefined_description = u'unknown'
248 description_path = os.path.join(self.path, idx_loc, 'description')
248 description_path = os.path.join(self.path, idx_loc, 'description')
249 if os.path.isfile(description_path):
249 if os.path.isfile(description_path):
250 return safe_unicode(open(description_path).read())
250 return safe_unicode(open(description_path).read())
251 else:
251 else:
252 return undefined_description
252 return undefined_description
253
253
254 @LazyProperty
254 @LazyProperty
255 def contact(self):
255 def contact(self):
256 undefined_contact = u'Unknown'
256 undefined_contact = u'Unknown'
257 return undefined_contact
257 return undefined_contact
258
258
259 @property
259 @property
260 def branches(self):
260 def branches(self):
261 if not self.revisions:
261 if not self.revisions:
262 return {}
262 return {}
263 refs = self._repo.refs.as_dict()
263 refs = self._repo.refs.as_dict()
264 sortkey = lambda ctx: ctx[0]
264 sortkey = lambda ctx: ctx[0]
265 _branches = [('/'.join(ref.split('/')[2:]), head)
265 _branches = [('/'.join(ref.split('/')[2:]), head)
266 for ref, head in refs.items()
266 for ref, head in refs.items()
267 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
267 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
268 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
268 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
269
269
270 def _heads(self, reverse=False):
270 def _heads(self, reverse=False):
271 refs = self._repo.get_refs()
271 refs = self._repo.get_refs()
272 heads = {}
272 heads = {}
273
273
274 for key, val in refs.items():
274 for key, val in refs.items():
275 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
275 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
276 if key.startswith(ref_key):
276 if key.startswith(ref_key):
277 n = key[len(ref_key):]
277 n = key[len(ref_key):]
278 if n not in ['HEAD']:
278 if n not in ['HEAD']:
279 heads[n] = val
279 heads[n] = val
280
280
281 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
281 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
282
282
283 def _get_tags(self):
283 def _get_tags(self):
284 if not self.revisions:
284 if not self.revisions:
285 return {}
285 return {}
286 sortkey = lambda ctx: ctx[0]
286 sortkey = lambda ctx: ctx[0]
287 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
287 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
288 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
288 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
289 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
289 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
290
290
291 @LazyProperty
291 @LazyProperty
292 def tags(self):
292 def tags(self):
293 return self._get_tags()
293 return self._get_tags()
294
294
295 def tag(self, name, user, revision=None, message=None, date=None,
295 def tag(self, name, user, revision=None, message=None, date=None,
296 **kwargs):
296 **kwargs):
297 """
297 """
298 Creates and returns a tag for the given ``revision``.
298 Creates and returns a tag for the given ``revision``.
299
299
300 :param name: name for new tag
300 :param name: name for new tag
301 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
301 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
302 :param revision: changeset id for which new tag would be created
302 :param revision: changeset id for which new tag would be created
303 :param message: message of the tag's commit
303 :param message: message of the tag's commit
304 :param date: date of tag's commit
304 :param date: date of tag's commit
305
305
306 :raises TagAlreadyExistError: if tag with same name already exists
306 :raises TagAlreadyExistError: if tag with same name already exists
307 """
307 """
308 if name in self.tags:
308 if name in self.tags:
309 raise TagAlreadyExistError("Tag %s already exists" % name)
309 raise TagAlreadyExistError("Tag %s already exists" % name)
310 changeset = self.get_changeset(revision)
310 changeset = self.get_changeset(revision)
311 message = message or "Added tag %s for commit %s" % (name,
311 message = message or "Added tag %s for commit %s" % (name,
312 changeset.raw_id)
312 changeset.raw_id)
313 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
313 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
314
314
315 self.tags = self._get_tags()
315 self.tags = self._get_tags()
316 return changeset
316 return changeset
317
317
318 def remove_tag(self, name, user, message=None, date=None):
318 def remove_tag(self, name, user, message=None, date=None):
319 """
319 """
320 Removes tag with the given ``name``.
320 Removes tag with the given ``name``.
321
321
322 :param name: name of the tag to be removed
322 :param name: name of the tag to be removed
323 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
323 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
324 :param message: message of the tag's removal commit
324 :param message: message of the tag's removal commit
325 :param date: date of tag's removal commit
325 :param date: date of tag's removal commit
326
326
327 :raises TagDoesNotExistError: if tag with given name does not exists
327 :raises TagDoesNotExistError: if tag with given name does not exists
328 """
328 """
329 if name not in self.tags:
329 if name not in self.tags:
330 raise TagDoesNotExistError("Tag %s does not exist" % name)
330 raise TagDoesNotExistError("Tag %s does not exist" % name)
331 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
331 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
332 try:
332 try:
333 os.remove(tagpath)
333 os.remove(tagpath)
334 self.tags = self._get_tags()
334 self.tags = self._get_tags()
335 except OSError, e:
335 except OSError, e:
336 raise RepositoryError(e.strerror)
336 raise RepositoryError(e.strerror)
337
337
338 def get_changeset(self, revision=None):
338 def get_changeset(self, revision=None):
339 """
339 """
340 Returns ``GitChangeset`` object representing commit from git repository
340 Returns ``GitChangeset`` object representing commit from git repository
341 at the given revision or head (most recent commit) if None given.
341 at the given revision or head (most recent commit) if None given.
342 """
342 """
343 if isinstance(revision, GitChangeset):
343 if isinstance(revision, GitChangeset):
344 return revision
344 return revision
345 revision = self._get_revision(revision)
345 revision = self._get_revision(revision)
346 changeset = GitChangeset(repository=self, revision=revision)
346 changeset = GitChangeset(repository=self, revision=revision)
347 return changeset
347 return changeset
348
348
349 def get_changesets(self, start=None, end=None, start_date=None,
349 def get_changesets(self, start=None, end=None, start_date=None,
350 end_date=None, branch_name=None, reverse=False):
350 end_date=None, branch_name=None, reverse=False):
351 """
351 """
352 Returns iterator of ``GitChangeset`` objects from start to end (both
352 Returns iterator of ``GitChangeset`` objects from start to end (both
353 are inclusive), in ascending date order (unless ``reverse`` is set).
353 are inclusive), in ascending date order (unless ``reverse`` is set).
354
354
355 :param start: changeset ID, as str; first returned changeset
355 :param start: changeset ID, as str; first returned changeset
356 :param end: changeset ID, as str; last returned changeset
356 :param end: changeset ID, as str; last returned changeset
357 :param start_date: if specified, changesets with commit date less than
357 :param start_date: if specified, changesets with commit date less than
358 ``start_date`` would be filtered out from returned set
358 ``start_date`` would be filtered out from returned set
359 :param end_date: if specified, changesets with commit date greater than
359 :param end_date: if specified, changesets with commit date greater than
360 ``end_date`` would be filtered out from returned set
360 ``end_date`` would be filtered out from returned set
361 :param branch_name: if specified, changesets not reachable from given
361 :param branch_name: if specified, changesets not reachable from given
362 branch would be filtered out from returned set
362 branch would be filtered out from returned set
363 :param reverse: if ``True``, returned generator would be reversed
363 :param reverse: if ``True``, returned generator would be reversed
364 (meaning that returned changesets would have descending date order)
364 (meaning that returned changesets would have descending date order)
365
365
366 :raise BranchDoesNotExistError: If given ``branch_name`` does not
366 :raise BranchDoesNotExistError: If given ``branch_name`` does not
367 exist.
367 exist.
368 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
368 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
369 ``end`` could not be found.
369 ``end`` could not be found.
370
370
371 """
371 """
372 if branch_name and branch_name not in self.branches:
372 if branch_name and branch_name not in self.branches:
373 raise BranchDoesNotExistError("Branch '%s' not found" \
373 raise BranchDoesNotExistError("Branch '%s' not found" \
374 % branch_name)
374 % branch_name)
375 # %H at format means (full) commit hash, initial hashes are retrieved
375 # %H at format means (full) commit hash, initial hashes are retrieved
376 # in ascending date order
376 # in ascending date order
377 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
377 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
378 cmd_params = {}
378 cmd_params = {}
379 if start_date:
379 if start_date:
380 cmd_template += ' --since "$since"'
380 cmd_template += ' --since "$since"'
381 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
381 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
382 if end_date:
382 if end_date:
383 cmd_template += ' --until "$until"'
383 cmd_template += ' --until "$until"'
384 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
384 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
385 if branch_name:
385 if branch_name:
386 cmd_template += ' $branch_name'
386 cmd_template += ' $branch_name'
387 cmd_params['branch_name'] = branch_name
387 cmd_params['branch_name'] = branch_name
388 else:
388 else:
389 cmd_template += ' --all'
389 cmd_template += ' --all'
390
390
391 cmd = Template(cmd_template).safe_substitute(**cmd_params)
391 cmd = Template(cmd_template).safe_substitute(**cmd_params)
392 revs = self.run_git_command(cmd)[0].splitlines()
392 revs = self.run_git_command(cmd)[0].splitlines()
393 start_pos = 0
393 start_pos = 0
394 end_pos = len(revs)
394 end_pos = len(revs)
395 if start:
395 if start:
396 _start = self._get_revision(start)
396 _start = self._get_revision(start)
397 try:
397 try:
398 start_pos = revs.index(_start)
398 start_pos = revs.index(_start)
399 except ValueError:
399 except ValueError:
400 pass
400 pass
401
401
402 if end is not None:
402 if end is not None:
403 _end = self._get_revision(end)
403 _end = self._get_revision(end)
404 try:
404 try:
405 end_pos = revs.index(_end)
405 end_pos = revs.index(_end)
406 except ValueError:
406 except ValueError:
407 pass
407 pass
408
408
409 if None not in [start, end] and start_pos > end_pos:
409 if None not in [start, end] and start_pos > end_pos:
410 raise RepositoryError('start cannot be after end')
410 raise RepositoryError('start cannot be after end')
411
411
412 if end_pos is not None:
412 if end_pos is not None:
413 end_pos += 1
413 end_pos += 1
414
414
415 revs = revs[start_pos:end_pos]
415 revs = revs[start_pos:end_pos]
416 if reverse:
416 if reverse:
417 revs = reversed(revs)
417 revs = reversed(revs)
418 for rev in revs:
418 for rev in revs:
419 yield self.get_changeset(rev)
419 yield self.get_changeset(rev)
420
420
421 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
421 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
422 context=3):
422 context=3):
423 """
423 """
424 Returns (git like) *diff*, as plain text. Shows changes introduced by
424 Returns (git like) *diff*, as plain text. Shows changes introduced by
425 ``rev2`` since ``rev1``.
425 ``rev2`` since ``rev1``.
426
426
427 :param rev1: Entry point from which diff is shown. Can be
427 :param rev1: Entry point from which diff is shown. Can be
428 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
428 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
429 the changes since empty state of the repository until ``rev2``
429 the changes since empty state of the repository until ``rev2``
430 :param rev2: Until which revision changes should be shown.
430 :param rev2: Until which revision changes should be shown.
431 :param ignore_whitespace: If set to ``True``, would not show whitespace
431 :param ignore_whitespace: If set to ``True``, would not show whitespace
432 changes. Defaults to ``False``.
432 changes. Defaults to ``False``.
433 :param context: How many lines before/after changed lines should be
433 :param context: How many lines before/after changed lines should be
434 shown. Defaults to ``3``.
434 shown. Defaults to ``3``.
435 """
435 """
436 flags = ['-U%s' % context]
436 flags = ['-U%s' % context]
437 if ignore_whitespace:
437 if ignore_whitespace:
438 flags.append('-w')
438 flags.append('-w')
439
439
440 if hasattr(rev1, 'raw_id'):
441 rev1 = getattr(rev1, 'raw_id')
442
443 if hasattr(rev2, 'raw_id'):
444 rev2 = getattr(rev2, 'raw_id')
445
440 if rev1 == self.EMPTY_CHANGESET:
446 if rev1 == self.EMPTY_CHANGESET:
441 rev2 = self.get_changeset(rev2).raw_id
447 rev2 = self.get_changeset(rev2).raw_id
442 cmd = ' '.join(['show'] + flags + [rev2])
448 cmd = ' '.join(['show'] + flags + [rev2])
443 else:
449 else:
444 rev1 = self.get_changeset(rev1).raw_id
450 rev1 = self.get_changeset(rev1).raw_id
445 rev2 = self.get_changeset(rev2).raw_id
451 rev2 = self.get_changeset(rev2).raw_id
446 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
452 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
447
453
448 if path:
454 if path:
449 cmd += ' -- "%s"' % path
455 cmd += ' -- "%s"' % path
450 stdout, stderr = self.run_git_command(cmd)
456 stdout, stderr = self.run_git_command(cmd)
451 # If we used 'show' command, strip first few lines (until actual diff
457 # If we used 'show' command, strip first few lines (until actual diff
452 # starts)
458 # starts)
453 if rev1 == self.EMPTY_CHANGESET:
459 if rev1 == self.EMPTY_CHANGESET:
454 lines = stdout.splitlines()
460 lines = stdout.splitlines()
455 x = 0
461 x = 0
456 for line in lines:
462 for line in lines:
457 if line.startswith('diff'):
463 if line.startswith('diff'):
458 break
464 break
459 x += 1
465 x += 1
460 # Append new line just like 'diff' command do
466 # Append new line just like 'diff' command do
461 stdout = '\n'.join(lines[x:]) + '\n'
467 stdout = '\n'.join(lines[x:]) + '\n'
462 return stdout
468 return stdout
463
469
464 @LazyProperty
470 @LazyProperty
465 def in_memory_changeset(self):
471 def in_memory_changeset(self):
466 """
472 """
467 Returns ``GitInMemoryChangeset`` object for this repository.
473 Returns ``GitInMemoryChangeset`` object for this repository.
468 """
474 """
469 return GitInMemoryChangeset(self)
475 return GitInMemoryChangeset(self)
470
476
471 def clone(self, url, update_after_clone=True, bare=False):
477 def clone(self, url, update_after_clone=True, bare=False):
472 """
478 """
473 Tries to clone changes from external location.
479 Tries to clone changes from external location.
474
480
475 :param update_after_clone: If set to ``False``, git won't checkout
481 :param update_after_clone: If set to ``False``, git won't checkout
476 working directory
482 working directory
477 :param bare: If set to ``True``, repository would be cloned into
483 :param bare: If set to ``True``, repository would be cloned into
478 *bare* git repository (no working directory at all).
484 *bare* git repository (no working directory at all).
479 """
485 """
480 url = self._get_url(url)
486 url = self._get_url(url)
481 cmd = ['clone']
487 cmd = ['clone']
482 if bare:
488 if bare:
483 cmd.append('--bare')
489 cmd.append('--bare')
484 elif not update_after_clone:
490 elif not update_after_clone:
485 cmd.append('--no-checkout')
491 cmd.append('--no-checkout')
486 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
492 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
487 cmd = ' '.join(cmd)
493 cmd = ' '.join(cmd)
488 # If error occurs run_git_command raises RepositoryError already
494 # If error occurs run_git_command raises RepositoryError already
489 self.run_git_command(cmd)
495 self.run_git_command(cmd)
490
496
491 def pull(self, url):
497 def pull(self, url):
492 """
498 """
493 Tries to pull changes from external location.
499 Tries to pull changes from external location.
494 """
500 """
495 url = self._get_url(url)
501 url = self._get_url(url)
496 cmd = ['pull']
502 cmd = ['pull']
497 cmd.append("--ff-only")
503 cmd.append("--ff-only")
498 cmd.append(url)
504 cmd.append(url)
499 cmd = ' '.join(cmd)
505 cmd = ' '.join(cmd)
500 # If error occurs run_git_command raises RepositoryError already
506 # If error occurs run_git_command raises RepositoryError already
501 self.run_git_command(cmd)
507 self.run_git_command(cmd)
502
508
503 def fetch(self, url):
509 def fetch(self, url):
504 """
510 """
505 Tries to pull changes from external location.
511 Tries to pull changes from external location.
506 """
512 """
507 url = self._get_url(url)
513 url = self._get_url(url)
508 cmd = ['fetch']
514 cmd = ['fetch']
509 cmd.append(url)
515 cmd.append(url)
510 cmd = ' '.join(cmd)
516 cmd = ' '.join(cmd)
511 # If error occurs run_git_command raises RepositoryError already
517 # If error occurs run_git_command raises RepositoryError already
512 self.run_git_command(cmd)
518 self.run_git_command(cmd)
513
519
514 @LazyProperty
520 @LazyProperty
515 def workdir(self):
521 def workdir(self):
516 """
522 """
517 Returns ``Workdir`` instance for this repository.
523 Returns ``Workdir`` instance for this repository.
518 """
524 """
519 return GitWorkdir(self)
525 return GitWorkdir(self)
520
526
521 def get_config_value(self, section, name, config_file=None):
527 def get_config_value(self, section, name, config_file=None):
522 """
528 """
523 Returns configuration value for a given [``section``] and ``name``.
529 Returns configuration value for a given [``section``] and ``name``.
524
530
525 :param section: Section we want to retrieve value from
531 :param section: Section we want to retrieve value from
526 :param name: Name of configuration we want to retrieve
532 :param name: Name of configuration we want to retrieve
527 :param config_file: A path to file which should be used to retrieve
533 :param config_file: A path to file which should be used to retrieve
528 configuration from (might also be a list of file paths)
534 configuration from (might also be a list of file paths)
529 """
535 """
530 if config_file is None:
536 if config_file is None:
531 config_file = []
537 config_file = []
532 elif isinstance(config_file, basestring):
538 elif isinstance(config_file, basestring):
533 config_file = [config_file]
539 config_file = [config_file]
534
540
535 def gen_configs():
541 def gen_configs():
536 for path in config_file + self._config_files:
542 for path in config_file + self._config_files:
537 try:
543 try:
538 yield ConfigFile.from_path(path)
544 yield ConfigFile.from_path(path)
539 except (IOError, OSError, ValueError):
545 except (IOError, OSError, ValueError):
540 continue
546 continue
541
547
542 for config in gen_configs():
548 for config in gen_configs():
543 try:
549 try:
544 return config.get(section, name)
550 return config.get(section, name)
545 except KeyError:
551 except KeyError:
546 continue
552 continue
547 return None
553 return None
548
554
549 def get_user_name(self, config_file=None):
555 def get_user_name(self, config_file=None):
550 """
556 """
551 Returns user's name from global configuration file.
557 Returns user's name from global configuration file.
552
558
553 :param config_file: A path to file which should be used to retrieve
559 :param config_file: A path to file which should be used to retrieve
554 configuration from (might also be a list of file paths)
560 configuration from (might also be a list of file paths)
555 """
561 """
556 return self.get_config_value('user', 'name', config_file)
562 return self.get_config_value('user', 'name', config_file)
557
563
558 def get_user_email(self, config_file=None):
564 def get_user_email(self, config_file=None):
559 """
565 """
560 Returns user's email from global configuration file.
566 Returns user's email from global configuration file.
561
567
562 :param config_file: A path to file which should be used to retrieve
568 :param config_file: A path to file which should be used to retrieve
563 configuration from (might also be a list of file paths)
569 configuration from (might also be a list of file paths)
564 """
570 """
565 return self.get_config_value('user', 'email', config_file)
571 return self.get_config_value('user', 'email', config_file)
@@ -1,357 +1,362 b''
1 import os
1 import os
2 import posixpath
2 import posixpath
3
3
4 from rhodecode.lib.vcs.backends.base import BaseChangeset
4 from rhodecode.lib.vcs.backends.base import BaseChangeset
5 from rhodecode.lib.vcs.conf import settings
5 from rhodecode.lib.vcs.conf import settings
6 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \
6 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \
7 ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
7 ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
8 from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
8 from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
9 ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
9 ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
10 RemovedFileNodesGenerator, RootNode, SubModuleNode
10 RemovedFileNodesGenerator, RootNode, SubModuleNode
11
11
12 from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
12 from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
13 from rhodecode.lib.vcs.utils.lazy import LazyProperty
13 from rhodecode.lib.vcs.utils.lazy import LazyProperty
14 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
14 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
15
15
16 from ...utils.hgcompat import archival, hex
16 from ...utils.hgcompat import archival, hex
17
17
18
18
19 class MercurialChangeset(BaseChangeset):
19 class MercurialChangeset(BaseChangeset):
20 """
20 """
21 Represents state of the repository at the single revision.
21 Represents state of the repository at the single revision.
22 """
22 """
23
23
24 def __init__(self, repository, revision):
24 def __init__(self, repository, revision):
25 self.repository = repository
25 self.repository = repository
26 self.raw_id = revision
26 self.raw_id = revision
27 self._ctx = repository._repo[revision]
27 self._ctx = repository._repo[revision]
28 self.revision = self._ctx._rev
28 self.revision = self._ctx._rev
29 self.nodes = {}
29 self.nodes = {}
30
30
31 @LazyProperty
31 @LazyProperty
32 def tags(self):
32 def tags(self):
33 return map(safe_unicode, self._ctx.tags())
33 return map(safe_unicode, self._ctx.tags())
34
34
35 @LazyProperty
35 @LazyProperty
36 def branch(self):
36 def branch(self):
37 return safe_unicode(self._ctx.branch())
37 return safe_unicode(self._ctx.branch())
38
38
39 @LazyProperty
39 @LazyProperty
40 def bookmarks(self):
40 def bookmarks(self):
41 return map(safe_unicode, self._ctx.bookmarks())
41 return map(safe_unicode, self._ctx.bookmarks())
42
42
43 @LazyProperty
43 @LazyProperty
44 def message(self):
44 def message(self):
45 return safe_unicode(self._ctx.description())
45 return safe_unicode(self._ctx.description())
46
46
47 @LazyProperty
47 @LazyProperty
48 def author(self):
48 def author(self):
49 return safe_unicode(self._ctx.user())
49 return safe_unicode(self._ctx.user())
50
50
51 @LazyProperty
51 @LazyProperty
52 def date(self):
52 def date(self):
53 return date_fromtimestamp(*self._ctx.date())
53 return date_fromtimestamp(*self._ctx.date())
54
54
55 @LazyProperty
55 @LazyProperty
56 def status(self):
56 def status(self):
57 """
57 """
58 Returns modified, added, removed, deleted files for current changeset
58 Returns modified, added, removed, deleted files for current changeset
59 """
59 """
60 return self.repository._repo.status(self._ctx.p1().node(),
60 return self.repository._repo.status(self._ctx.p1().node(),
61 self._ctx.node())
61 self._ctx.node())
62
62
63 @LazyProperty
63 @LazyProperty
64 def _file_paths(self):
64 def _file_paths(self):
65 return list(self._ctx)
65 return list(self._ctx)
66
66
67 @LazyProperty
67 @LazyProperty
68 def _dir_paths(self):
68 def _dir_paths(self):
69 p = list(set(get_dirs_for_path(*self._file_paths)))
69 p = list(set(get_dirs_for_path(*self._file_paths)))
70 p.insert(0, '')
70 p.insert(0, '')
71 return p
71 return p
72
72
73 @LazyProperty
73 @LazyProperty
74 def _paths(self):
74 def _paths(self):
75 return self._dir_paths + self._file_paths
75 return self._dir_paths + self._file_paths
76
76
77 @LazyProperty
77 @LazyProperty
78 def id(self):
78 def id(self):
79 if self.last:
79 if self.last:
80 return u'tip'
80 return u'tip'
81 return self.short_id
81 return self.short_id
82
82
83 @LazyProperty
83 @LazyProperty
84 def short_id(self):
84 def short_id(self):
85 return self.raw_id[:12]
85 return self.raw_id[:12]
86
86
87 @LazyProperty
87 @LazyProperty
88 def parents(self):
88 def parents(self):
89 """
89 """
90 Returns list of parents changesets.
90 Returns list of parents changesets.
91 """
91 """
92 return [self.repository.get_changeset(parent.rev())
92 return [self.repository.get_changeset(parent.rev())
93 for parent in self._ctx.parents() if parent.rev() >= 0]
93 for parent in self._ctx.parents() if parent.rev() >= 0]
94
94
95 def next(self, branch=None):
95 def next(self, branch=None):
96
96
97 if branch and self.branch != branch:
97 if branch and self.branch != branch:
98 raise VCSError('Branch option used on changeset not belonging '
98 raise VCSError('Branch option used on changeset not belonging '
99 'to that branch')
99 'to that branch')
100
100
101 def _next(changeset, branch):
101 def _next(changeset, branch):
102 try:
102 try:
103 next_ = changeset.revision + 1
103 next_ = changeset.revision + 1
104 next_rev = changeset.repository.revisions[next_]
104 next_rev = changeset.repository.revisions[next_]
105 except IndexError:
105 except IndexError:
106 raise ChangesetDoesNotExistError
106 raise ChangesetDoesNotExistError
107 cs = changeset.repository.get_changeset(next_rev)
107 cs = changeset.repository.get_changeset(next_rev)
108
108
109 if branch and branch != cs.branch:
109 if branch and branch != cs.branch:
110 return _next(cs, branch)
110 return _next(cs, branch)
111
111
112 return cs
112 return cs
113
113
114 return _next(self, branch)
114 return _next(self, branch)
115
115
116 def prev(self, branch=None):
116 def prev(self, branch=None):
117 if branch and self.branch != branch:
117 if branch and self.branch != branch:
118 raise VCSError('Branch option used on changeset not belonging '
118 raise VCSError('Branch option used on changeset not belonging '
119 'to that branch')
119 'to that branch')
120
120
121 def _prev(changeset, branch):
121 def _prev(changeset, branch):
122 try:
122 try:
123 prev_ = changeset.revision - 1
123 prev_ = changeset.revision - 1
124 if prev_ < 0:
124 if prev_ < 0:
125 raise IndexError
125 raise IndexError
126 prev_rev = changeset.repository.revisions[prev_]
126 prev_rev = changeset.repository.revisions[prev_]
127 except IndexError:
127 except IndexError:
128 raise ChangesetDoesNotExistError
128 raise ChangesetDoesNotExistError
129
129
130 cs = changeset.repository.get_changeset(prev_rev)
130 cs = changeset.repository.get_changeset(prev_rev)
131
131
132 if branch and branch != cs.branch:
132 if branch and branch != cs.branch:
133 return _prev(cs, branch)
133 return _prev(cs, branch)
134
134
135 return cs
135 return cs
136
136
137 return _prev(self, branch)
137 return _prev(self, branch)
138
138
139 def diff(self, ignore_whitespace=True, context=3):
140 return ''.join(self._ctx.diff(git=True,
141 ignore_whitespace=ignore_whitespace,
142 context=context))
143
139 def _fix_path(self, path):
144 def _fix_path(self, path):
140 """
145 """
141 Paths are stored without trailing slash so we need to get rid off it if
146 Paths are stored without trailing slash so we need to get rid off it if
142 needed. Also mercurial keeps filenodes as str so we need to decode
147 needed. Also mercurial keeps filenodes as str so we need to decode
143 from unicode to str
148 from unicode to str
144 """
149 """
145 if path.endswith('/'):
150 if path.endswith('/'):
146 path = path.rstrip('/')
151 path = path.rstrip('/')
147
152
148 return safe_str(path)
153 return safe_str(path)
149
154
150 def _get_kind(self, path):
155 def _get_kind(self, path):
151 path = self._fix_path(path)
156 path = self._fix_path(path)
152 if path in self._file_paths:
157 if path in self._file_paths:
153 return NodeKind.FILE
158 return NodeKind.FILE
154 elif path in self._dir_paths:
159 elif path in self._dir_paths:
155 return NodeKind.DIR
160 return NodeKind.DIR
156 else:
161 else:
157 raise ChangesetError("Node does not exist at the given path %r"
162 raise ChangesetError("Node does not exist at the given path %r"
158 % (path))
163 % (path))
159
164
160 def _get_filectx(self, path):
165 def _get_filectx(self, path):
161 path = self._fix_path(path)
166 path = self._fix_path(path)
162 if self._get_kind(path) != NodeKind.FILE:
167 if self._get_kind(path) != NodeKind.FILE:
163 raise ChangesetError("File does not exist for revision %r at "
168 raise ChangesetError("File does not exist for revision %r at "
164 " %r" % (self.revision, path))
169 " %r" % (self.revision, path))
165 return self._ctx.filectx(path)
170 return self._ctx.filectx(path)
166
171
167 def _extract_submodules(self):
172 def _extract_submodules(self):
168 """
173 """
169 returns a dictionary with submodule information from substate file
174 returns a dictionary with submodule information from substate file
170 of hg repository
175 of hg repository
171 """
176 """
172 return self._ctx.substate
177 return self._ctx.substate
173
178
174 def get_file_mode(self, path):
179 def get_file_mode(self, path):
175 """
180 """
176 Returns stat mode of the file at the given ``path``.
181 Returns stat mode of the file at the given ``path``.
177 """
182 """
178 fctx = self._get_filectx(path)
183 fctx = self._get_filectx(path)
179 if 'x' in fctx.flags():
184 if 'x' in fctx.flags():
180 return 0100755
185 return 0100755
181 else:
186 else:
182 return 0100644
187 return 0100644
183
188
184 def get_file_content(self, path):
189 def get_file_content(self, path):
185 """
190 """
186 Returns content of the file at given ``path``.
191 Returns content of the file at given ``path``.
187 """
192 """
188 fctx = self._get_filectx(path)
193 fctx = self._get_filectx(path)
189 return fctx.data()
194 return fctx.data()
190
195
191 def get_file_size(self, path):
196 def get_file_size(self, path):
192 """
197 """
193 Returns size of the file at given ``path``.
198 Returns size of the file at given ``path``.
194 """
199 """
195 fctx = self._get_filectx(path)
200 fctx = self._get_filectx(path)
196 return fctx.size()
201 return fctx.size()
197
202
198 def get_file_changeset(self, path):
203 def get_file_changeset(self, path):
199 """
204 """
200 Returns last commit of the file at the given ``path``.
205 Returns last commit of the file at the given ``path``.
201 """
206 """
202 node = self.get_node(path)
207 node = self.get_node(path)
203 return node.history[0]
208 return node.history[0]
204
209
205 def get_file_history(self, path):
210 def get_file_history(self, path):
206 """
211 """
207 Returns history of file as reversed list of ``Changeset`` objects for
212 Returns history of file as reversed list of ``Changeset`` objects for
208 which file at given ``path`` has been modified.
213 which file at given ``path`` has been modified.
209 """
214 """
210 fctx = self._get_filectx(path)
215 fctx = self._get_filectx(path)
211 nodes = [fctx.filectx(x).node() for x in fctx.filelog()]
216 nodes = [fctx.filectx(x).node() for x in fctx.filelog()]
212 changesets = [self.repository.get_changeset(hex(node))
217 changesets = [self.repository.get_changeset(hex(node))
213 for node in reversed(nodes)]
218 for node in reversed(nodes)]
214 return changesets
219 return changesets
215
220
216 def get_file_annotate(self, path):
221 def get_file_annotate(self, path):
217 """
222 """
218 Returns a list of three element tuples with lineno,changeset and line
223 Returns a list of three element tuples with lineno,changeset and line
219 """
224 """
220 fctx = self._get_filectx(path)
225 fctx = self._get_filectx(path)
221 annotate = []
226 annotate = []
222 for i, annotate_data in enumerate(fctx.annotate()):
227 for i, annotate_data in enumerate(fctx.annotate()):
223 ln_no = i + 1
228 ln_no = i + 1
224 annotate.append((ln_no, self.repository\
229 annotate.append((ln_no, self.repository\
225 .get_changeset(hex(annotate_data[0].node())),
230 .get_changeset(hex(annotate_data[0].node())),
226 annotate_data[1],))
231 annotate_data[1],))
227
232
228 return annotate
233 return annotate
229
234
230 def fill_archive(self, stream=None, kind='tgz', prefix=None,
235 def fill_archive(self, stream=None, kind='tgz', prefix=None,
231 subrepos=False):
236 subrepos=False):
232 """
237 """
233 Fills up given stream.
238 Fills up given stream.
234
239
235 :param stream: file like object.
240 :param stream: file like object.
236 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
241 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
237 Default: ``tgz``.
242 Default: ``tgz``.
238 :param prefix: name of root directory in archive.
243 :param prefix: name of root directory in archive.
239 Default is repository name and changeset's raw_id joined with dash
244 Default is repository name and changeset's raw_id joined with dash
240 (``repo-tip.<KIND>``).
245 (``repo-tip.<KIND>``).
241 :param subrepos: include subrepos in this archive.
246 :param subrepos: include subrepos in this archive.
242
247
243 :raise ImproperArchiveTypeError: If given kind is wrong.
248 :raise ImproperArchiveTypeError: If given kind is wrong.
244 :raise VcsError: If given stream is None
249 :raise VcsError: If given stream is None
245 """
250 """
246
251
247 allowed_kinds = settings.ARCHIVE_SPECS.keys()
252 allowed_kinds = settings.ARCHIVE_SPECS.keys()
248 if kind not in allowed_kinds:
253 if kind not in allowed_kinds:
249 raise ImproperArchiveTypeError('Archive kind not supported use one'
254 raise ImproperArchiveTypeError('Archive kind not supported use one'
250 'of %s', allowed_kinds)
255 'of %s', allowed_kinds)
251
256
252 if stream is None:
257 if stream is None:
253 raise VCSError('You need to pass in a valid stream for filling'
258 raise VCSError('You need to pass in a valid stream for filling'
254 ' with archival data')
259 ' with archival data')
255
260
256 if prefix is None:
261 if prefix is None:
257 prefix = '%s-%s' % (self.repository.name, self.short_id)
262 prefix = '%s-%s' % (self.repository.name, self.short_id)
258 elif prefix.startswith('/'):
263 elif prefix.startswith('/'):
259 raise VCSError("Prefix cannot start with leading slash")
264 raise VCSError("Prefix cannot start with leading slash")
260 elif prefix.strip() == '':
265 elif prefix.strip() == '':
261 raise VCSError("Prefix cannot be empty")
266 raise VCSError("Prefix cannot be empty")
262
267
263 archival.archive(self.repository._repo, stream, self.raw_id,
268 archival.archive(self.repository._repo, stream, self.raw_id,
264 kind, prefix=prefix, subrepos=subrepos)
269 kind, prefix=prefix, subrepos=subrepos)
265
270
266 if stream.closed and hasattr(stream, 'name'):
271 if stream.closed and hasattr(stream, 'name'):
267 stream = open(stream.name, 'rb')
272 stream = open(stream.name, 'rb')
268 elif hasattr(stream, 'mode') and 'r' not in stream.mode:
273 elif hasattr(stream, 'mode') and 'r' not in stream.mode:
269 stream = open(stream.name, 'rb')
274 stream = open(stream.name, 'rb')
270 else:
275 else:
271 stream.seek(0)
276 stream.seek(0)
272
277
273 def get_nodes(self, path):
278 def get_nodes(self, path):
274 """
279 """
275 Returns combined ``DirNode`` and ``FileNode`` objects list representing
280 Returns combined ``DirNode`` and ``FileNode`` objects list representing
276 state of changeset at the given ``path``. If node at the given ``path``
281 state of changeset at the given ``path``. If node at the given ``path``
277 is not instance of ``DirNode``, ChangesetError would be raised.
282 is not instance of ``DirNode``, ChangesetError would be raised.
278 """
283 """
279
284
280 if self._get_kind(path) != NodeKind.DIR:
285 if self._get_kind(path) != NodeKind.DIR:
281 raise ChangesetError("Directory does not exist for revision %r at "
286 raise ChangesetError("Directory does not exist for revision %r at "
282 " %r" % (self.revision, path))
287 " %r" % (self.revision, path))
283 path = self._fix_path(path)
288 path = self._fix_path(path)
284
289
285 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
290 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
286 if os.path.dirname(f) == path]
291 if os.path.dirname(f) == path]
287 dirs = path == '' and '' or [d for d in self._dir_paths
292 dirs = path == '' and '' or [d for d in self._dir_paths
288 if d and posixpath.dirname(d) == path]
293 if d and posixpath.dirname(d) == path]
289 dirnodes = [DirNode(d, changeset=self) for d in dirs
294 dirnodes = [DirNode(d, changeset=self) for d in dirs
290 if os.path.dirname(d) == path]
295 if os.path.dirname(d) == path]
291
296
292 als = self.repository.alias
297 als = self.repository.alias
293 for k, vals in self._extract_submodules().iteritems():
298 for k, vals in self._extract_submodules().iteritems():
294 #vals = url,rev,type
299 #vals = url,rev,type
295 loc = vals[0]
300 loc = vals[0]
296 cs = vals[1]
301 cs = vals[1]
297 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
302 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
298 alias=als))
303 alias=als))
299 nodes = dirnodes + filenodes
304 nodes = dirnodes + filenodes
300 # cache nodes
305 # cache nodes
301 for node in nodes:
306 for node in nodes:
302 self.nodes[node.path] = node
307 self.nodes[node.path] = node
303 nodes.sort()
308 nodes.sort()
304
309
305 return nodes
310 return nodes
306
311
307 def get_node(self, path):
312 def get_node(self, path):
308 """
313 """
309 Returns ``Node`` object from the given ``path``. If there is no node at
314 Returns ``Node`` object from the given ``path``. If there is no node at
310 the given ``path``, ``ChangesetError`` would be raised.
315 the given ``path``, ``ChangesetError`` would be raised.
311 """
316 """
312
317
313 path = self._fix_path(path)
318 path = self._fix_path(path)
314
319
315 if not path in self.nodes:
320 if not path in self.nodes:
316 if path in self._file_paths:
321 if path in self._file_paths:
317 node = FileNode(path, changeset=self)
322 node = FileNode(path, changeset=self)
318 elif path in self._dir_paths or path in self._dir_paths:
323 elif path in self._dir_paths or path in self._dir_paths:
319 if path == '':
324 if path == '':
320 node = RootNode(changeset=self)
325 node = RootNode(changeset=self)
321 else:
326 else:
322 node = DirNode(path, changeset=self)
327 node = DirNode(path, changeset=self)
323 else:
328 else:
324 raise NodeDoesNotExistError("There is no file nor directory "
329 raise NodeDoesNotExistError("There is no file nor directory "
325 "at the given path: %r at revision %r"
330 "at the given path: %r at revision %r"
326 % (path, self.short_id))
331 % (path, self.short_id))
327 # cache node
332 # cache node
328 self.nodes[path] = node
333 self.nodes[path] = node
329 return self.nodes[path]
334 return self.nodes[path]
330
335
331 @LazyProperty
336 @LazyProperty
332 def affected_files(self):
337 def affected_files(self):
333 """
338 """
334 Get's a fast accessible file changes for given changeset
339 Get's a fast accessible file changes for given changeset
335 """
340 """
336 return self._ctx.files()
341 return self._ctx.files()
337
342
338 @property
343 @property
339 def added(self):
344 def added(self):
340 """
345 """
341 Returns list of added ``FileNode`` objects.
346 Returns list of added ``FileNode`` objects.
342 """
347 """
343 return AddedFileNodesGenerator([n for n in self.status[1]], self)
348 return AddedFileNodesGenerator([n for n in self.status[1]], self)
344
349
345 @property
350 @property
346 def changed(self):
351 def changed(self):
347 """
352 """
348 Returns list of modified ``FileNode`` objects.
353 Returns list of modified ``FileNode`` objects.
349 """
354 """
350 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
355 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
351
356
352 @property
357 @property
353 def removed(self):
358 def removed(self):
354 """
359 """
355 Returns list of removed ``FileNode`` objects.
360 Returns list of removed ``FileNode`` objects.
356 """
361 """
357 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
362 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
General Comments 0
You need to be logged in to leave comments. Login now