##// END OF EJS Templates
fixed issues with gitsubmodule diffs
marcink -
r2233:07fce193 beta
parent child Browse files
Show More
@@ -1,517 +1,524 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
28 import re
28 import re
29 import difflib
29 import difflib
30 import markupsafe
30 import markupsafe
31 from itertools import tee, imap
31 from itertools import tee, imap
32
32
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34
34
35 from rhodecode.lib.vcs.exceptions import VCSError
35 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.nodes import FileNode
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37
37 from rhodecode.lib.helpers import escape
38 from rhodecode.lib.utils import EmptyChangeset
38 from rhodecode.lib.utils import EmptyChangeset
39
39
40
40
41 def wrap_to_table(str_):
41 def wrap_to_table(str_):
42 return '''<table class="code-difftable">
42 return '''<table class="code-difftable">
43 <tr class="line no-comment">
43 <tr class="line no-comment">
44 <td class="lineno new"></td>
44 <td class="lineno new"></td>
45 <td class="code no-comment"><pre>%s</pre></td>
45 <td class="code no-comment"><pre>%s</pre></td>
46 </tr>
46 </tr>
47 </table>''' % str_
47 </table>''' % str_
48
48
49
49
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
51 ignore_whitespace=True, line_context=3,
51 ignore_whitespace=True, line_context=3,
52 enable_comments=False):
52 enable_comments=False):
53 """
53 """
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
55 proper message
55 proper message
56 """
56 """
57
57
58 if filenode_old is None:
58 if filenode_old is None:
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
60
60
61 if filenode_old.is_binary or filenode_new.is_binary:
61 if filenode_old.is_binary or filenode_new.is_binary:
62 diff = wrap_to_table(_('binary file'))
62 diff = wrap_to_table(_('binary file'))
63 stats = (0, 0)
63 stats = (0, 0)
64 size = 0
64 size = 0
65
65
66 elif cut_off_limit != -1 and (cut_off_limit is None or
66 elif cut_off_limit != -1 and (cut_off_limit is None or
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
68
68
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
70 ignore_whitespace=ignore_whitespace,
70 ignore_whitespace=ignore_whitespace,
71 context=line_context)
71 context=line_context)
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
73
73
74 diff = diff_processor.as_html(enable_comments=enable_comments)
74 diff = diff_processor.as_html(enable_comments=enable_comments)
75 stats = diff_processor.stat()
75 stats = diff_processor.stat()
76 size = len(diff or '')
76 size = len(diff or '')
77 else:
77 else:
78 diff = wrap_to_table(_('Changeset was to big and was cut off, use '
78 diff = wrap_to_table(_('Changeset was to big and was cut off, use '
79 'diff menu to display this diff'))
79 'diff menu to display this diff'))
80 stats = (0, 0)
80 stats = (0, 0)
81 size = 0
81 size = 0
82
83 if not diff:
82 if not diff:
84 diff = wrap_to_table(_('No changes detected'))
83 submodules = filter(lambda o: isinstance(o, SubModuleNode),
84 [filenode_new, filenode_old])
85 if submodules:
86 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
87 else:
88 diff = wrap_to_table(_('No changes detected'))
85
89
86 cs1 = filenode_old.changeset.raw_id
90 cs1 = filenode_old.changeset.raw_id
87 cs2 = filenode_new.changeset.raw_id
91 cs2 = filenode_new.changeset.raw_id
88
92
89 return size, cs1, cs2, diff, stats
93 return size, cs1, cs2, diff, stats
90
94
91
95
92 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
96 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
93 """
97 """
94 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
98 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
95
99
96 :param ignore_whitespace: ignore whitespaces in diff
100 :param ignore_whitespace: ignore whitespaces in diff
97 """
101 """
98 # make sure we pass in default context
102 # make sure we pass in default context
99 context = context or 3
103 context = context or 3
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
105 [filenode_new, filenode_old])
106 if submodules:
107 return ''
100
108
101 for filenode in (filenode_old, filenode_new):
109 for filenode in (filenode_old, filenode_new):
102 if not isinstance(filenode, FileNode):
110 if not isinstance(filenode, FileNode):
103 raise VCSError("Given object should be FileNode object, not %s"
111 raise VCSError("Given object should be FileNode object, not %s"
104 % filenode.__class__)
112 % filenode.__class__)
105
113
106 repo = filenode_new.changeset.repository
114 repo = filenode_new.changeset.repository
107 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
115 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
108 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
116 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
109
117
110 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
118 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
111 ignore_whitespace, context)
119 ignore_whitespace, context)
112
113 return vcs_gitdiff
120 return vcs_gitdiff
114
121
115
122
116 class DiffProcessor(object):
123 class DiffProcessor(object):
117 """
124 """
118 Give it a unified diff and it returns a list of the files that were
125 Give it a unified diff and it returns a list of the files that were
119 mentioned in the diff together with a dict of meta information that
126 mentioned in the diff together with a dict of meta information that
120 can be used to render it in a HTML template.
127 can be used to render it in a HTML template.
121 """
128 """
122 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
129 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
123
130
124 def __init__(self, diff, differ='diff', format='udiff'):
131 def __init__(self, diff, differ='diff', format='udiff'):
125 """
132 """
126 :param diff: a text in diff format or generator
133 :param diff: a text in diff format or generator
127 :param format: format of diff passed, `udiff` or `gitdiff`
134 :param format: format of diff passed, `udiff` or `gitdiff`
128 """
135 """
129 if isinstance(diff, basestring):
136 if isinstance(diff, basestring):
130 diff = [diff]
137 diff = [diff]
131
138
132 self.__udiff = diff
139 self.__udiff = diff
133 self.__format = format
140 self.__format = format
134 self.adds = 0
141 self.adds = 0
135 self.removes = 0
142 self.removes = 0
136
143
137 if isinstance(self.__udiff, basestring):
144 if isinstance(self.__udiff, basestring):
138 self.lines = iter(self.__udiff.splitlines(1))
145 self.lines = iter(self.__udiff.splitlines(1))
139
146
140 elif self.__format == 'gitdiff':
147 elif self.__format == 'gitdiff':
141 udiff_copy = self.copy_iterator()
148 udiff_copy = self.copy_iterator()
142 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
149 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
143 else:
150 else:
144 udiff_copy = self.copy_iterator()
151 udiff_copy = self.copy_iterator()
145 self.lines = imap(self.escaper, udiff_copy)
152 self.lines = imap(self.escaper, udiff_copy)
146
153
147 # Select a differ.
154 # Select a differ.
148 if differ == 'difflib':
155 if differ == 'difflib':
149 self.differ = self._highlight_line_difflib
156 self.differ = self._highlight_line_difflib
150 else:
157 else:
151 self.differ = self._highlight_line_udiff
158 self.differ = self._highlight_line_udiff
152
159
153 def escaper(self, string):
160 def escaper(self, string):
154 return markupsafe.escape(string)
161 return markupsafe.escape(string)
155
162
156 def copy_iterator(self):
163 def copy_iterator(self):
157 """
164 """
158 make a fresh copy of generator, we should not iterate thru
165 make a fresh copy of generator, we should not iterate thru
159 an original as it's needed for repeating operations on
166 an original as it's needed for repeating operations on
160 this instance of DiffProcessor
167 this instance of DiffProcessor
161 """
168 """
162 self.__udiff, iterator_copy = tee(self.__udiff)
169 self.__udiff, iterator_copy = tee(self.__udiff)
163 return iterator_copy
170 return iterator_copy
164
171
165 def _extract_rev(self, line1, line2):
172 def _extract_rev(self, line1, line2):
166 """
173 """
167 Extract the filename and revision hint from a line.
174 Extract the filename and revision hint from a line.
168 """
175 """
169
176
170 try:
177 try:
171 if line1.startswith('--- ') and line2.startswith('+++ '):
178 if line1.startswith('--- ') and line2.startswith('+++ '):
172 l1 = line1[4:].split(None, 1)
179 l1 = line1[4:].split(None, 1)
173 old_filename = (l1[0].replace('a/', '', 1)
180 old_filename = (l1[0].replace('a/', '', 1)
174 if len(l1) >= 1 else None)
181 if len(l1) >= 1 else None)
175 old_rev = l1[1] if len(l1) == 2 else 'old'
182 old_rev = l1[1] if len(l1) == 2 else 'old'
176
183
177 l2 = line2[4:].split(None, 1)
184 l2 = line2[4:].split(None, 1)
178 new_filename = (l2[0].replace('b/', '', 1)
185 new_filename = (l2[0].replace('b/', '', 1)
179 if len(l1) >= 1 else None)
186 if len(l1) >= 1 else None)
180 new_rev = l2[1] if len(l2) == 2 else 'new'
187 new_rev = l2[1] if len(l2) == 2 else 'new'
181
188
182 filename = (old_filename
189 filename = (old_filename
183 if old_filename != '/dev/null' else new_filename)
190 if old_filename != '/dev/null' else new_filename)
184
191
185 return filename, new_rev, old_rev
192 return filename, new_rev, old_rev
186 except (ValueError, IndexError):
193 except (ValueError, IndexError):
187 pass
194 pass
188
195
189 return None, None, None
196 return None, None, None
190
197
191 def _parse_gitdiff(self, diffiterator):
198 def _parse_gitdiff(self, diffiterator):
192 def line_decoder(l):
199 def line_decoder(l):
193 if l.startswith('+') and not l.startswith('+++'):
200 if l.startswith('+') and not l.startswith('+++'):
194 self.adds += 1
201 self.adds += 1
195 elif l.startswith('-') and not l.startswith('---'):
202 elif l.startswith('-') and not l.startswith('---'):
196 self.removes += 1
203 self.removes += 1
197 return l.decode('utf8', 'replace')
204 return l.decode('utf8', 'replace')
198
205
199 output = list(diffiterator)
206 output = list(diffiterator)
200 size = len(output)
207 size = len(output)
201
208
202 if size == 2:
209 if size == 2:
203 l = []
210 l = []
204 l.extend([output[0]])
211 l.extend([output[0]])
205 l.extend(output[1].splitlines(1))
212 l.extend(output[1].splitlines(1))
206 return map(line_decoder, l)
213 return map(line_decoder, l)
207 elif size == 1:
214 elif size == 1:
208 return map(line_decoder, output[0].splitlines(1))
215 return map(line_decoder, output[0].splitlines(1))
209 elif size == 0:
216 elif size == 0:
210 return []
217 return []
211
218
212 raise Exception('wrong size of diff %s' % size)
219 raise Exception('wrong size of diff %s' % size)
213
220
214 def _highlight_line_difflib(self, line, next_):
221 def _highlight_line_difflib(self, line, next_):
215 """
222 """
216 Highlight inline changes in both lines.
223 Highlight inline changes in both lines.
217 """
224 """
218
225
219 if line['action'] == 'del':
226 if line['action'] == 'del':
220 old, new = line, next_
227 old, new = line, next_
221 else:
228 else:
222 old, new = next_, line
229 old, new = next_, line
223
230
224 oldwords = re.split(r'(\W)', old['line'])
231 oldwords = re.split(r'(\W)', old['line'])
225 newwords = re.split(r'(\W)', new['line'])
232 newwords = re.split(r'(\W)', new['line'])
226
233
227 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
234 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
228
235
229 oldfragments, newfragments = [], []
236 oldfragments, newfragments = [], []
230 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
237 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
231 oldfrag = ''.join(oldwords[i1:i2])
238 oldfrag = ''.join(oldwords[i1:i2])
232 newfrag = ''.join(newwords[j1:j2])
239 newfrag = ''.join(newwords[j1:j2])
233 if tag != 'equal':
240 if tag != 'equal':
234 if oldfrag:
241 if oldfrag:
235 oldfrag = '<del>%s</del>' % oldfrag
242 oldfrag = '<del>%s</del>' % oldfrag
236 if newfrag:
243 if newfrag:
237 newfrag = '<ins>%s</ins>' % newfrag
244 newfrag = '<ins>%s</ins>' % newfrag
238 oldfragments.append(oldfrag)
245 oldfragments.append(oldfrag)
239 newfragments.append(newfrag)
246 newfragments.append(newfrag)
240
247
241 old['line'] = "".join(oldfragments)
248 old['line'] = "".join(oldfragments)
242 new['line'] = "".join(newfragments)
249 new['line'] = "".join(newfragments)
243
250
244 def _highlight_line_udiff(self, line, next_):
251 def _highlight_line_udiff(self, line, next_):
245 """
252 """
246 Highlight inline changes in both lines.
253 Highlight inline changes in both lines.
247 """
254 """
248 start = 0
255 start = 0
249 limit = min(len(line['line']), len(next_['line']))
256 limit = min(len(line['line']), len(next_['line']))
250 while start < limit and line['line'][start] == next_['line'][start]:
257 while start < limit and line['line'][start] == next_['line'][start]:
251 start += 1
258 start += 1
252 end = -1
259 end = -1
253 limit -= start
260 limit -= start
254 while -end <= limit and line['line'][end] == next_['line'][end]:
261 while -end <= limit and line['line'][end] == next_['line'][end]:
255 end -= 1
262 end -= 1
256 end += 1
263 end += 1
257 if start or end:
264 if start or end:
258 def do(l):
265 def do(l):
259 last = end + len(l['line'])
266 last = end + len(l['line'])
260 if l['action'] == 'add':
267 if l['action'] == 'add':
261 tag = 'ins'
268 tag = 'ins'
262 else:
269 else:
263 tag = 'del'
270 tag = 'del'
264 l['line'] = '%s<%s>%s</%s>%s' % (
271 l['line'] = '%s<%s>%s</%s>%s' % (
265 l['line'][:start],
272 l['line'][:start],
266 tag,
273 tag,
267 l['line'][start:last],
274 l['line'][start:last],
268 tag,
275 tag,
269 l['line'][last:]
276 l['line'][last:]
270 )
277 )
271 do(line)
278 do(line)
272 do(next_)
279 do(next_)
273
280
274 def _parse_udiff(self):
281 def _parse_udiff(self):
275 """
282 """
276 Parse the diff an return data for the template.
283 Parse the diff an return data for the template.
277 """
284 """
278 lineiter = self.lines
285 lineiter = self.lines
279 files = []
286 files = []
280 try:
287 try:
281 line = lineiter.next()
288 line = lineiter.next()
282 # skip first context
289 # skip first context
283 skipfirst = True
290 skipfirst = True
284 while 1:
291 while 1:
285 # continue until we found the old file
292 # continue until we found the old file
286 if not line.startswith('--- '):
293 if not line.startswith('--- '):
287 line = lineiter.next()
294 line = lineiter.next()
288 continue
295 continue
289
296
290 chunks = []
297 chunks = []
291 filename, old_rev, new_rev = \
298 filename, old_rev, new_rev = \
292 self._extract_rev(line, lineiter.next())
299 self._extract_rev(line, lineiter.next())
293 files.append({
300 files.append({
294 'filename': filename,
301 'filename': filename,
295 'old_revision': old_rev,
302 'old_revision': old_rev,
296 'new_revision': new_rev,
303 'new_revision': new_rev,
297 'chunks': chunks
304 'chunks': chunks
298 })
305 })
299
306
300 line = lineiter.next()
307 line = lineiter.next()
301 while line:
308 while line:
302 match = self._chunk_re.match(line)
309 match = self._chunk_re.match(line)
303 if not match:
310 if not match:
304 break
311 break
305
312
306 lines = []
313 lines = []
307 chunks.append(lines)
314 chunks.append(lines)
308
315
309 old_line, old_end, new_line, new_end = \
316 old_line, old_end, new_line, new_end = \
310 [int(x or 1) for x in match.groups()[:-1]]
317 [int(x or 1) for x in match.groups()[:-1]]
311 old_line -= 1
318 old_line -= 1
312 new_line -= 1
319 new_line -= 1
313 context = len(match.groups()) == 5
320 context = len(match.groups()) == 5
314 old_end += old_line
321 old_end += old_line
315 new_end += new_line
322 new_end += new_line
316
323
317 if context:
324 if context:
318 if not skipfirst:
325 if not skipfirst:
319 lines.append({
326 lines.append({
320 'old_lineno': '...',
327 'old_lineno': '...',
321 'new_lineno': '...',
328 'new_lineno': '...',
322 'action': 'context',
329 'action': 'context',
323 'line': line,
330 'line': line,
324 })
331 })
325 else:
332 else:
326 skipfirst = False
333 skipfirst = False
327
334
328 line = lineiter.next()
335 line = lineiter.next()
329 while old_line < old_end or new_line < new_end:
336 while old_line < old_end or new_line < new_end:
330 if line:
337 if line:
331 command, line = line[0], line[1:]
338 command, line = line[0], line[1:]
332 else:
339 else:
333 command = ' '
340 command = ' '
334 affects_old = affects_new = False
341 affects_old = affects_new = False
335
342
336 # ignore those if we don't expect them
343 # ignore those if we don't expect them
337 if command in '#@':
344 if command in '#@':
338 continue
345 continue
339 elif command == '+':
346 elif command == '+':
340 affects_new = True
347 affects_new = True
341 action = 'add'
348 action = 'add'
342 elif command == '-':
349 elif command == '-':
343 affects_old = True
350 affects_old = True
344 action = 'del'
351 action = 'del'
345 else:
352 else:
346 affects_old = affects_new = True
353 affects_old = affects_new = True
347 action = 'unmod'
354 action = 'unmod'
348
355
349 old_line += affects_old
356 old_line += affects_old
350 new_line += affects_new
357 new_line += affects_new
351 lines.append({
358 lines.append({
352 'old_lineno': affects_old and old_line or '',
359 'old_lineno': affects_old and old_line or '',
353 'new_lineno': affects_new and new_line or '',
360 'new_lineno': affects_new and new_line or '',
354 'action': action,
361 'action': action,
355 'line': line
362 'line': line
356 })
363 })
357 line = lineiter.next()
364 line = lineiter.next()
358
365
359 except StopIteration:
366 except StopIteration:
360 pass
367 pass
361
368
362 # highlight inline changes
369 # highlight inline changes
363 for _ in files:
370 for _ in files:
364 for chunk in chunks:
371 for chunk in chunks:
365 lineiter = iter(chunk)
372 lineiter = iter(chunk)
366 #first = True
373 #first = True
367 try:
374 try:
368 while 1:
375 while 1:
369 line = lineiter.next()
376 line = lineiter.next()
370 if line['action'] != 'unmod':
377 if line['action'] != 'unmod':
371 nextline = lineiter.next()
378 nextline = lineiter.next()
372 if nextline['action'] == 'unmod' or \
379 if nextline['action'] == 'unmod' or \
373 nextline['action'] == line['action']:
380 nextline['action'] == line['action']:
374 continue
381 continue
375 self.differ(line, nextline)
382 self.differ(line, nextline)
376 except StopIteration:
383 except StopIteration:
377 pass
384 pass
378
385
379 return files
386 return files
380
387
381 def prepare(self):
388 def prepare(self):
382 """
389 """
383 Prepare the passed udiff for HTML rendering. It'l return a list
390 Prepare the passed udiff for HTML rendering. It'l return a list
384 of dicts
391 of dicts
385 """
392 """
386 return self._parse_udiff()
393 return self._parse_udiff()
387
394
388 def _safe_id(self, idstring):
395 def _safe_id(self, idstring):
389 """Make a string safe for including in an id attribute.
396 """Make a string safe for including in an id attribute.
390
397
391 The HTML spec says that id attributes 'must begin with
398 The HTML spec says that id attributes 'must begin with
392 a letter ([A-Za-z]) and may be followed by any number
399 a letter ([A-Za-z]) and may be followed by any number
393 of letters, digits ([0-9]), hyphens ("-"), underscores
400 of letters, digits ([0-9]), hyphens ("-"), underscores
394 ("_"), colons (":"), and periods (".")'. These regexps
401 ("_"), colons (":"), and periods (".")'. These regexps
395 are slightly over-zealous, in that they remove colons
402 are slightly over-zealous, in that they remove colons
396 and periods unnecessarily.
403 and periods unnecessarily.
397
404
398 Whitespace is transformed into underscores, and then
405 Whitespace is transformed into underscores, and then
399 anything which is not a hyphen or a character that
406 anything which is not a hyphen or a character that
400 matches \w (alphanumerics and underscore) is removed.
407 matches \w (alphanumerics and underscore) is removed.
401
408
402 """
409 """
403 # Transform all whitespace to underscore
410 # Transform all whitespace to underscore
404 idstring = re.sub(r'\s', "_", '%s' % idstring)
411 idstring = re.sub(r'\s', "_", '%s' % idstring)
405 # Remove everything that is not a hyphen or a member of \w
412 # Remove everything that is not a hyphen or a member of \w
406 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
413 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
407 return idstring
414 return idstring
408
415
409 def raw_diff(self):
416 def raw_diff(self):
410 """
417 """
411 Returns raw string as udiff
418 Returns raw string as udiff
412 """
419 """
413 udiff_copy = self.copy_iterator()
420 udiff_copy = self.copy_iterator()
414 if self.__format == 'gitdiff':
421 if self.__format == 'gitdiff':
415 udiff_copy = self._parse_gitdiff(udiff_copy)
422 udiff_copy = self._parse_gitdiff(udiff_copy)
416 return u''.join(udiff_copy)
423 return u''.join(udiff_copy)
417
424
418 def as_html(self, table_class='code-difftable', line_class='line',
425 def as_html(self, table_class='code-difftable', line_class='line',
419 new_lineno_class='lineno old', old_lineno_class='lineno new',
426 new_lineno_class='lineno old', old_lineno_class='lineno new',
420 code_class='code', enable_comments=False):
427 code_class='code', enable_comments=False):
421 """
428 """
422 Return udiff as html table with customized css classes
429 Return udiff as html table with customized css classes
423 """
430 """
424 def _link_to_if(condition, label, url):
431 def _link_to_if(condition, label, url):
425 """
432 """
426 Generates a link if condition is meet or just the label if not.
433 Generates a link if condition is meet or just the label if not.
427 """
434 """
428
435
429 if condition:
436 if condition:
430 return '''<a href="%(url)s">%(label)s</a>''' % {
437 return '''<a href="%(url)s">%(label)s</a>''' % {
431 'url': url,
438 'url': url,
432 'label': label
439 'label': label
433 }
440 }
434 else:
441 else:
435 return label
442 return label
436 diff_lines = self.prepare()
443 diff_lines = self.prepare()
437 _html_empty = True
444 _html_empty = True
438 _html = []
445 _html = []
439 _html.append('''<table class="%(table_class)s">\n''' % {
446 _html.append('''<table class="%(table_class)s">\n''' % {
440 'table_class': table_class
447 'table_class': table_class
441 })
448 })
442 for diff in diff_lines:
449 for diff in diff_lines:
443 for line in diff['chunks']:
450 for line in diff['chunks']:
444 _html_empty = False
451 _html_empty = False
445 for change in line:
452 for change in line:
446 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
453 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
447 'lc': line_class,
454 'lc': line_class,
448 'action': change['action']
455 'action': change['action']
449 })
456 })
450 anchor_old_id = ''
457 anchor_old_id = ''
451 anchor_new_id = ''
458 anchor_new_id = ''
452 anchor_old = "%(filename)s_o%(oldline_no)s" % {
459 anchor_old = "%(filename)s_o%(oldline_no)s" % {
453 'filename': self._safe_id(diff['filename']),
460 'filename': self._safe_id(diff['filename']),
454 'oldline_no': change['old_lineno']
461 'oldline_no': change['old_lineno']
455 }
462 }
456 anchor_new = "%(filename)s_n%(oldline_no)s" % {
463 anchor_new = "%(filename)s_n%(oldline_no)s" % {
457 'filename': self._safe_id(diff['filename']),
464 'filename': self._safe_id(diff['filename']),
458 'oldline_no': change['new_lineno']
465 'oldline_no': change['new_lineno']
459 }
466 }
460 cond_old = (change['old_lineno'] != '...' and
467 cond_old = (change['old_lineno'] != '...' and
461 change['old_lineno'])
468 change['old_lineno'])
462 cond_new = (change['new_lineno'] != '...' and
469 cond_new = (change['new_lineno'] != '...' and
463 change['new_lineno'])
470 change['new_lineno'])
464 if cond_old:
471 if cond_old:
465 anchor_old_id = 'id="%s"' % anchor_old
472 anchor_old_id = 'id="%s"' % anchor_old
466 if cond_new:
473 if cond_new:
467 anchor_new_id = 'id="%s"' % anchor_new
474 anchor_new_id = 'id="%s"' % anchor_new
468 ###########################################################
475 ###########################################################
469 # OLD LINE NUMBER
476 # OLD LINE NUMBER
470 ###########################################################
477 ###########################################################
471 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
478 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
472 'a_id': anchor_old_id,
479 'a_id': anchor_old_id,
473 'olc': old_lineno_class
480 'olc': old_lineno_class
474 })
481 })
475
482
476 _html.append('''%(link)s''' % {
483 _html.append('''%(link)s''' % {
477 'link': _link_to_if(True, change['old_lineno'],
484 'link': _link_to_if(True, change['old_lineno'],
478 '#%s' % anchor_old)
485 '#%s' % anchor_old)
479 })
486 })
480 _html.append('''</td>\n''')
487 _html.append('''</td>\n''')
481 ###########################################################
488 ###########################################################
482 # NEW LINE NUMBER
489 # NEW LINE NUMBER
483 ###########################################################
490 ###########################################################
484
491
485 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
492 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
486 'a_id': anchor_new_id,
493 'a_id': anchor_new_id,
487 'nlc': new_lineno_class
494 'nlc': new_lineno_class
488 })
495 })
489
496
490 _html.append('''%(link)s''' % {
497 _html.append('''%(link)s''' % {
491 'link': _link_to_if(True, change['new_lineno'],
498 'link': _link_to_if(True, change['new_lineno'],
492 '#%s' % anchor_new)
499 '#%s' % anchor_new)
493 })
500 })
494 _html.append('''</td>\n''')
501 _html.append('''</td>\n''')
495 ###########################################################
502 ###########################################################
496 # CODE
503 # CODE
497 ###########################################################
504 ###########################################################
498 comments = '' if enable_comments else 'no-comment'
505 comments = '' if enable_comments else 'no-comment'
499 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
506 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
500 'cc': code_class,
507 'cc': code_class,
501 'inc': comments
508 'inc': comments
502 })
509 })
503 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
510 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
504 'code': change['line']
511 'code': change['line']
505 })
512 })
506 _html.append('''\t</td>''')
513 _html.append('''\t</td>''')
507 _html.append('''\n</tr>\n''')
514 _html.append('''\n</tr>\n''')
508 _html.append('''</table>''')
515 _html.append('''</table>''')
509 if _html_empty:
516 if _html_empty:
510 return None
517 return None
511 return ''.join(_html)
518 return ''.join(_html)
512
519
513 def stat(self):
520 def stat(self):
514 """
521 """
515 Returns tuple of added, and removed lines for this instance
522 Returns tuple of added, and removed lines for this instance
516 """
523 """
517 return self.adds, self.removes
524 return self.adds, self.removes
@@ -1,454 +1,460 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 try:
40 try:
41 self.message = safe_unicode(commit.message[:-1])
41 self.message = safe_unicode(commit.message[:-1])
42 # Always strip last eol
42 # Always strip last eol
43 except UnicodeDecodeError:
43 except UnicodeDecodeError:
44 self.message = commit.message[:-1].decode(commit.encoding
44 self.message = commit.message[:-1].decode(commit.encoding
45 or 'utf-8')
45 or 'utf-8')
46 #self.branch = None
46 #self.branch = None
47 self.tags = []
47 self.tags = []
48 #tree = self.repository.get_object(self._tree_id)
48 #tree = self.repository.get_object(self._tree_id)
49 self.nodes = {}
49 self.nodes = {}
50 self._paths = {}
50 self._paths = {}
51
51
52 @LazyProperty
52 @LazyProperty
53 def author(self):
53 def author(self):
54 return safe_unicode(self._commit.committer)
54 return safe_unicode(self._commit.committer)
55
55
56 @LazyProperty
56 @LazyProperty
57 def date(self):
57 def date(self):
58 return date_fromtimestamp(self._commit.commit_time,
58 return date_fromtimestamp(self._commit.commit_time,
59 self._commit.commit_timezone)
59 self._commit.commit_timezone)
60
60
61 @LazyProperty
61 @LazyProperty
62 def status(self):
62 def status(self):
63 """
63 """
64 Returns modified, added, removed, deleted files for current changeset
64 Returns modified, added, removed, deleted files for current changeset
65 """
65 """
66 return self.changed, self.added, self.removed
66 return self.changed, self.added, self.removed
67
67
68 @LazyProperty
68 @LazyProperty
69 def branch(self):
69 def branch(self):
70
70
71 heads = self.repository._heads(reverse=False)
71 heads = self.repository._heads(reverse=False)
72
72
73 ref = heads.get(self.raw_id)
73 ref = heads.get(self.raw_id)
74 if ref:
74 if ref:
75 return safe_unicode(ref)
75 return safe_unicode(ref)
76
76
77 def _fix_path(self, path):
77 def _fix_path(self, path):
78 """
78 """
79 Paths are stored without trailing slash so we need to get rid off it if
79 Paths are stored without trailing slash so we need to get rid off it if
80 needed.
80 needed.
81 """
81 """
82 if path.endswith('/'):
82 if path.endswith('/'):
83 path = path.rstrip('/')
83 path = path.rstrip('/')
84 return path
84 return path
85
85
86 def _get_id_for_path(self, path):
86 def _get_id_for_path(self, path):
87
87
88 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
88 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
89 if not path in self._paths:
89 if not path in self._paths:
90 path = path.strip('/')
90 path = path.strip('/')
91 # set root tree
91 # set root tree
92 tree = self.repository._repo[self._commit.tree]
92 tree = self.repository._repo[self._commit.tree]
93 if path == '':
93 if path == '':
94 self._paths[''] = tree.id
94 self._paths[''] = tree.id
95 return tree.id
95 return tree.id
96 splitted = path.split('/')
96 splitted = path.split('/')
97 dirs, name = splitted[:-1], splitted[-1]
97 dirs, name = splitted[:-1], splitted[-1]
98 curdir = ''
98 curdir = ''
99
99
100 # initially extract things from root dir
100 # initially extract things from root dir
101 for item, stat, id in tree.iteritems():
101 for item, stat, id in tree.iteritems():
102 if curdir:
102 if curdir:
103 name = '/'.join((curdir, item))
103 name = '/'.join((curdir, item))
104 else:
104 else:
105 name = item
105 name = item
106 self._paths[name] = id
106 self._paths[name] = id
107 self._stat_modes[name] = stat
107 self._stat_modes[name] = stat
108
108
109 for dir in dirs:
109 for dir in dirs:
110 if curdir:
110 if curdir:
111 curdir = '/'.join((curdir, dir))
111 curdir = '/'.join((curdir, dir))
112 else:
112 else:
113 curdir = dir
113 curdir = dir
114 dir_id = None
114 dir_id = None
115 for item, stat, id in tree.iteritems():
115 for item, stat, id in tree.iteritems():
116 if dir == item:
116 if dir == item:
117 dir_id = id
117 dir_id = id
118 if dir_id:
118 if dir_id:
119 # Update tree
119 # Update tree
120 tree = self.repository._repo[dir_id]
120 tree = self.repository._repo[dir_id]
121 if not isinstance(tree, objects.Tree):
121 if not isinstance(tree, objects.Tree):
122 raise ChangesetError('%s is not a directory' % curdir)
122 raise ChangesetError('%s is not a directory' % curdir)
123 else:
123 else:
124 raise ChangesetError('%s have not been found' % curdir)
124 raise ChangesetError('%s have not been found' % curdir)
125
125
126 # cache all items from the given traversed tree
126 # cache all items from the given traversed tree
127 for item, stat, id in tree.iteritems():
127 for item, stat, id in tree.iteritems():
128 if curdir:
128 if curdir:
129 name = '/'.join((curdir, item))
129 name = '/'.join((curdir, item))
130 else:
130 else:
131 name = item
131 name = item
132 self._paths[name] = id
132 self._paths[name] = id
133 self._stat_modes[name] = stat
133 self._stat_modes[name] = stat
134 if not path in self._paths:
134 if not path in self._paths:
135 raise NodeDoesNotExistError("There is no file nor directory "
135 raise NodeDoesNotExistError("There is no file nor directory "
136 "at the given path %r at revision %r"
136 "at the given path %r at revision %r"
137 % (path, self.short_id))
137 % (path, self.short_id))
138 return self._paths[path]
138 return self._paths[path]
139
139
140 def _get_kind(self, path):
140 def _get_kind(self, path):
141 id = self._get_id_for_path(path)
141 id = self._get_id_for_path(path)
142 obj = self.repository._repo[id]
142 obj = self.repository._repo[id]
143 if isinstance(obj, objects.Blob):
143 if isinstance(obj, objects.Blob):
144 return NodeKind.FILE
144 return NodeKind.FILE
145 elif isinstance(obj, objects.Tree):
145 elif isinstance(obj, objects.Tree):
146 return NodeKind.DIR
146 return NodeKind.DIR
147
147
148 def _get_file_nodes(self):
148 def _get_file_nodes(self):
149 return chain(*(t[2] for t in self.walk()))
149 return chain(*(t[2] for t in self.walk()))
150
150
151 @LazyProperty
151 @LazyProperty
152 def parents(self):
152 def parents(self):
153 """
153 """
154 Returns list of parents changesets.
154 Returns list of parents changesets.
155 """
155 """
156 return [self.repository.get_changeset(parent)
156 return [self.repository.get_changeset(parent)
157 for parent in self._commit.parents]
157 for parent in self._commit.parents]
158
158
159 def next(self, branch=None):
159 def next(self, branch=None):
160
160
161 if branch and self.branch != branch:
161 if branch and self.branch != branch:
162 raise VCSError('Branch option used on changeset not belonging '
162 raise VCSError('Branch option used on changeset not belonging '
163 'to that branch')
163 'to that branch')
164
164
165 def _next(changeset, branch):
165 def _next(changeset, branch):
166 try:
166 try:
167 next_ = changeset.revision + 1
167 next_ = changeset.revision + 1
168 next_rev = changeset.repository.revisions[next_]
168 next_rev = changeset.repository.revisions[next_]
169 except IndexError:
169 except IndexError:
170 raise ChangesetDoesNotExistError
170 raise ChangesetDoesNotExistError
171 cs = changeset.repository.get_changeset(next_rev)
171 cs = changeset.repository.get_changeset(next_rev)
172
172
173 if branch and branch != cs.branch:
173 if branch and branch != cs.branch:
174 return _next(cs, branch)
174 return _next(cs, branch)
175
175
176 return cs
176 return cs
177
177
178 return _next(self, branch)
178 return _next(self, branch)
179
179
180 def prev(self, branch=None):
180 def prev(self, branch=None):
181 if branch and self.branch != branch:
181 if branch and self.branch != branch:
182 raise VCSError('Branch option used on changeset not belonging '
182 raise VCSError('Branch option used on changeset not belonging '
183 'to that branch')
183 'to that branch')
184
184
185 def _prev(changeset, branch):
185 def _prev(changeset, branch):
186 try:
186 try:
187 prev_ = changeset.revision - 1
187 prev_ = changeset.revision - 1
188 if prev_ < 0:
188 if prev_ < 0:
189 raise IndexError
189 raise IndexError
190 prev_rev = changeset.repository.revisions[prev_]
190 prev_rev = changeset.repository.revisions[prev_]
191 except IndexError:
191 except IndexError:
192 raise ChangesetDoesNotExistError
192 raise ChangesetDoesNotExistError
193
193
194 cs = changeset.repository.get_changeset(prev_rev)
194 cs = changeset.repository.get_changeset(prev_rev)
195
195
196 if branch and branch != cs.branch:
196 if branch and branch != cs.branch:
197 return _prev(cs, branch)
197 return _prev(cs, branch)
198
198
199 return cs
199 return cs
200
200
201 return _prev(self, branch)
201 return _prev(self, branch)
202
202
203 def get_file_mode(self, path):
203 def get_file_mode(self, path):
204 """
204 """
205 Returns stat mode of the file at the given ``path``.
205 Returns stat mode of the file at the given ``path``.
206 """
206 """
207 # ensure path is traversed
207 # ensure path is traversed
208 self._get_id_for_path(path)
208 self._get_id_for_path(path)
209 return self._stat_modes[path]
209 return self._stat_modes[path]
210
210
211 def get_file_content(self, path):
211 def get_file_content(self, path):
212 """
212 """
213 Returns content of the file at given ``path``.
213 Returns content of the file at given ``path``.
214 """
214 """
215 id = self._get_id_for_path(path)
215 id = self._get_id_for_path(path)
216 blob = self.repository._repo[id]
216 blob = self.repository._repo[id]
217 return blob.as_pretty_string()
217 return blob.as_pretty_string()
218
218
219 def get_file_size(self, path):
219 def get_file_size(self, path):
220 """
220 """
221 Returns size of the file at given ``path``.
221 Returns size of the file at given ``path``.
222 """
222 """
223 id = self._get_id_for_path(path)
223 id = self._get_id_for_path(path)
224 blob = self.repository._repo[id]
224 blob = self.repository._repo[id]
225 return blob.raw_length()
225 return blob.raw_length()
226
226
227 def get_file_changeset(self, path):
227 def get_file_changeset(self, path):
228 """
228 """
229 Returns last commit of the file at the given ``path``.
229 Returns last commit of the file at the given ``path``.
230 """
230 """
231 node = self.get_node(path)
231 node = self.get_node(path)
232 return node.history[0]
232 return node.history[0]
233
233
234 def get_file_history(self, path):
234 def get_file_history(self, path):
235 """
235 """
236 Returns history of file as reversed list of ``Changeset`` objects for
236 Returns history of file as reversed list of ``Changeset`` objects for
237 which file at given ``path`` has been modified.
237 which file at given ``path`` has been modified.
238
238
239 TODO: This function now uses os underlying 'git' and 'grep' commands
239 TODO: This function now uses os underlying 'git' and 'grep' commands
240 which is generally not good. Should be replaced with algorithm
240 which is generally not good. Should be replaced with algorithm
241 iterating commits.
241 iterating commits.
242 """
242 """
243 cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
243 cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
244 self.id, path
244 self.id, path
245 )
245 )
246 so, se = self.repository.run_git_command(cmd)
246 so, se = self.repository.run_git_command(cmd)
247 ids = re.findall(r'\w{40}', so)
247 ids = re.findall(r'\w{40}', so)
248 return [self.repository.get_changeset(id) for id in ids]
248 return [self.repository.get_changeset(id) for id in ids]
249
249
250 def get_file_annotate(self, path):
250 def get_file_annotate(self, path):
251 """
251 """
252 Returns a list of three element tuples with lineno,changeset and line
252 Returns a list of three element tuples with lineno,changeset and line
253
253
254 TODO: This function now uses os underlying 'git' command which is
254 TODO: This function now uses os underlying 'git' command which is
255 generally not good. Should be replaced with algorithm iterating
255 generally not good. Should be replaced with algorithm iterating
256 commits.
256 commits.
257 """
257 """
258 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
258 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
259 # -l ==> outputs long shas (and we need all 40 characters)
259 # -l ==> outputs long shas (and we need all 40 characters)
260 # --root ==> doesn't put '^' character for bounderies
260 # --root ==> doesn't put '^' character for bounderies
261 # -r sha ==> blames for the given revision
261 # -r sha ==> blames for the given revision
262 so, se = self.repository.run_git_command(cmd)
262 so, se = self.repository.run_git_command(cmd)
263 annotate = []
263 annotate = []
264 for i, blame_line in enumerate(so.split('\n')[:-1]):
264 for i, blame_line in enumerate(so.split('\n')[:-1]):
265 ln_no = i + 1
265 ln_no = i + 1
266 id, line = re.split(r' \(.+?\) ', blame_line, 1)
266 id, line = re.split(r' \(.+?\) ', blame_line, 1)
267 annotate.append((ln_no, self.repository.get_changeset(id), line))
267 annotate.append((ln_no, self.repository.get_changeset(id), line))
268 return annotate
268 return annotate
269
269
270 def fill_archive(self, stream=None, kind='tgz', prefix=None,
270 def fill_archive(self, stream=None, kind='tgz', prefix=None,
271 subrepos=False):
271 subrepos=False):
272 """
272 """
273 Fills up given stream.
273 Fills up given stream.
274
274
275 :param stream: file like object.
275 :param stream: file like object.
276 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
276 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
277 Default: ``tgz``.
277 Default: ``tgz``.
278 :param prefix: name of root directory in archive.
278 :param prefix: name of root directory in archive.
279 Default is repository name and changeset's raw_id joined with dash
279 Default is repository name and changeset's raw_id joined with dash
280 (``repo-tip.<KIND>``).
280 (``repo-tip.<KIND>``).
281 :param subrepos: include subrepos in this archive.
281 :param subrepos: include subrepos in this archive.
282
282
283 :raise ImproperArchiveTypeError: If given kind is wrong.
283 :raise ImproperArchiveTypeError: If given kind is wrong.
284 :raise VcsError: If given stream is None
284 :raise VcsError: If given stream is None
285
285
286 """
286 """
287 allowed_kinds = settings.ARCHIVE_SPECS.keys()
287 allowed_kinds = settings.ARCHIVE_SPECS.keys()
288 if kind not in allowed_kinds:
288 if kind not in allowed_kinds:
289 raise ImproperArchiveTypeError('Archive kind not supported use one'
289 raise ImproperArchiveTypeError('Archive kind not supported use one'
290 'of %s', allowed_kinds)
290 'of %s', allowed_kinds)
291
291
292 if prefix is None:
292 if prefix is None:
293 prefix = '%s-%s' % (self.repository.name, self.short_id)
293 prefix = '%s-%s' % (self.repository.name, self.short_id)
294 elif prefix.startswith('/'):
294 elif prefix.startswith('/'):
295 raise VCSError("Prefix cannot start with leading slash")
295 raise VCSError("Prefix cannot start with leading slash")
296 elif prefix.strip() == '':
296 elif prefix.strip() == '':
297 raise VCSError("Prefix cannot be empty")
297 raise VCSError("Prefix cannot be empty")
298
298
299 if kind == 'zip':
299 if kind == 'zip':
300 frmt = 'zip'
300 frmt = 'zip'
301 else:
301 else:
302 frmt = 'tar'
302 frmt = 'tar'
303 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
303 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
304 self.raw_id)
304 self.raw_id)
305 if kind == 'tgz':
305 if kind == 'tgz':
306 cmd += ' | gzip -9'
306 cmd += ' | gzip -9'
307 elif kind == 'tbz2':
307 elif kind == 'tbz2':
308 cmd += ' | bzip2 -9'
308 cmd += ' | bzip2 -9'
309
309
310 if stream is None:
310 if stream is None:
311 raise VCSError('You need to pass in a valid stream for filling'
311 raise VCSError('You need to pass in a valid stream for filling'
312 ' with archival data')
312 ' with archival data')
313 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
313 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
314 cwd=self.repository.path)
314 cwd=self.repository.path)
315
315
316 buffer_size = 1024 * 8
316 buffer_size = 1024 * 8
317 chunk = popen.stdout.read(buffer_size)
317 chunk = popen.stdout.read(buffer_size)
318 while chunk:
318 while chunk:
319 stream.write(chunk)
319 stream.write(chunk)
320 chunk = popen.stdout.read(buffer_size)
320 chunk = popen.stdout.read(buffer_size)
321 # Make sure all descriptors would be read
321 # Make sure all descriptors would be read
322 popen.communicate()
322 popen.communicate()
323
323
324 def get_nodes(self, path):
324 def get_nodes(self, path):
325 if self._get_kind(path) != NodeKind.DIR:
325 if self._get_kind(path) != NodeKind.DIR:
326 raise ChangesetError("Directory does not exist for revision %r at "
326 raise ChangesetError("Directory does not exist for revision %r at "
327 " %r" % (self.revision, path))
327 " %r" % (self.revision, path))
328 path = self._fix_path(path)
328 path = self._fix_path(path)
329 id = self._get_id_for_path(path)
329 id = self._get_id_for_path(path)
330 tree = self.repository._repo[id]
330 tree = self.repository._repo[id]
331 dirnodes = []
331 dirnodes = []
332 filenodes = []
332 filenodes = []
333 als = self.repository.alias
333 als = self.repository.alias
334 for name, stat, id in tree.iteritems():
334 for name, stat, id in tree.iteritems():
335 if objects.S_ISGITLINK(stat):
335 if objects.S_ISGITLINK(stat):
336 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
336 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
337 alias=als))
337 alias=als))
338 continue
338 continue
339
339
340 obj = self.repository._repo.get_object(id)
340 obj = self.repository._repo.get_object(id)
341 if path != '':
341 if path != '':
342 obj_path = '/'.join((path, name))
342 obj_path = '/'.join((path, name))
343 else:
343 else:
344 obj_path = name
344 obj_path = name
345 if obj_path not in self._stat_modes:
345 if obj_path not in self._stat_modes:
346 self._stat_modes[obj_path] = stat
346 self._stat_modes[obj_path] = stat
347 if isinstance(obj, objects.Tree):
347 if isinstance(obj, objects.Tree):
348 dirnodes.append(DirNode(obj_path, changeset=self))
348 dirnodes.append(DirNode(obj_path, changeset=self))
349 elif isinstance(obj, objects.Blob):
349 elif isinstance(obj, objects.Blob):
350 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
350 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
351 else:
351 else:
352 raise ChangesetError("Requested object should be Tree "
352 raise ChangesetError("Requested object should be Tree "
353 "or Blob, is %r" % type(obj))
353 "or Blob, is %r" % type(obj))
354 nodes = dirnodes + filenodes
354 nodes = dirnodes + filenodes
355 for node in nodes:
355 for node in nodes:
356 if not node.path in self.nodes:
356 if not node.path in self.nodes:
357 self.nodes[node.path] = node
357 self.nodes[node.path] = node
358 nodes.sort()
358 nodes.sort()
359 return nodes
359 return nodes
360
360
361 def get_node(self, path):
361 def get_node(self, path):
362 if isinstance(path, unicode):
362 if isinstance(path, unicode):
363 path = path.encode('utf-8')
363 path = path.encode('utf-8')
364 path = self._fix_path(path)
364 path = self._fix_path(path)
365 if not path in self.nodes:
365 if not path in self.nodes:
366 try:
366 try:
367 id = self._get_id_for_path(path)
367 id_ = self._get_id_for_path(path)
368 except ChangesetError:
368 except ChangesetError:
369 raise NodeDoesNotExistError("Cannot find one of parents' "
369 raise NodeDoesNotExistError("Cannot find one of parents' "
370 "directories for a given path: %s" % path)
370 "directories for a given path: %s" % path)
371 obj = self.repository._repo.get_object(id)
371
372 if isinstance(obj, objects.Tree):
372 als = self.repository.alias
373 if path == '':
373 _GL = lambda m: m and objects.S_ISGITLINK(m)
374 node = RootNode(changeset=self)
374 if _GL(self._stat_modes.get(path)):
375 node = SubModuleNode(path, url=None, changeset=id_, alias=als)
376 else:
377 obj = self.repository._repo.get_object(id_)
378
379 if isinstance(obj, objects.Tree):
380 if path == '':
381 node = RootNode(changeset=self)
382 else:
383 node = DirNode(path, changeset=self)
384 node._tree = obj
385 elif isinstance(obj, objects.Blob):
386 node = FileNode(path, changeset=self)
387 node._blob = obj
375 else:
388 else:
376 node = DirNode(path, changeset=self)
389 raise NodeDoesNotExistError("There is no file nor directory "
377 node._tree = obj
390 "at the given path %r at revision %r"
378 elif isinstance(obj, objects.Blob):
391 % (path, self.short_id))
379 node = FileNode(path, changeset=self)
380 node._blob = obj
381 else:
382 raise NodeDoesNotExistError("There is no file nor directory "
383 "at the given path %r at revision %r"
384 % (path, self.short_id))
385 # cache node
392 # cache node
386 self.nodes[path] = node
393 self.nodes[path] = node
387 return self.nodes[path]
394 return self.nodes[path]
388
395
389 @LazyProperty
396 @LazyProperty
390 def affected_files(self):
397 def affected_files(self):
391 """
398 """
392 Get's a fast accessible file changes for given changeset
399 Get's a fast accessible file changes for given changeset
393 """
400 """
394
401
395 return self.added + self.changed
402 return self.added + self.changed
396
403
397 @LazyProperty
404 @LazyProperty
398 def _diff_name_status(self):
405 def _diff_name_status(self):
399 output = []
406 output = []
400 for parent in self.parents:
407 for parent in self.parents:
401 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
408 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
402 so, se = self.repository.run_git_command(cmd)
409 so, se = self.repository.run_git_command(cmd)
403 output.append(so.strip())
410 output.append(so.strip())
404 return '\n'.join(output)
411 return '\n'.join(output)
405
412
406 def _get_paths_for_status(self, status):
413 def _get_paths_for_status(self, status):
407 """
414 """
408 Returns sorted list of paths for given ``status``.
415 Returns sorted list of paths for given ``status``.
409
416
410 :param status: one of: *added*, *modified* or *deleted*
417 :param status: one of: *added*, *modified* or *deleted*
411 """
418 """
412 paths = set()
419 paths = set()
413 char = status[0].upper()
420 char = status[0].upper()
414 for line in self._diff_name_status.splitlines():
421 for line in self._diff_name_status.splitlines():
415 if not line:
422 if not line:
416 continue
423 continue
417
424
418 if line.startswith(char):
425 if line.startswith(char):
419 splitted = line.split(char, 1)
426 splitted = line.split(char, 1)
420 if not len(splitted) == 2:
427 if not len(splitted) == 2:
421 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
428 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
422 "particularly that line: %s" % (self._diff_name_status,
429 "particularly that line: %s" % (self._diff_name_status,
423 line))
430 line))
424 _path = splitted[1].strip()
431 _path = splitted[1].strip()
425 paths.add(_path)
432 paths.add(_path)
426
427 return sorted(paths)
433 return sorted(paths)
428
434
429 @LazyProperty
435 @LazyProperty
430 def added(self):
436 def added(self):
431 """
437 """
432 Returns list of added ``FileNode`` objects.
438 Returns list of added ``FileNode`` objects.
433 """
439 """
434 if not self.parents:
440 if not self.parents:
435 return list(self._get_file_nodes())
441 return list(self._get_file_nodes())
436 return [self.get_node(path) for path in self._get_paths_for_status('added')]
442 return [self.get_node(path) for path in self._get_paths_for_status('added')]
437
443
438 @LazyProperty
444 @LazyProperty
439 def changed(self):
445 def changed(self):
440 """
446 """
441 Returns list of modified ``FileNode`` objects.
447 Returns list of modified ``FileNode`` objects.
442 """
448 """
443 if not self.parents:
449 if not self.parents:
444 return []
450 return []
445 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
451 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
446
452
447 @LazyProperty
453 @LazyProperty
448 def removed(self):
454 def removed(self):
449 """
455 """
450 Returns list of removed ``FileNode`` objects.
456 Returns list of removed ``FileNode`` objects.
451 """
457 """
452 if not self.parents:
458 if not self.parents:
453 return []
459 return []
454 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
460 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -1,546 +1,546 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
69
70 @LazyProperty
70 @LazyProperty
71 def revisions(self):
71 def revisions(self):
72 """
72 """
73 Returns list of revisions' ids, in ascending order. Being lazy
73 Returns list of revisions' ids, in ascending order. Being lazy
74 attribute allows external tools to inject shas from cache.
74 attribute allows external tools to inject shas from cache.
75 """
75 """
76 return self._get_all_revisions()
76 return self._get_all_revisions()
77
77
78 def run_git_command(self, cmd):
78 def run_git_command(self, cmd):
79 """
79 """
80 Runs given ``cmd`` as git command and returns tuple
80 Runs given ``cmd`` as git command and returns tuple
81 (returncode, stdout, stderr).
81 (returncode, stdout, stderr).
82
82
83 .. note::
83 .. note::
84 This method exists only until log/blame functionality is implemented
84 This method exists only until log/blame functionality is implemented
85 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
85 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
86 os command's output is road to hell...
86 os command's output is road to hell...
87
87
88 :param cmd: git command to be executed
88 :param cmd: git command to be executed
89 """
89 """
90
90
91 _copts = ['-c', 'core.quotepath=false', ]
91 _copts = ['-c', 'core.quotepath=false', ]
92 _str_cmd = False
92 _str_cmd = False
93 if isinstance(cmd, basestring):
93 if isinstance(cmd, basestring):
94 cmd = [cmd]
94 cmd = [cmd]
95 _str_cmd = True
95 _str_cmd = True
96
96
97 cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + _copts + cmd
97 cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + _copts + cmd
98 if _str_cmd:
98 if _str_cmd:
99 cmd = ' '.join(cmd)
99 cmd = ' '.join(cmd)
100 try:
100 try:
101 opts = dict(
101 opts = dict(
102 shell=isinstance(cmd, basestring),
102 shell=isinstance(cmd, basestring),
103 stdout=PIPE,
103 stdout=PIPE,
104 stderr=PIPE)
104 stderr=PIPE)
105 if os.path.isdir(self.path):
105 if os.path.isdir(self.path):
106 opts['cwd'] = self.path
106 opts['cwd'] = self.path
107 p = Popen(cmd, **opts)
107 p = Popen(cmd, **opts)
108 except OSError, err:
108 except OSError, err:
109 raise RepositoryError("Couldn't run git command (%s).\n"
109 raise RepositoryError("Couldn't run git command (%s).\n"
110 "Original error was:%s" % (cmd, err))
110 "Original error was:%s" % (cmd, err))
111 so, se = p.communicate()
111 so, se = p.communicate()
112 if not se.startswith("fatal: bad default revision 'HEAD'") and \
112 if not se.startswith("fatal: bad default revision 'HEAD'") and \
113 p.returncode != 0:
113 p.returncode != 0:
114 raise RepositoryError("Couldn't run git command (%s).\n"
114 raise RepositoryError("Couldn't run git command (%s).\n"
115 "stderr:\n%s" % (cmd, se))
115 "stderr:\n%s" % (cmd, se))
116 return so, se
116 return so, se
117
117
118 def _check_url(self, url):
118 def _check_url(self, url):
119 """
119 """
120 Functon will check given url and try to verify if it's a valid
120 Functon will check given url and try to verify if it's a valid
121 link. Sometimes it may happened that mercurial will issue basic
121 link. Sometimes it may happened that mercurial will issue basic
122 auth request that can cause whole API to hang when used from python
122 auth request that can cause whole API to hang when used from python
123 or other external calls.
123 or other external calls.
124
124
125 On failures it'll raise urllib2.HTTPError
125 On failures it'll raise urllib2.HTTPError
126 """
126 """
127
127
128 #TODO: implement this
128 #TODO: implement this
129 pass
129 pass
130
130
131 def _get_repo(self, create, src_url=None, update_after_clone=False,
131 def _get_repo(self, create, src_url=None, update_after_clone=False,
132 bare=False):
132 bare=False):
133 if create and os.path.exists(self.path):
133 if create and os.path.exists(self.path):
134 raise RepositoryError("Location already exist")
134 raise RepositoryError("Location already exist")
135 if src_url and not create:
135 if src_url and not create:
136 raise RepositoryError("Create should be set to True if src_url is "
136 raise RepositoryError("Create should be set to True if src_url is "
137 "given (clone operation creates repository)")
137 "given (clone operation creates repository)")
138 try:
138 try:
139 if create and src_url:
139 if create and src_url:
140 self._check_url(src_url)
140 self._check_url(src_url)
141 self.clone(src_url, update_after_clone, bare)
141 self.clone(src_url, update_after_clone, bare)
142 return Repo(self.path)
142 return Repo(self.path)
143 elif create:
143 elif create:
144 os.mkdir(self.path)
144 os.mkdir(self.path)
145 if bare:
145 if bare:
146 return Repo.init_bare(self.path)
146 return Repo.init_bare(self.path)
147 else:
147 else:
148 return Repo.init(self.path)
148 return Repo.init(self.path)
149 else:
149 else:
150 return Repo(self.path)
150 return Repo(self.path)
151 except (NotGitRepository, OSError), err:
151 except (NotGitRepository, OSError), err:
152 raise RepositoryError(err)
152 raise RepositoryError(err)
153
153
154 def _get_all_revisions(self):
154 def _get_all_revisions(self):
155 cmd = 'rev-list --all --date-order'
155 cmd = 'rev-list --all --date-order'
156 try:
156 try:
157 so, se = self.run_git_command(cmd)
157 so, se = self.run_git_command(cmd)
158 except RepositoryError:
158 except RepositoryError:
159 # Can be raised for empty repositories
159 # Can be raised for empty repositories
160 return []
160 return []
161 revisions = so.splitlines()
161 revisions = so.splitlines()
162 revisions.reverse()
162 revisions.reverse()
163 return revisions
163 return revisions
164
164
165 def _get_revision(self, revision):
165 def _get_revision(self, revision):
166 """
166 """
167 For git backend we always return integer here. This way we ensure
167 For git backend we always return integer here. This way we ensure
168 that changset's revision attribute would become integer.
168 that changset's revision attribute would become integer.
169 """
169 """
170 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
170 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
171 is_bstr = lambda o: isinstance(o, (str, unicode))
171 is_bstr = lambda o: isinstance(o, (str, unicode))
172 is_null = lambda o: len(o) == revision.count('0')
172 is_null = lambda o: len(o) == revision.count('0')
173
173
174 if len(self.revisions) == 0:
174 if len(self.revisions) == 0:
175 raise EmptyRepositoryError("There are no changesets yet")
175 raise EmptyRepositoryError("There are no changesets yet")
176
176
177 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
177 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
178 revision = self.revisions[-1]
178 revision = self.revisions[-1]
179
179
180 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
180 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
181 or isinstance(revision, int) or is_null(revision)):
181 or isinstance(revision, int) or is_null(revision)):
182 try:
182 try:
183 revision = self.revisions[int(revision)]
183 revision = self.revisions[int(revision)]
184 except:
184 except:
185 raise ChangesetDoesNotExistError("Revision %r does not exist "
185 raise ChangesetDoesNotExistError("Revision %r does not exist "
186 "for this repository %s" % (revision, self))
186 "for this repository %s" % (revision, self))
187
187
188 elif is_bstr(revision):
188 elif is_bstr(revision):
189 if not pattern.match(revision) or revision not in self.revisions:
189 if not pattern.match(revision) or revision not in self.revisions:
190 raise ChangesetDoesNotExistError("Revision %r does not exist "
190 raise ChangesetDoesNotExistError("Revision %r does not exist "
191 "for this repository %s" % (revision, self))
191 "for this repository %s" % (revision, self))
192
192
193 # Ensure we return full id
193 # Ensure we return full id
194 if not pattern.match(str(revision)):
194 if not pattern.match(str(revision)):
195 raise ChangesetDoesNotExistError("Given revision %r not recognized"
195 raise ChangesetDoesNotExistError("Given revision %r not recognized"
196 % revision)
196 % revision)
197 return revision
197 return revision
198
198
199 def _get_archives(self, archive_name='tip'):
199 def _get_archives(self, archive_name='tip'):
200
200
201 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
201 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
202 yield {"type": i[0], "extension": i[1], "node": archive_name}
202 yield {"type": i[0], "extension": i[1], "node": archive_name}
203
203
204 def _get_url(self, url):
204 def _get_url(self, url):
205 """
205 """
206 Returns normalized url. If schema is not given, would fall to
206 Returns normalized url. If schema is not given, would fall to
207 filesystem (``file:///``) schema.
207 filesystem (``file:///``) schema.
208 """
208 """
209 url = str(url)
209 url = str(url)
210 if url != 'default' and not '://' in url:
210 if url != 'default' and not '://' in url:
211 url = ':///'.join(('file', url))
211 url = ':///'.join(('file', url))
212 return url
212 return url
213
213
214 @LazyProperty
214 @LazyProperty
215 def name(self):
215 def name(self):
216 return os.path.basename(self.path)
216 return os.path.basename(self.path)
217
217
218 @LazyProperty
218 @LazyProperty
219 def last_change(self):
219 def last_change(self):
220 """
220 """
221 Returns last change made on this repository as datetime object
221 Returns last change made on this repository as datetime object
222 """
222 """
223 return date_fromtimestamp(self._get_mtime(), makedate()[1])
223 return date_fromtimestamp(self._get_mtime(), makedate()[1])
224
224
225 def _get_mtime(self):
225 def _get_mtime(self):
226 try:
226 try:
227 return time.mktime(self.get_changeset().date.timetuple())
227 return time.mktime(self.get_changeset().date.timetuple())
228 except RepositoryError:
228 except RepositoryError:
229 # fallback to filesystem
229 # fallback to filesystem
230 in_path = os.path.join(self.path, '.git', "index")
230 in_path = os.path.join(self.path, '.git', "index")
231 he_path = os.path.join(self.path, '.git', "HEAD")
231 he_path = os.path.join(self.path, '.git', "HEAD")
232 if os.path.exists(in_path):
232 if os.path.exists(in_path):
233 return os.stat(in_path).st_mtime
233 return os.stat(in_path).st_mtime
234 else:
234 else:
235 return os.stat(he_path).st_mtime
235 return os.stat(he_path).st_mtime
236
236
237 @LazyProperty
237 @LazyProperty
238 def description(self):
238 def description(self):
239 undefined_description = u'unknown'
239 undefined_description = u'unknown'
240 description_path = os.path.join(self.path, '.git', 'description')
240 description_path = os.path.join(self.path, '.git', 'description')
241 if os.path.isfile(description_path):
241 if os.path.isfile(description_path):
242 return safe_unicode(open(description_path).read())
242 return safe_unicode(open(description_path).read())
243 else:
243 else:
244 return undefined_description
244 return undefined_description
245
245
246 @LazyProperty
246 @LazyProperty
247 def contact(self):
247 def contact(self):
248 undefined_contact = u'Unknown'
248 undefined_contact = u'Unknown'
249 return undefined_contact
249 return undefined_contact
250
250
251 @property
251 @property
252 def branches(self):
252 def branches(self):
253 if not self.revisions:
253 if not self.revisions:
254 return {}
254 return {}
255 refs = self._repo.refs.as_dict()
255 refs = self._repo.refs.as_dict()
256 sortkey = lambda ctx: ctx[0]
256 sortkey = lambda ctx: ctx[0]
257 _branches = [('/'.join(ref.split('/')[2:]), head)
257 _branches = [('/'.join(ref.split('/')[2:]), head)
258 for ref, head in refs.items()
258 for ref, head in refs.items()
259 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
259 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
260 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
260 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
261
261
262 def _heads(self, reverse=False):
262 def _heads(self, reverse=False):
263 refs = self._repo.get_refs()
263 refs = self._repo.get_refs()
264 heads = {}
264 heads = {}
265
265
266 for key, val in refs.items():
266 for key, val in refs.items():
267 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
267 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
268 if key.startswith(ref_key):
268 if key.startswith(ref_key):
269 n = key[len(ref_key):]
269 n = key[len(ref_key):]
270 if n not in ['HEAD']:
270 if n not in ['HEAD']:
271 heads[n] = val
271 heads[n] = val
272
272
273 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
273 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
274
274
275 def _get_tags(self):
275 def _get_tags(self):
276 if not self.revisions:
276 if not self.revisions:
277 return {}
277 return {}
278 sortkey = lambda ctx: ctx[0]
278 sortkey = lambda ctx: ctx[0]
279 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
279 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
280 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
280 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
281 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
281 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
282
282
283 @LazyProperty
283 @LazyProperty
284 def tags(self):
284 def tags(self):
285 return self._get_tags()
285 return self._get_tags()
286
286
287 def tag(self, name, user, revision=None, message=None, date=None,
287 def tag(self, name, user, revision=None, message=None, date=None,
288 **kwargs):
288 **kwargs):
289 """
289 """
290 Creates and returns a tag for the given ``revision``.
290 Creates and returns a tag for the given ``revision``.
291
291
292 :param name: name for new tag
292 :param name: name for new tag
293 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
293 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
294 :param revision: changeset id for which new tag would be created
294 :param revision: changeset id for which new tag would be created
295 :param message: message of the tag's commit
295 :param message: message of the tag's commit
296 :param date: date of tag's commit
296 :param date: date of tag's commit
297
297
298 :raises TagAlreadyExistError: if tag with same name already exists
298 :raises TagAlreadyExistError: if tag with same name already exists
299 """
299 """
300 if name in self.tags:
300 if name in self.tags:
301 raise TagAlreadyExistError("Tag %s already exists" % name)
301 raise TagAlreadyExistError("Tag %s already exists" % name)
302 changeset = self.get_changeset(revision)
302 changeset = self.get_changeset(revision)
303 message = message or "Added tag %s for commit %s" % (name,
303 message = message or "Added tag %s for commit %s" % (name,
304 changeset.raw_id)
304 changeset.raw_id)
305 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
305 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
306
306
307 self.tags = self._get_tags()
307 self.tags = self._get_tags()
308 return changeset
308 return changeset
309
309
310 def remove_tag(self, name, user, message=None, date=None):
310 def remove_tag(self, name, user, message=None, date=None):
311 """
311 """
312 Removes tag with the given ``name``.
312 Removes tag with the given ``name``.
313
313
314 :param name: name of the tag to be removed
314 :param name: name of the tag to be removed
315 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
315 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
316 :param message: message of the tag's removal commit
316 :param message: message of the tag's removal commit
317 :param date: date of tag's removal commit
317 :param date: date of tag's removal commit
318
318
319 :raises TagDoesNotExistError: if tag with given name does not exists
319 :raises TagDoesNotExistError: if tag with given name does not exists
320 """
320 """
321 if name not in self.tags:
321 if name not in self.tags:
322 raise TagDoesNotExistError("Tag %s does not exist" % name)
322 raise TagDoesNotExistError("Tag %s does not exist" % name)
323 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
323 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
324 try:
324 try:
325 os.remove(tagpath)
325 os.remove(tagpath)
326 self.tags = self._get_tags()
326 self.tags = self._get_tags()
327 except OSError, e:
327 except OSError, e:
328 raise RepositoryError(e.strerror)
328 raise RepositoryError(e.strerror)
329
329
330 def get_changeset(self, revision=None):
330 def get_changeset(self, revision=None):
331 """
331 """
332 Returns ``GitChangeset`` object representing commit from git repository
332 Returns ``GitChangeset`` object representing commit from git repository
333 at the given revision or head (most recent commit) if None given.
333 at the given revision or head (most recent commit) if None given.
334 """
334 """
335 if isinstance(revision, GitChangeset):
335 if isinstance(revision, GitChangeset):
336 return revision
336 return revision
337 revision = self._get_revision(revision)
337 revision = self._get_revision(revision)
338 changeset = GitChangeset(repository=self, revision=revision)
338 changeset = GitChangeset(repository=self, revision=revision)
339 return changeset
339 return changeset
340
340
341 def get_changesets(self, start=None, end=None, start_date=None,
341 def get_changesets(self, start=None, end=None, start_date=None,
342 end_date=None, branch_name=None, reverse=False):
342 end_date=None, branch_name=None, reverse=False):
343 """
343 """
344 Returns iterator of ``GitChangeset`` objects from start to end (both
344 Returns iterator of ``GitChangeset`` objects from start to end (both
345 are inclusive), in ascending date order (unless ``reverse`` is set).
345 are inclusive), in ascending date order (unless ``reverse`` is set).
346
346
347 :param start: changeset ID, as str; first returned changeset
347 :param start: changeset ID, as str; first returned changeset
348 :param end: changeset ID, as str; last returned changeset
348 :param end: changeset ID, as str; last returned changeset
349 :param start_date: if specified, changesets with commit date less than
349 :param start_date: if specified, changesets with commit date less than
350 ``start_date`` would be filtered out from returned set
350 ``start_date`` would be filtered out from returned set
351 :param end_date: if specified, changesets with commit date greater than
351 :param end_date: if specified, changesets with commit date greater than
352 ``end_date`` would be filtered out from returned set
352 ``end_date`` would be filtered out from returned set
353 :param branch_name: if specified, changesets not reachable from given
353 :param branch_name: if specified, changesets not reachable from given
354 branch would be filtered out from returned set
354 branch would be filtered out from returned set
355 :param reverse: if ``True``, returned generator would be reversed
355 :param reverse: if ``True``, returned generator would be reversed
356 (meaning that returned changesets would have descending date order)
356 (meaning that returned changesets would have descending date order)
357
357
358 :raise BranchDoesNotExistError: If given ``branch_name`` does not
358 :raise BranchDoesNotExistError: If given ``branch_name`` does not
359 exist.
359 exist.
360 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
360 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
361 ``end`` could not be found.
361 ``end`` could not be found.
362
362
363 """
363 """
364 if branch_name and branch_name not in self.branches:
364 if branch_name and branch_name not in self.branches:
365 raise BranchDoesNotExistError("Branch '%s' not found" \
365 raise BranchDoesNotExistError("Branch '%s' not found" \
366 % branch_name)
366 % branch_name)
367 # %H at format means (full) commit hash, initial hashes are retrieved
367 # %H at format means (full) commit hash, initial hashes are retrieved
368 # in ascending date order
368 # in ascending date order
369 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
369 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
370 cmd_params = {}
370 cmd_params = {}
371 if start_date:
371 if start_date:
372 cmd_template += ' --since "$since"'
372 cmd_template += ' --since "$since"'
373 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
373 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
374 if end_date:
374 if end_date:
375 cmd_template += ' --until "$until"'
375 cmd_template += ' --until "$until"'
376 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
376 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
377 if branch_name:
377 if branch_name:
378 cmd_template += ' $branch_name'
378 cmd_template += ' $branch_name'
379 cmd_params['branch_name'] = branch_name
379 cmd_params['branch_name'] = branch_name
380 else:
380 else:
381 cmd_template += ' --all'
381 cmd_template += ' --all'
382
382
383 cmd = Template(cmd_template).safe_substitute(**cmd_params)
383 cmd = Template(cmd_template).safe_substitute(**cmd_params)
384 revs = self.run_git_command(cmd)[0].splitlines()
384 revs = self.run_git_command(cmd)[0].splitlines()
385 start_pos = 0
385 start_pos = 0
386 end_pos = len(revs)
386 end_pos = len(revs)
387 if start:
387 if start:
388 _start = self._get_revision(start)
388 _start = self._get_revision(start)
389 try:
389 try:
390 start_pos = revs.index(_start)
390 start_pos = revs.index(_start)
391 except ValueError:
391 except ValueError:
392 pass
392 pass
393
393
394 if end is not None:
394 if end is not None:
395 _end = self._get_revision(end)
395 _end = self._get_revision(end)
396 try:
396 try:
397 end_pos = revs.index(_end)
397 end_pos = revs.index(_end)
398 except ValueError:
398 except ValueError:
399 pass
399 pass
400
400
401 if None not in [start, end] and start_pos > end_pos:
401 if None not in [start, end] and start_pos > end_pos:
402 raise RepositoryError('start cannot be after end')
402 raise RepositoryError('start cannot be after end')
403
403
404 if end_pos is not None:
404 if end_pos is not None:
405 end_pos += 1
405 end_pos += 1
406
406
407 revs = revs[start_pos:end_pos]
407 revs = revs[start_pos:end_pos]
408 if reverse:
408 if reverse:
409 revs = reversed(revs)
409 revs = reversed(revs)
410 for rev in revs:
410 for rev in revs:
411 yield self.get_changeset(rev)
411 yield self.get_changeset(rev)
412
412
413 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
413 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
414 context=3):
414 context=3):
415 """
415 """
416 Returns (git like) *diff*, as plain text. Shows changes introduced by
416 Returns (git like) *diff*, as plain text. Shows changes introduced by
417 ``rev2`` since ``rev1``.
417 ``rev2`` since ``rev1``.
418
418
419 :param rev1: Entry point from which diff is shown. Can be
419 :param rev1: Entry point from which diff is shown. Can be
420 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
420 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
421 the changes since empty state of the repository until ``rev2``
421 the changes since empty state of the repository until ``rev2``
422 :param rev2: Until which revision changes should be shown.
422 :param rev2: Until which revision changes should be shown.
423 :param ignore_whitespace: If set to ``True``, would not show whitespace
423 :param ignore_whitespace: If set to ``True``, would not show whitespace
424 changes. Defaults to ``False``.
424 changes. Defaults to ``False``.
425 :param context: How many lines before/after changed lines should be
425 :param context: How many lines before/after changed lines should be
426 shown. Defaults to ``3``.
426 shown. Defaults to ``3``.
427 """
427 """
428 flags = ['-U%s' % context]
428 flags = ['-U%s' % context]
429 if ignore_whitespace:
429 if ignore_whitespace:
430 flags.append('-w')
430 flags.append('-w')
431
431
432 if rev1 == self.EMPTY_CHANGESET:
432 if rev1 == self.EMPTY_CHANGESET:
433 rev2 = self.get_changeset(rev2).raw_id
433 rev2 = self.get_changeset(rev2).raw_id
434 cmd = ' '.join(['show'] + flags + [rev2])
434 cmd = ' '.join(['show'] + flags + [rev2])
435 else:
435 else:
436 rev1 = self.get_changeset(rev1).raw_id
436 rev1 = self.get_changeset(rev1).raw_id
437 rev2 = self.get_changeset(rev2).raw_id
437 rev2 = self.get_changeset(rev2).raw_id
438 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
438 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
439
439
440 if path:
440 if path:
441 cmd += ' -- "%s"' % path
441 cmd += ' -- "%s"' % path
442 stdout, stderr = self.run_git_command(cmd)
442 stdout, stderr = self.run_git_command(cmd)
443 # If we used 'show' command, strip first few lines (until actual diff
443 # If we used 'show' command, strip first few lines (until actual diff
444 # starts)
444 # starts)
445 if rev1 == self.EMPTY_CHANGESET:
445 if rev1 == self.EMPTY_CHANGESET:
446 lines = stdout.splitlines()
446 lines = stdout.splitlines()
447 x = 0
447 x = 0
448 for line in lines:
448 for line in lines:
449 if line.startswith('diff'):
449 if line.startswith('diff'):
450 break
450 break
451 x += 1
451 x += 1
452 # Append new line just like 'diff' command do
452 # Append new line just like 'diff' command do
453 stdout = '\n'.join(lines[x:]) + '\n'
453 stdout = '\n'.join(lines[x:]) + '\n'
454 return stdout
454 return stdout
455
455
456 @LazyProperty
456 @LazyProperty
457 def in_memory_changeset(self):
457 def in_memory_changeset(self):
458 """
458 """
459 Returns ``GitInMemoryChangeset`` object for this repository.
459 Returns ``GitInMemoryChangeset`` object for this repository.
460 """
460 """
461 return GitInMemoryChangeset(self)
461 return GitInMemoryChangeset(self)
462
462
463 def clone(self, url, update_after_clone=True, bare=False):
463 def clone(self, url, update_after_clone=True, bare=False):
464 """
464 """
465 Tries to clone changes from external location.
465 Tries to clone changes from external location.
466
466
467 :param update_after_clone: If set to ``False``, git won't checkout
467 :param update_after_clone: If set to ``False``, git won't checkout
468 working directory
468 working directory
469 :param bare: If set to ``True``, repository would be cloned into
469 :param bare: If set to ``True``, repository would be cloned into
470 *bare* git repository (no working directory at all).
470 *bare* git repository (no working directory at all).
471 """
471 """
472 url = self._get_url(url)
472 url = self._get_url(url)
473 cmd = ['clone']
473 cmd = ['clone']
474 if bare:
474 if bare:
475 cmd.append('--bare')
475 cmd.append('--bare')
476 elif not update_after_clone:
476 elif not update_after_clone:
477 cmd.append('--no-checkout')
477 cmd.append('--no-checkout')
478 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
478 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
479 cmd = ' '.join(cmd)
479 cmd = ' '.join(cmd)
480 # If error occurs run_git_command raises RepositoryError already
480 # If error occurs run_git_command raises RepositoryError already
481 self.run_git_command(cmd)
481 self.run_git_command(cmd)
482
482
483 def pull(self, url):
483 def pull(self, url):
484 """
484 """
485 Tries to pull changes from external location.
485 Tries to pull changes from external location.
486 """
486 """
487 url = self._get_url(url)
487 url = self._get_url(url)
488 cmd = ['pull']
488 cmd = ['pull']
489 cmd.append("--ff-only")
489 cmd.append("--ff-only")
490 cmd.append(url)
490 cmd.append(url)
491 cmd = ' '.join(cmd)
491 cmd = ' '.join(cmd)
492 # If error occurs run_git_command raises RepositoryError already
492 # If error occurs run_git_command raises RepositoryError already
493 self.run_git_command(cmd)
493 self.run_git_command(cmd)
494
494
495 @LazyProperty
495 @LazyProperty
496 def workdir(self):
496 def workdir(self):
497 """
497 """
498 Returns ``Workdir`` instance for this repository.
498 Returns ``Workdir`` instance for this repository.
499 """
499 """
500 return GitWorkdir(self)
500 return GitWorkdir(self)
501
501
502 def get_config_value(self, section, name, config_file=None):
502 def get_config_value(self, section, name, config_file=None):
503 """
503 """
504 Returns configuration value for a given [``section``] and ``name``.
504 Returns configuration value for a given [``section``] and ``name``.
505
505
506 :param section: Section we want to retrieve value from
506 :param section: Section we want to retrieve value from
507 :param name: Name of configuration we want to retrieve
507 :param name: Name of configuration we want to retrieve
508 :param config_file: A path to file which should be used to retrieve
508 :param config_file: A path to file which should be used to retrieve
509 configuration from (might also be a list of file paths)
509 configuration from (might also be a list of file paths)
510 """
510 """
511 if config_file is None:
511 if config_file is None:
512 config_file = []
512 config_file = []
513 elif isinstance(config_file, basestring):
513 elif isinstance(config_file, basestring):
514 config_file = [config_file]
514 config_file = [config_file]
515
515
516 def gen_configs():
516 def gen_configs():
517 for path in config_file + self._config_files:
517 for path in config_file + self._config_files:
518 try:
518 try:
519 yield ConfigFile.from_path(path)
519 yield ConfigFile.from_path(path)
520 except (IOError, OSError, ValueError):
520 except (IOError, OSError, ValueError):
521 continue
521 continue
522
522
523 for config in gen_configs():
523 for config in gen_configs():
524 try:
524 try:
525 return config.get(section, name)
525 return config.get(section, name)
526 except KeyError:
526 except KeyError:
527 continue
527 continue
528 return None
528 return None
529
529
530 def get_user_name(self, config_file=None):
530 def get_user_name(self, config_file=None):
531 """
531 """
532 Returns user's name from global configuration file.
532 Returns user's name from global configuration file.
533
533
534 :param config_file: A path to file which should be used to retrieve
534 :param config_file: A path to file which should be used to retrieve
535 configuration from (might also be a list of file paths)
535 configuration from (might also be a list of file paths)
536 """
536 """
537 return self.get_config_value('user', 'name', config_file)
537 return self.get_config_value('user', 'name', config_file)
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 return self.get_config_value('user', 'email', config_file)
546 return self.get_config_value('user', 'email', config_file)
@@ -1,600 +1,611 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.nodes
3 vcs.nodes
4 ~~~~~~~~~
4 ~~~~~~~~~
5
5
6 Module holding everything related to vcs nodes.
6 Module holding everything related to vcs nodes.
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 import os
11 import os
12 import stat
12 import stat
13 import posixpath
13 import posixpath
14 import mimetypes
14 import mimetypes
15
15
16 from pygments import lexers
16 from pygments import lexers
17
17
18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
19 from rhodecode.lib.vcs.utils import safe_unicode, safe_str
19 from rhodecode.lib.vcs.utils import safe_unicode, safe_str
20 from rhodecode.lib.vcs.exceptions import NodeError
20 from rhodecode.lib.vcs.exceptions import NodeError
21 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
21 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
22 from rhodecode.lib.utils import EmptyChangeset
22
23
23
24
24 class NodeKind:
25 class NodeKind:
25 SUBMODULE = -1
26 SUBMODULE = -1
26 DIR = 1
27 DIR = 1
27 FILE = 2
28 FILE = 2
28
29
29
30
30 class NodeState:
31 class NodeState:
31 ADDED = u'added'
32 ADDED = u'added'
32 CHANGED = u'changed'
33 CHANGED = u'changed'
33 NOT_CHANGED = u'not changed'
34 NOT_CHANGED = u'not changed'
34 REMOVED = u'removed'
35 REMOVED = u'removed'
35
36
36
37
37 class NodeGeneratorBase(object):
38 class NodeGeneratorBase(object):
38 """
39 """
39 Base class for removed added and changed filenodes, it's a lazy generator
40 Base class for removed added and changed filenodes, it's a lazy generator
40 class that will create filenodes only on iteration or call
41 class that will create filenodes only on iteration or call
41
42
42 The len method doesn't need to create filenodes at all
43 The len method doesn't need to create filenodes at all
43 """
44 """
44
45
45 def __init__(self, current_paths, cs):
46 def __init__(self, current_paths, cs):
46 self.cs = cs
47 self.cs = cs
47 self.current_paths = current_paths
48 self.current_paths = current_paths
48
49
49 def __call__(self):
50 def __call__(self):
50 return [n for n in self]
51 return [n for n in self]
51
52
52 def __getslice__(self, i, j):
53 def __getslice__(self, i, j):
53 for p in self.current_paths[i:j]:
54 for p in self.current_paths[i:j]:
54 yield self.cs.get_node(p)
55 yield self.cs.get_node(p)
55
56
56 def __len__(self):
57 def __len__(self):
57 return len(self.current_paths)
58 return len(self.current_paths)
58
59
59 def __iter__(self):
60 def __iter__(self):
60 for p in self.current_paths:
61 for p in self.current_paths:
61 yield self.cs.get_node(p)
62 yield self.cs.get_node(p)
62
63
63
64
64 class AddedFileNodesGenerator(NodeGeneratorBase):
65 class AddedFileNodesGenerator(NodeGeneratorBase):
65 """
66 """
66 Class holding Added files for current changeset
67 Class holding Added files for current changeset
67 """
68 """
68 pass
69 pass
69
70
70
71
71 class ChangedFileNodesGenerator(NodeGeneratorBase):
72 class ChangedFileNodesGenerator(NodeGeneratorBase):
72 """
73 """
73 Class holding Changed files for current changeset
74 Class holding Changed files for current changeset
74 """
75 """
75 pass
76 pass
76
77
77
78
78 class RemovedFileNodesGenerator(NodeGeneratorBase):
79 class RemovedFileNodesGenerator(NodeGeneratorBase):
79 """
80 """
80 Class holding removed files for current changeset
81 Class holding removed files for current changeset
81 """
82 """
82 def __iter__(self):
83 def __iter__(self):
83 for p in self.current_paths:
84 for p in self.current_paths:
84 yield RemovedFileNode(path=p)
85 yield RemovedFileNode(path=p)
85
86
86 def __getslice__(self, i, j):
87 def __getslice__(self, i, j):
87 for p in self.current_paths[i:j]:
88 for p in self.current_paths[i:j]:
88 yield RemovedFileNode(path=p)
89 yield RemovedFileNode(path=p)
89
90
90
91
91 class Node(object):
92 class Node(object):
92 """
93 """
93 Simplest class representing file or directory on repository. SCM backends
94 Simplest class representing file or directory on repository. SCM backends
94 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
95 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
95 directly.
96 directly.
96
97
97 Node's ``path`` cannot start with slash as we operate on *relative* paths
98 Node's ``path`` cannot start with slash as we operate on *relative* paths
98 only. Moreover, every single node is identified by the ``path`` attribute,
99 only. Moreover, every single node is identified by the ``path`` attribute,
99 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
100 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
100 """
101 """
101
102
102 def __init__(self, path, kind):
103 def __init__(self, path, kind):
103 if path.startswith('/'):
104 if path.startswith('/'):
104 raise NodeError("Cannot initialize Node objects with slash at "
105 raise NodeError("Cannot initialize Node objects with slash at "
105 "the beginning as only relative paths are supported")
106 "the beginning as only relative paths are supported")
106 self.path = path.rstrip('/')
107 self.path = path.rstrip('/')
107 if path == '' and kind != NodeKind.DIR:
108 if path == '' and kind != NodeKind.DIR:
108 raise NodeError("Only DirNode and its subclasses may be "
109 raise NodeError("Only DirNode and its subclasses may be "
109 "initialized with empty path")
110 "initialized with empty path")
110 self.kind = kind
111 self.kind = kind
111 #self.dirs, self.files = [], []
112 #self.dirs, self.files = [], []
112 if self.is_root() and not self.is_dir():
113 if self.is_root() and not self.is_dir():
113 raise NodeError("Root node cannot be FILE kind")
114 raise NodeError("Root node cannot be FILE kind")
114
115
115 @LazyProperty
116 @LazyProperty
116 def parent(self):
117 def parent(self):
117 parent_path = self.get_parent_path()
118 parent_path = self.get_parent_path()
118 if parent_path:
119 if parent_path:
119 if self.changeset:
120 if self.changeset:
120 return self.changeset.get_node(parent_path)
121 return self.changeset.get_node(parent_path)
121 return DirNode(parent_path)
122 return DirNode(parent_path)
122 return None
123 return None
123
124
124 @LazyProperty
125 @LazyProperty
125 def unicode_path(self):
126 def unicode_path(self):
126 return safe_unicode(self.path)
127 return safe_unicode(self.path)
127
128
128 @LazyProperty
129 @LazyProperty
129 def name(self):
130 def name(self):
130 """
131 """
131 Returns name of the node so if its path
132 Returns name of the node so if its path
132 then only last part is returned.
133 then only last part is returned.
133 """
134 """
134 return safe_unicode(self.path.rstrip('/').split('/')[-1])
135 return safe_unicode(self.path.rstrip('/').split('/')[-1])
135
136
136 def _get_kind(self):
137 def _get_kind(self):
137 return self._kind
138 return self._kind
138
139
139 def _set_kind(self, kind):
140 def _set_kind(self, kind):
140 if hasattr(self, '_kind'):
141 if hasattr(self, '_kind'):
141 raise NodeError("Cannot change node's kind")
142 raise NodeError("Cannot change node's kind")
142 else:
143 else:
143 self._kind = kind
144 self._kind = kind
144 # Post setter check (path's trailing slash)
145 # Post setter check (path's trailing slash)
145 if self.path.endswith('/'):
146 if self.path.endswith('/'):
146 raise NodeError("Node's path cannot end with slash")
147 raise NodeError("Node's path cannot end with slash")
147
148
148 kind = property(_get_kind, _set_kind)
149 kind = property(_get_kind, _set_kind)
149
150
150 def __cmp__(self, other):
151 def __cmp__(self, other):
151 """
152 """
152 Comparator using name of the node, needed for quick list sorting.
153 Comparator using name of the node, needed for quick list sorting.
153 """
154 """
154 kind_cmp = cmp(self.kind, other.kind)
155 kind_cmp = cmp(self.kind, other.kind)
155 if kind_cmp:
156 if kind_cmp:
156 return kind_cmp
157 return kind_cmp
157 return cmp(self.name, other.name)
158 return cmp(self.name, other.name)
158
159
159 def __eq__(self, other):
160 def __eq__(self, other):
160 for attr in ['name', 'path', 'kind']:
161 for attr in ['name', 'path', 'kind']:
161 if getattr(self, attr) != getattr(other, attr):
162 if getattr(self, attr) != getattr(other, attr):
162 return False
163 return False
163 if self.is_file():
164 if self.is_file():
164 if self.content != other.content:
165 if self.content != other.content:
165 return False
166 return False
166 else:
167 else:
167 # For DirNode's check without entering each dir
168 # For DirNode's check without entering each dir
168 self_nodes_paths = list(sorted(n.path for n in self.nodes))
169 self_nodes_paths = list(sorted(n.path for n in self.nodes))
169 other_nodes_paths = list(sorted(n.path for n in self.nodes))
170 other_nodes_paths = list(sorted(n.path for n in self.nodes))
170 if self_nodes_paths != other_nodes_paths:
171 if self_nodes_paths != other_nodes_paths:
171 return False
172 return False
172 return True
173 return True
173
174
174 def __nq__(self, other):
175 def __nq__(self, other):
175 return not self.__eq__(other)
176 return not self.__eq__(other)
176
177
177 def __repr__(self):
178 def __repr__(self):
178 return '<%s %r>' % (self.__class__.__name__, self.path)
179 return '<%s %r>' % (self.__class__.__name__, self.path)
179
180
180 def __str__(self):
181 def __str__(self):
181 return self.__repr__()
182 return self.__repr__()
182
183
183 def __unicode__(self):
184 def __unicode__(self):
184 return self.name
185 return self.name
185
186
186 def get_parent_path(self):
187 def get_parent_path(self):
187 """
188 """
188 Returns node's parent path or empty string if node is root.
189 Returns node's parent path or empty string if node is root.
189 """
190 """
190 if self.is_root():
191 if self.is_root():
191 return ''
192 return ''
192 return posixpath.dirname(self.path.rstrip('/')) + '/'
193 return posixpath.dirname(self.path.rstrip('/')) + '/'
193
194
194 def is_file(self):
195 def is_file(self):
195 """
196 """
196 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
197 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
197 otherwise.
198 otherwise.
198 """
199 """
199 return self.kind == NodeKind.FILE
200 return self.kind == NodeKind.FILE
200
201
201 def is_dir(self):
202 def is_dir(self):
202 """
203 """
203 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
204 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
204 otherwise.
205 otherwise.
205 """
206 """
206 return self.kind == NodeKind.DIR
207 return self.kind == NodeKind.DIR
207
208
208 def is_root(self):
209 def is_root(self):
209 """
210 """
210 Returns ``True`` if node is a root node and ``False`` otherwise.
211 Returns ``True`` if node is a root node and ``False`` otherwise.
211 """
212 """
212 return self.kind == NodeKind.DIR and self.path == ''
213 return self.kind == NodeKind.DIR and self.path == ''
213
214
214 def is_submodule(self):
215 def is_submodule(self):
215 """
216 """
216 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
217 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
217 otherwise.
218 otherwise.
218 """
219 """
219 return self.kind == NodeKind.SUBMODULE
220 return self.kind == NodeKind.SUBMODULE
220
221
221 @LazyProperty
222 @LazyProperty
222 def added(self):
223 def added(self):
223 return self.state is NodeState.ADDED
224 return self.state is NodeState.ADDED
224
225
225 @LazyProperty
226 @LazyProperty
226 def changed(self):
227 def changed(self):
227 return self.state is NodeState.CHANGED
228 return self.state is NodeState.CHANGED
228
229
229 @LazyProperty
230 @LazyProperty
230 def not_changed(self):
231 def not_changed(self):
231 return self.state is NodeState.NOT_CHANGED
232 return self.state is NodeState.NOT_CHANGED
232
233
233 @LazyProperty
234 @LazyProperty
234 def removed(self):
235 def removed(self):
235 return self.state is NodeState.REMOVED
236 return self.state is NodeState.REMOVED
236
237
237
238
238 class FileNode(Node):
239 class FileNode(Node):
239 """
240 """
240 Class representing file nodes.
241 Class representing file nodes.
241
242
242 :attribute: path: path to the node, relative to repostiory's root
243 :attribute: path: path to the node, relative to repostiory's root
243 :attribute: content: if given arbitrary sets content of the file
244 :attribute: content: if given arbitrary sets content of the file
244 :attribute: changeset: if given, first time content is accessed, callback
245 :attribute: changeset: if given, first time content is accessed, callback
245 :attribute: mode: octal stat mode for a node. Default is 0100644.
246 :attribute: mode: octal stat mode for a node. Default is 0100644.
246 """
247 """
247
248
248 def __init__(self, path, content=None, changeset=None, mode=None):
249 def __init__(self, path, content=None, changeset=None, mode=None):
249 """
250 """
250 Only one of ``content`` and ``changeset`` may be given. Passing both
251 Only one of ``content`` and ``changeset`` may be given. Passing both
251 would raise ``NodeError`` exception.
252 would raise ``NodeError`` exception.
252
253
253 :param path: relative path to the node
254 :param path: relative path to the node
254 :param content: content may be passed to constructor
255 :param content: content may be passed to constructor
255 :param changeset: if given, will use it to lazily fetch content
256 :param changeset: if given, will use it to lazily fetch content
256 :param mode: octal representation of ST_MODE (i.e. 0100644)
257 :param mode: octal representation of ST_MODE (i.e. 0100644)
257 """
258 """
258
259
259 if content and changeset:
260 if content and changeset:
260 raise NodeError("Cannot use both content and changeset")
261 raise NodeError("Cannot use both content and changeset")
261 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
262 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
262 self.changeset = changeset
263 self.changeset = changeset
263 self._content = content
264 self._content = content
264 self._mode = mode or 0100644
265 self._mode = mode or 0100644
265
266
266 @LazyProperty
267 @LazyProperty
267 def mode(self):
268 def mode(self):
268 """
269 """
269 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
270 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
270 use value given at initialization or 0100644 (default).
271 use value given at initialization or 0100644 (default).
271 """
272 """
272 if self.changeset:
273 if self.changeset:
273 mode = self.changeset.get_file_mode(self.path)
274 mode = self.changeset.get_file_mode(self.path)
274 else:
275 else:
275 mode = self._mode
276 mode = self._mode
276 return mode
277 return mode
277
278
278 @property
279 @property
279 def content(self):
280 def content(self):
280 """
281 """
281 Returns lazily content of the FileNode. If possible, would try to
282 Returns lazily content of the FileNode. If possible, would try to
282 decode content from UTF-8.
283 decode content from UTF-8.
283 """
284 """
284 if self.changeset:
285 if self.changeset:
285 content = self.changeset.get_file_content(self.path)
286 content = self.changeset.get_file_content(self.path)
286 else:
287 else:
287 content = self._content
288 content = self._content
288
289
289 if bool(content and '\0' in content):
290 if bool(content and '\0' in content):
290 return content
291 return content
291 return safe_unicode(content)
292 return safe_unicode(content)
292
293
293 @LazyProperty
294 @LazyProperty
294 def size(self):
295 def size(self):
295 if self.changeset:
296 if self.changeset:
296 return self.changeset.get_file_size(self.path)
297 return self.changeset.get_file_size(self.path)
297 raise NodeError("Cannot retrieve size of the file without related "
298 raise NodeError("Cannot retrieve size of the file without related "
298 "changeset attribute")
299 "changeset attribute")
299
300
300 @LazyProperty
301 @LazyProperty
301 def message(self):
302 def message(self):
302 if self.changeset:
303 if self.changeset:
303 return self.last_changeset.message
304 return self.last_changeset.message
304 raise NodeError("Cannot retrieve message of the file without related "
305 raise NodeError("Cannot retrieve message of the file without related "
305 "changeset attribute")
306 "changeset attribute")
306
307
307 @LazyProperty
308 @LazyProperty
308 def last_changeset(self):
309 def last_changeset(self):
309 if self.changeset:
310 if self.changeset:
310 return self.changeset.get_file_changeset(self.path)
311 return self.changeset.get_file_changeset(self.path)
311 raise NodeError("Cannot retrieve last changeset of the file without "
312 raise NodeError("Cannot retrieve last changeset of the file without "
312 "related changeset attribute")
313 "related changeset attribute")
313
314
314 def get_mimetype(self):
315 def get_mimetype(self):
315 """
316 """
316 Mimetype is calculated based on the file's content. If ``_mimetype``
317 Mimetype is calculated based on the file's content. If ``_mimetype``
317 attribute is available, it will be returned (backends which store
318 attribute is available, it will be returned (backends which store
318 mimetypes or can easily recognize them, should set this private
319 mimetypes or can easily recognize them, should set this private
319 attribute to indicate that type should *NOT* be calculated).
320 attribute to indicate that type should *NOT* be calculated).
320 """
321 """
321 if hasattr(self, '_mimetype'):
322 if hasattr(self, '_mimetype'):
322 if (isinstance(self._mimetype, (tuple, list,)) and
323 if (isinstance(self._mimetype, (tuple, list,)) and
323 len(self._mimetype) == 2):
324 len(self._mimetype) == 2):
324 return self._mimetype
325 return self._mimetype
325 else:
326 else:
326 raise NodeError('given _mimetype attribute must be an 2 '
327 raise NodeError('given _mimetype attribute must be an 2 '
327 'element list or tuple')
328 'element list or tuple')
328
329
329 mtype, encoding = mimetypes.guess_type(self.name)
330 mtype, encoding = mimetypes.guess_type(self.name)
330
331
331 if mtype is None:
332 if mtype is None:
332 if self.is_binary:
333 if self.is_binary:
333 mtype = 'application/octet-stream'
334 mtype = 'application/octet-stream'
334 encoding = None
335 encoding = None
335 else:
336 else:
336 mtype = 'text/plain'
337 mtype = 'text/plain'
337 encoding = None
338 encoding = None
338 return mtype, encoding
339 return mtype, encoding
339
340
340 @LazyProperty
341 @LazyProperty
341 def mimetype(self):
342 def mimetype(self):
342 """
343 """
343 Wrapper around full mimetype info. It returns only type of fetched
344 Wrapper around full mimetype info. It returns only type of fetched
344 mimetype without the encoding part. use get_mimetype function to fetch
345 mimetype without the encoding part. use get_mimetype function to fetch
345 full set of (type,encoding)
346 full set of (type,encoding)
346 """
347 """
347 return self.get_mimetype()[0]
348 return self.get_mimetype()[0]
348
349
349 @LazyProperty
350 @LazyProperty
350 def mimetype_main(self):
351 def mimetype_main(self):
351 return self.mimetype.split('/')[0]
352 return self.mimetype.split('/')[0]
352
353
353 @LazyProperty
354 @LazyProperty
354 def lexer(self):
355 def lexer(self):
355 """
356 """
356 Returns pygment's lexer class. Would try to guess lexer taking file's
357 Returns pygment's lexer class. Would try to guess lexer taking file's
357 content, name and mimetype.
358 content, name and mimetype.
358 """
359 """
359 try:
360 try:
360 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
361 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
361 except lexers.ClassNotFound:
362 except lexers.ClassNotFound:
362 lexer = lexers.TextLexer()
363 lexer = lexers.TextLexer()
363 # returns first alias
364 # returns first alias
364 return lexer
365 return lexer
365
366
366 @LazyProperty
367 @LazyProperty
367 def lexer_alias(self):
368 def lexer_alias(self):
368 """
369 """
369 Returns first alias of the lexer guessed for this file.
370 Returns first alias of the lexer guessed for this file.
370 """
371 """
371 return self.lexer.aliases[0]
372 return self.lexer.aliases[0]
372
373
373 @LazyProperty
374 @LazyProperty
374 def history(self):
375 def history(self):
375 """
376 """
376 Returns a list of changeset for this file in which the file was changed
377 Returns a list of changeset for this file in which the file was changed
377 """
378 """
378 if self.changeset is None:
379 if self.changeset is None:
379 raise NodeError('Unable to get changeset for this FileNode')
380 raise NodeError('Unable to get changeset for this FileNode')
380 return self.changeset.get_file_history(self.path)
381 return self.changeset.get_file_history(self.path)
381
382
382 @LazyProperty
383 @LazyProperty
383 def annotate(self):
384 def annotate(self):
384 """
385 """
385 Returns a list of three element tuples with lineno,changeset and line
386 Returns a list of three element tuples with lineno,changeset and line
386 """
387 """
387 if self.changeset is None:
388 if self.changeset is None:
388 raise NodeError('Unable to get changeset for this FileNode')
389 raise NodeError('Unable to get changeset for this FileNode')
389 return self.changeset.get_file_annotate(self.path)
390 return self.changeset.get_file_annotate(self.path)
390
391
391 @LazyProperty
392 @LazyProperty
392 def state(self):
393 def state(self):
393 if not self.changeset:
394 if not self.changeset:
394 raise NodeError("Cannot check state of the node if it's not "
395 raise NodeError("Cannot check state of the node if it's not "
395 "linked with changeset")
396 "linked with changeset")
396 elif self.path in (node.path for node in self.changeset.added):
397 elif self.path in (node.path for node in self.changeset.added):
397 return NodeState.ADDED
398 return NodeState.ADDED
398 elif self.path in (node.path for node in self.changeset.changed):
399 elif self.path in (node.path for node in self.changeset.changed):
399 return NodeState.CHANGED
400 return NodeState.CHANGED
400 else:
401 else:
401 return NodeState.NOT_CHANGED
402 return NodeState.NOT_CHANGED
402
403
403 @property
404 @property
404 def is_binary(self):
405 def is_binary(self):
405 """
406 """
406 Returns True if file has binary content.
407 Returns True if file has binary content.
407 """
408 """
408 _bin = '\0' in self.content
409 _bin = '\0' in self.content
409 return _bin
410 return _bin
410
411
411 @LazyProperty
412 @LazyProperty
412 def extension(self):
413 def extension(self):
413 """Returns filenode extension"""
414 """Returns filenode extension"""
414 return self.name.split('.')[-1]
415 return self.name.split('.')[-1]
415
416
416 def is_executable(self):
417 def is_executable(self):
417 """
418 """
418 Returns ``True`` if file has executable flag turned on.
419 Returns ``True`` if file has executable flag turned on.
419 """
420 """
420 return bool(self.mode & stat.S_IXUSR)
421 return bool(self.mode & stat.S_IXUSR)
421
422
422 def __repr__(self):
423 def __repr__(self):
423 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
424 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
424 self.changeset.short_id)
425 self.changeset.short_id)
425
426
426
427
427 class RemovedFileNode(FileNode):
428 class RemovedFileNode(FileNode):
428 """
429 """
429 Dummy FileNode class - trying to access any public attribute except path,
430 Dummy FileNode class - trying to access any public attribute except path,
430 name, kind or state (or methods/attributes checking those two) would raise
431 name, kind or state (or methods/attributes checking those two) would raise
431 RemovedFileNodeError.
432 RemovedFileNodeError.
432 """
433 """
433 ALLOWED_ATTRIBUTES = ['name', 'path', 'state', 'is_root', 'is_file',
434 ALLOWED_ATTRIBUTES = ['name', 'path', 'state', 'is_root', 'is_file',
434 'is_dir', 'kind', 'added', 'changed', 'not_changed', 'removed']
435 'is_dir', 'kind', 'added', 'changed', 'not_changed', 'removed']
435
436
436 def __init__(self, path):
437 def __init__(self, path):
437 """
438 """
438 :param path: relative path to the node
439 :param path: relative path to the node
439 """
440 """
440 super(RemovedFileNode, self).__init__(path=path)
441 super(RemovedFileNode, self).__init__(path=path)
441
442
442 def __getattribute__(self, attr):
443 def __getattribute__(self, attr):
443 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
444 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
444 return super(RemovedFileNode, self).__getattribute__(attr)
445 return super(RemovedFileNode, self).__getattribute__(attr)
445 raise RemovedFileNodeError("Cannot access attribute %s on "
446 raise RemovedFileNodeError("Cannot access attribute %s on "
446 "RemovedFileNode" % attr)
447 "RemovedFileNode" % attr)
447
448
448 @LazyProperty
449 @LazyProperty
449 def state(self):
450 def state(self):
450 return NodeState.REMOVED
451 return NodeState.REMOVED
451
452
452
453
453 class DirNode(Node):
454 class DirNode(Node):
454 """
455 """
455 DirNode stores list of files and directories within this node.
456 DirNode stores list of files and directories within this node.
456 Nodes may be used standalone but within repository context they
457 Nodes may be used standalone but within repository context they
457 lazily fetch data within same repositorty's changeset.
458 lazily fetch data within same repositorty's changeset.
458 """
459 """
459
460
460 def __init__(self, path, nodes=(), changeset=None):
461 def __init__(self, path, nodes=(), changeset=None):
461 """
462 """
462 Only one of ``nodes`` and ``changeset`` may be given. Passing both
463 Only one of ``nodes`` and ``changeset`` may be given. Passing both
463 would raise ``NodeError`` exception.
464 would raise ``NodeError`` exception.
464
465
465 :param path: relative path to the node
466 :param path: relative path to the node
466 :param nodes: content may be passed to constructor
467 :param nodes: content may be passed to constructor
467 :param changeset: if given, will use it to lazily fetch content
468 :param changeset: if given, will use it to lazily fetch content
468 :param size: always 0 for ``DirNode``
469 :param size: always 0 for ``DirNode``
469 """
470 """
470 if nodes and changeset:
471 if nodes and changeset:
471 raise NodeError("Cannot use both nodes and changeset")
472 raise NodeError("Cannot use both nodes and changeset")
472 super(DirNode, self).__init__(path, NodeKind.DIR)
473 super(DirNode, self).__init__(path, NodeKind.DIR)
473 self.changeset = changeset
474 self.changeset = changeset
474 self._nodes = nodes
475 self._nodes = nodes
475
476
476 @LazyProperty
477 @LazyProperty
477 def content(self):
478 def content(self):
478 raise NodeError("%s represents a dir and has no ``content`` attribute"
479 raise NodeError("%s represents a dir and has no ``content`` attribute"
479 % self)
480 % self)
480
481
481 @LazyProperty
482 @LazyProperty
482 def nodes(self):
483 def nodes(self):
483 if self.changeset:
484 if self.changeset:
484 nodes = self.changeset.get_nodes(self.path)
485 nodes = self.changeset.get_nodes(self.path)
485 else:
486 else:
486 nodes = self._nodes
487 nodes = self._nodes
487 self._nodes_dict = dict((node.path, node) for node in nodes)
488 self._nodes_dict = dict((node.path, node) for node in nodes)
488 return sorted(nodes)
489 return sorted(nodes)
489
490
490 @LazyProperty
491 @LazyProperty
491 def files(self):
492 def files(self):
492 return sorted((node for node in self.nodes if node.is_file()))
493 return sorted((node for node in self.nodes if node.is_file()))
493
494
494 @LazyProperty
495 @LazyProperty
495 def dirs(self):
496 def dirs(self):
496 return sorted((node for node in self.nodes if node.is_dir()))
497 return sorted((node for node in self.nodes if node.is_dir()))
497
498
498 def __iter__(self):
499 def __iter__(self):
499 for node in self.nodes:
500 for node in self.nodes:
500 yield node
501 yield node
501
502
502 def get_node(self, path):
503 def get_node(self, path):
503 """
504 """
504 Returns node from within this particular ``DirNode``, so it is now
505 Returns node from within this particular ``DirNode``, so it is now
505 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
506 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
506 'docs'. In order to access deeper nodes one must fetch nodes between
507 'docs'. In order to access deeper nodes one must fetch nodes between
507 them first - this would work::
508 them first - this would work::
508
509
509 docs = root.get_node('docs')
510 docs = root.get_node('docs')
510 docs.get_node('api').get_node('index.rst')
511 docs.get_node('api').get_node('index.rst')
511
512
512 :param: path - relative to the current node
513 :param: path - relative to the current node
513
514
514 .. note::
515 .. note::
515 To access lazily (as in example above) node have to be initialized
516 To access lazily (as in example above) node have to be initialized
516 with related changeset object - without it node is out of
517 with related changeset object - without it node is out of
517 context and may know nothing about anything else than nearest
518 context and may know nothing about anything else than nearest
518 (located at same level) nodes.
519 (located at same level) nodes.
519 """
520 """
520 try:
521 try:
521 path = path.rstrip('/')
522 path = path.rstrip('/')
522 if path == '':
523 if path == '':
523 raise NodeError("Cannot retrieve node without path")
524 raise NodeError("Cannot retrieve node without path")
524 self.nodes # access nodes first in order to set _nodes_dict
525 self.nodes # access nodes first in order to set _nodes_dict
525 paths = path.split('/')
526 paths = path.split('/')
526 if len(paths) == 1:
527 if len(paths) == 1:
527 if not self.is_root():
528 if not self.is_root():
528 path = '/'.join((self.path, paths[0]))
529 path = '/'.join((self.path, paths[0]))
529 else:
530 else:
530 path = paths[0]
531 path = paths[0]
531 return self._nodes_dict[path]
532 return self._nodes_dict[path]
532 elif len(paths) > 1:
533 elif len(paths) > 1:
533 if self.changeset is None:
534 if self.changeset is None:
534 raise NodeError("Cannot access deeper "
535 raise NodeError("Cannot access deeper "
535 "nodes without changeset")
536 "nodes without changeset")
536 else:
537 else:
537 path1, path2 = paths[0], '/'.join(paths[1:])
538 path1, path2 = paths[0], '/'.join(paths[1:])
538 return self.get_node(path1).get_node(path2)
539 return self.get_node(path1).get_node(path2)
539 else:
540 else:
540 raise KeyError
541 raise KeyError
541 except KeyError:
542 except KeyError:
542 raise NodeError("Node does not exist at %s" % path)
543 raise NodeError("Node does not exist at %s" % path)
543
544
544 @LazyProperty
545 @LazyProperty
545 def state(self):
546 def state(self):
546 raise NodeError("Cannot access state of DirNode")
547 raise NodeError("Cannot access state of DirNode")
547
548
548 @LazyProperty
549 @LazyProperty
549 def size(self):
550 def size(self):
550 size = 0
551 size = 0
551 for root, dirs, files in self.changeset.walk(self.path):
552 for root, dirs, files in self.changeset.walk(self.path):
552 for f in files:
553 for f in files:
553 size += f.size
554 size += f.size
554
555
555 return size
556 return size
556
557
557 def __repr__(self):
558 def __repr__(self):
558 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
559 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
559 self.changeset.short_id)
560 self.changeset.short_id)
560
561
561
562
562 class RootNode(DirNode):
563 class RootNode(DirNode):
563 """
564 """
564 DirNode being the root node of the repository.
565 DirNode being the root node of the repository.
565 """
566 """
566
567
567 def __init__(self, nodes=(), changeset=None):
568 def __init__(self, nodes=(), changeset=None):
568 super(RootNode, self).__init__(path='', nodes=nodes,
569 super(RootNode, self).__init__(path='', nodes=nodes,
569 changeset=changeset)
570 changeset=changeset)
570
571
571 def __repr__(self):
572 def __repr__(self):
572 return '<%s>' % self.__class__.__name__
573 return '<%s>' % self.__class__.__name__
573
574
574
575
575 class SubModuleNode(Node):
576 class SubModuleNode(Node):
576 """
577 """
577 represents a SubModule of Git or SubRepo of Mercurial
578 represents a SubModule of Git or SubRepo of Mercurial
578 """
579 """
580 is_binary = False
581 size = 0
582
579 def __init__(self, name, url=None, changeset=None, alias=None):
583 def __init__(self, name, url=None, changeset=None, alias=None):
580 self.path = name
584 self.path = name
581 self.kind = NodeKind.SUBMODULE
585 self.kind = NodeKind.SUBMODULE
582 self.alias = alias
586 self.alias = alias
583 # changeset MUST be STR !! since it can point to non-valid SCM
587 # we have to use emptyChangeset here since this can point to svn/git/hg
584 self.changeset = str(changeset)
588 # submodules we cannot get from repository
589 self.changeset = EmptyChangeset(str(changeset), alias=alias)
585 self.url = url or self._extract_submodule_url()
590 self.url = url or self._extract_submodule_url()
586
591
592 def __repr__(self):
593 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
594 self.changeset.short_id)
595
587 def _extract_submodule_url(self):
596 def _extract_submodule_url(self):
588 if self.alias == 'git':
597 if self.alias == 'git':
598 #TODO: find a way to parse gits submodule file and extract the
599 # linking URL
589 return self.path
600 return self.path
590 if self.alias == 'hg':
601 if self.alias == 'hg':
591 return self.path
602 return self.path
592
603
593 @LazyProperty
604 @LazyProperty
594 def name(self):
605 def name(self):
595 """
606 """
596 Returns name of the node so if its path
607 Returns name of the node so if its path
597 then only last part is returned.
608 then only last part is returned.
598 """
609 """
599 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
610 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
600 return u'%s @ %s' % (org, self.changeset[:12])
611 return u'%s @ %s' % (org, self.changeset.short_id)
General Comments 0
You need to be logged in to leave comments. Login now