##// END OF EJS Templates
Fixed lookup by Tag sha in git backend
marcink -
r2536:aaa41736 beta
parent child Browse files
Show More
@@ -1,504 +1,502
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 from __future__ import with_statement
26 26 import os
27 27 import logging
28 28 import traceback
29 29 import tempfile
30 30
31 31 from pylons import request, response, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from pylons.decorators import jsonify
35 from paste.fileapp import FileApp, _FileIter
36 35
37 36 from rhodecode.lib import diffs
38 37 from rhodecode.lib import helpers as h
39 38
40 39 from rhodecode.lib.compat import OrderedDict
41 40 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str
42 41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 42 from rhodecode.lib.base import BaseRepoController, render
44 43 from rhodecode.lib.utils import EmptyChangeset
45 44 from rhodecode.lib.vcs.conf import settings
46 45 from rhodecode.lib.vcs.exceptions import RepositoryError, \
47 46 ChangesetDoesNotExistError, EmptyRepositoryError, \
48 47 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
49 48 from rhodecode.lib.vcs.nodes import FileNode
50 49
51 50 from rhodecode.model.repo import RepoModel
52 51 from rhodecode.model.scm import ScmModel
53 52 from rhodecode.model.db import Repository
54 53
55 54 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
56 55 _context_url, get_line_ctx, get_ignore_ws
57 56
58 57
59 58 log = logging.getLogger(__name__)
60 59
61 60
62 61 class FilesController(BaseRepoController):
63 62
64
65 63 def __before__(self):
66 64 super(FilesController, self).__before__()
67 65 c.cut_off_limit = self.cut_off_limit
68 66
69 67 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
70 68 """
71 69 Safe way to get changeset if error occur it redirects to tip with
72 70 proper message
73 71
74 72 :param rev: revision to fetch
75 73 :param repo_name: repo name to redirect after
76 74 """
77 75
78 76 try:
79 77 return c.rhodecode_repo.get_changeset(rev)
80 78 except EmptyRepositoryError, e:
81 79 if not redirect_after:
82 80 return None
83 81 url_ = url('files_add_home',
84 82 repo_name=c.repo_name,
85 83 revision=0, f_path='')
86 84 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
87 85 h.flash(h.literal(_('There are no files yet %s' % add_new)),
88 86 category='warning')
89 87 redirect(h.url('summary_home', repo_name=repo_name))
90 88
91 89 except RepositoryError, e:
92 90 h.flash(str(e), category='warning')
93 91 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
94 92
95 93 def __get_filenode_or_redirect(self, repo_name, cs, path):
96 94 """
97 95 Returns file_node, if error occurs or given path is directory,
98 96 it'll redirect to top level path
99 97
100 98 :param repo_name: repo_name
101 99 :param cs: given changeset
102 100 :param path: path to lookup
103 101 """
104 102
105 103 try:
106 104 file_node = cs.get_node(path)
107 105 if file_node.is_dir():
108 106 raise RepositoryError('given path is a directory')
109 107 except RepositoryError, e:
110 108 h.flash(str(e), category='warning')
111 109 redirect(h.url('files_home', repo_name=repo_name,
112 110 revision=cs.raw_id))
113 111
114 112 return file_node
115 113
116 114 @LoginRequired()
117 115 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
118 116 'repository.admin')
119 117 def index(self, repo_name, revision, f_path, annotate=False):
120 118 # redirect to given revision from form if given
121 119 post_revision = request.POST.get('at_rev', None)
122 120 if post_revision:
123 121 cs = self.__get_cs_or_redirect(post_revision, repo_name)
124 122 redirect(url('files_home', repo_name=c.repo_name,
125 123 revision=cs.raw_id, f_path=f_path))
126 124
127 125 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
128 126 c.branch = request.GET.get('branch', None)
129 127 c.f_path = f_path
130 128 c.annotate = annotate
131 129 cur_rev = c.changeset.revision
132 130
133 131 # prev link
134 132 try:
135 133 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
136 134 c.url_prev = url('files_home', repo_name=c.repo_name,
137 135 revision=prev_rev.raw_id, f_path=f_path)
138 136 if c.branch:
139 137 c.url_prev += '?branch=%s' % c.branch
140 138 except (ChangesetDoesNotExistError, VCSError):
141 139 c.url_prev = '#'
142 140
143 141 # next link
144 142 try:
145 143 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
146 144 c.url_next = url('files_home', repo_name=c.repo_name,
147 145 revision=next_rev.raw_id, f_path=f_path)
148 146 if c.branch:
149 147 c.url_next += '?branch=%s' % c.branch
150 148 except (ChangesetDoesNotExistError, VCSError):
151 149 c.url_next = '#'
152 150
153 151 # files or dirs
154 152 try:
155 153 c.file = c.changeset.get_node(f_path)
156 154
157 155 if c.file.is_file():
158 156 _hist = c.changeset.get_file_history(f_path)
159 157 c.file_history = self._get_node_history(c.changeset, f_path,
160 158 _hist)
161 159 c.authors = []
162 160 for a in set([x.author for x in _hist]):
163 161 c.authors.append((h.email(a), h.person(a)))
164 162 else:
165 163 c.authors = c.file_history = []
166 164 except RepositoryError, e:
167 165 h.flash(str(e), category='warning')
168 166 redirect(h.url('files_home', repo_name=repo_name,
169 revision=revision))
167 revision='tip'))
170 168
171 169 return render('files/files.html')
172 170
173 171 @LoginRequired()
174 172 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
175 173 'repository.admin')
176 174 def rawfile(self, repo_name, revision, f_path):
177 175 cs = self.__get_cs_or_redirect(revision, repo_name)
178 176 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
179 177
180 178 response.content_disposition = 'attachment; filename=%s' % \
181 179 safe_str(f_path.split(Repository.url_sep())[-1])
182 180
183 181 response.content_type = file_node.mimetype
184 182 return file_node.content
185 183
186 184 @LoginRequired()
187 185 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
188 186 'repository.admin')
189 187 def raw(self, repo_name, revision, f_path):
190 188 cs = self.__get_cs_or_redirect(revision, repo_name)
191 189 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
192 190
193 191 raw_mimetype_mapping = {
194 192 # map original mimetype to a mimetype used for "show as raw"
195 193 # you can also provide a content-disposition to override the
196 194 # default "attachment" disposition.
197 195 # orig_type: (new_type, new_dispo)
198 196
199 197 # show images inline:
200 198 'image/x-icon': ('image/x-icon', 'inline'),
201 199 'image/png': ('image/png', 'inline'),
202 200 'image/gif': ('image/gif', 'inline'),
203 201 'image/jpeg': ('image/jpeg', 'inline'),
204 202 'image/svg+xml': ('image/svg+xml', 'inline'),
205 203 }
206 204
207 205 mimetype = file_node.mimetype
208 206 try:
209 207 mimetype, dispo = raw_mimetype_mapping[mimetype]
210 208 except KeyError:
211 209 # we don't know anything special about this, handle it safely
212 210 if file_node.is_binary:
213 211 # do same as download raw for binary files
214 212 mimetype, dispo = 'application/octet-stream', 'attachment'
215 213 else:
216 214 # do not just use the original mimetype, but force text/plain,
217 215 # otherwise it would serve text/html and that might be unsafe.
218 216 # Note: underlying vcs library fakes text/plain mimetype if the
219 217 # mimetype can not be determined and it thinks it is not
220 218 # binary.This might lead to erroneous text display in some
221 219 # cases, but helps in other cases, like with text files
222 220 # without extension.
223 221 mimetype, dispo = 'text/plain', 'inline'
224 222
225 223 if dispo == 'attachment':
226 224 dispo = 'attachment; filename=%s' % \
227 225 safe_str(f_path.split(os.sep)[-1])
228 226
229 227 response.content_disposition = dispo
230 228 response.content_type = mimetype
231 229 return file_node.content
232 230
233 231 @LoginRequired()
234 232 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
235 233 def edit(self, repo_name, revision, f_path):
236 234 r_post = request.POST
237 235
238 236 c.cs = self.__get_cs_or_redirect(revision, repo_name)
239 237 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
240 238
241 239 if c.file.is_binary:
242 240 return redirect(url('files_home', repo_name=c.repo_name,
243 241 revision=c.cs.raw_id, f_path=f_path))
244 242
245 243 c.f_path = f_path
246 244
247 245 if r_post:
248 246
249 247 old_content = c.file.content
250 248 sl = old_content.splitlines(1)
251 249 first_line = sl[0] if sl else ''
252 250 # modes: 0 - Unix, 1 - Mac, 2 - DOS
253 251 mode = detect_mode(first_line, 0)
254 252 content = convert_line_endings(r_post.get('content'), mode)
255 253
256 254 message = r_post.get('message') or (_('Edited %s via RhodeCode')
257 255 % (f_path))
258 256 author = self.rhodecode_user.full_contact
259 257
260 258 if content == old_content:
261 259 h.flash(_('No changes'),
262 260 category='warning')
263 261 return redirect(url('changeset_home', repo_name=c.repo_name,
264 262 revision='tip'))
265 263
266 264 try:
267 265 self.scm_model.commit_change(repo=c.rhodecode_repo,
268 266 repo_name=repo_name, cs=c.cs,
269 267 user=self.rhodecode_user,
270 268 author=author, message=message,
271 269 content=content, f_path=f_path)
272 270 h.flash(_('Successfully committed to %s' % f_path),
273 271 category='success')
274 272
275 273 except Exception:
276 274 log.error(traceback.format_exc())
277 275 h.flash(_('Error occurred during commit'), category='error')
278 276 return redirect(url('changeset_home',
279 277 repo_name=c.repo_name, revision='tip'))
280 278
281 279 return render('files/files_edit.html')
282 280
283 281 @LoginRequired()
284 282 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
285 283 def add(self, repo_name, revision, f_path):
286 284 r_post = request.POST
287 285 c.cs = self.__get_cs_or_redirect(revision, repo_name,
288 286 redirect_after=False)
289 287 if c.cs is None:
290 288 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
291 289
292 290 c.f_path = f_path
293 291
294 292 if r_post:
295 293 unix_mode = 0
296 294 content = convert_line_endings(r_post.get('content'), unix_mode)
297 295
298 296 message = r_post.get('message') or (_('Added %s via RhodeCode')
299 297 % (f_path))
300 298 location = r_post.get('location')
301 299 filename = r_post.get('filename')
302 300 file_obj = r_post.get('upload_file', None)
303 301
304 302 if file_obj is not None and hasattr(file_obj, 'filename'):
305 303 filename = file_obj.filename
306 304 content = file_obj.file
307 305
308 306 node_path = os.path.join(location, filename)
309 307 author = self.rhodecode_user.full_contact
310 308
311 309 if not content:
312 310 h.flash(_('No content'), category='warning')
313 311 return redirect(url('changeset_home', repo_name=c.repo_name,
314 312 revision='tip'))
315 313 if not filename:
316 314 h.flash(_('No filename'), category='warning')
317 315 return redirect(url('changeset_home', repo_name=c.repo_name,
318 316 revision='tip'))
319 317
320 318 try:
321 319 self.scm_model.create_node(repo=c.rhodecode_repo,
322 320 repo_name=repo_name, cs=c.cs,
323 321 user=self.rhodecode_user,
324 322 author=author, message=message,
325 323 content=content, f_path=node_path)
326 324 h.flash(_('Successfully committed to %s' % node_path),
327 325 category='success')
328 326 except NodeAlreadyExistsError, e:
329 327 h.flash(_(e), category='error')
330 328 except Exception:
331 329 log.error(traceback.format_exc())
332 330 h.flash(_('Error occurred during commit'), category='error')
333 331 return redirect(url('changeset_home',
334 332 repo_name=c.repo_name, revision='tip'))
335 333
336 334 return render('files/files_add.html')
337 335
338 336 @LoginRequired()
339 337 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
340 338 'repository.admin')
341 339 def archivefile(self, repo_name, fname):
342 340
343 341 fileformat = None
344 342 revision = None
345 343 ext = None
346 344 subrepos = request.GET.get('subrepos') == 'true'
347 345
348 346 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
349 347 archive_spec = fname.split(ext_data[1])
350 348 if len(archive_spec) == 2 and archive_spec[1] == '':
351 349 fileformat = a_type or ext_data[1]
352 350 revision = archive_spec[0]
353 351 ext = ext_data[1]
354 352
355 353 try:
356 354 dbrepo = RepoModel().get_by_repo_name(repo_name)
357 355 if dbrepo.enable_downloads is False:
358 356 return _('downloads disabled')
359 357
360 358 if c.rhodecode_repo.alias == 'hg':
361 359 # patch and reset hooks section of UI config to not run any
362 360 # hooks on fetching archives with subrepos
363 361 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
364 362 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
365 363
366 364 cs = c.rhodecode_repo.get_changeset(revision)
367 365 content_type = settings.ARCHIVE_SPECS[fileformat][0]
368 366 except ChangesetDoesNotExistError:
369 367 return _('Unknown revision %s') % revision
370 368 except EmptyRepositoryError:
371 369 return _('Empty repository')
372 370 except (ImproperArchiveTypeError, KeyError):
373 371 return _('Unknown archive type')
374 372
375 373 fd, archive = tempfile.mkstemp()
376 374 t = open(archive, 'wb')
377 375 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
378 376 t.close()
379 377
380 378 def get_chunked_archive(archive):
381 379 stream = open(archive, 'rb')
382 380 while True:
383 381 data = stream.read(16 * 1024)
384 382 if not data:
385 383 stream.close()
386 384 os.close(fd)
387 385 os.remove(archive)
388 386 break
389 387 yield data
390 388
391 389 response.content_disposition = str('attachment; filename=%s-%s%s' \
392 390 % (repo_name, revision[:12], ext))
393 391 response.content_type = str(content_type)
394 392 return get_chunked_archive(archive)
395 393
396 394 @LoginRequired()
397 395 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
398 396 'repository.admin')
399 397 def diff(self, repo_name, f_path):
400 398 ignore_whitespace = request.GET.get('ignorews') == '1'
401 399 line_context = request.GET.get('context', 3)
402 400 diff1 = request.GET.get('diff1', '')
403 401 diff2 = request.GET.get('diff2', '')
404 402 c.action = request.GET.get('diff')
405 403 c.no_changes = diff1 == diff2
406 404 c.f_path = f_path
407 405 c.big_diff = False
408 406 c.anchor_url = anchor_url
409 407 c.ignorews_url = _ignorews_url
410 408 c.context_url = _context_url
411 409 c.changes = OrderedDict()
412 410 c.changes[diff2] = []
413 411 try:
414 412 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
415 413 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
416 414 node1 = c.changeset_1.get_node(f_path)
417 415 else:
418 416 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
419 417 node1 = FileNode('.', '', changeset=c.changeset_1)
420 418
421 419 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
422 420 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
423 421 node2 = c.changeset_2.get_node(f_path)
424 422 else:
425 423 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
426 424 node2 = FileNode('.', '', changeset=c.changeset_2)
427 425 except RepositoryError:
428 426 return redirect(url('files_home', repo_name=c.repo_name,
429 427 f_path=f_path))
430 428
431 429 if c.action == 'download':
432 430 _diff = diffs.get_gitdiff(node1, node2,
433 431 ignore_whitespace=ignore_whitespace,
434 432 context=line_context)
435 433 diff = diffs.DiffProcessor(_diff, format='gitdiff')
436 434
437 435 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
438 436 response.content_type = 'text/plain'
439 437 response.content_disposition = (
440 438 'attachment; filename=%s' % diff_name
441 439 )
442 440 return diff.raw_diff()
443 441
444 442 elif c.action == 'raw':
445 443 _diff = diffs.get_gitdiff(node1, node2,
446 444 ignore_whitespace=ignore_whitespace,
447 445 context=line_context)
448 446 diff = diffs.DiffProcessor(_diff, format='gitdiff')
449 447 response.content_type = 'text/plain'
450 448 return diff.raw_diff()
451 449
452 450 else:
453 451 fid = h.FID(diff2, node2.path)
454 452 line_context_lcl = get_line_ctx(fid, request.GET)
455 453 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
456 454
457 455 lim = request.GET.get('fulldiff') or self.cut_off_limit
458 456 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
459 457 filenode_new=node2,
460 458 cut_off_limit=lim,
461 459 ignore_whitespace=ign_whitespace_lcl,
462 460 line_context=line_context_lcl,
463 461 enable_comments=False)
464 462
465 463 c.changes = [('', node2, diff, cs1, cs2, st,)]
466 464
467 465 return render('files/file_diff.html')
468 466
469 467 def _get_node_history(self, cs, f_path, changesets=None):
470 468 if changesets is None:
471 469 changesets = cs.get_file_history(f_path)
472 470 hist_l = []
473 471
474 472 changesets_group = ([], _("Changesets"))
475 473 branches_group = ([], _("Branches"))
476 474 tags_group = ([], _("Tags"))
477 475 _hg = cs.repository.alias == 'hg'
478 476 for chs in changesets:
479 477 _branch = '(%s)' % chs.branch if _hg else ''
480 478 n_desc = 'r%s:%s %s' % (chs.revision, chs.short_id, _branch)
481 479 changesets_group[0].append((chs.raw_id, n_desc,))
482 480
483 481 hist_l.append(changesets_group)
484 482
485 483 for name, chs in c.rhodecode_repo.branches.items():
486 484 branches_group[0].append((chs, name),)
487 485 hist_l.append(branches_group)
488 486
489 487 for name, chs in c.rhodecode_repo.tags.items():
490 488 tags_group[0].append((chs, name),)
491 489 hist_l.append(tags_group)
492 490
493 491 return hist_l
494 492
495 493 @LoginRequired()
496 494 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
497 495 'repository.admin')
498 496 @jsonify
499 497 def nodelist(self, repo_name, revision, f_path):
500 498 if request.environ.get('HTTP_X_PARTIAL_XHR'):
501 499 cs = self.__get_cs_or_redirect(revision, repo_name)
502 500 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
503 501 flat=False)
504 502 return {'nodes': _d + _f}
@@ -1,462 +1,472
1 1 import re
2 2 from itertools import chain
3 3 from dulwich import objects
4 4 from subprocess import Popen, PIPE
5 5 from rhodecode.lib.vcs.conf import settings
6 6 from rhodecode.lib.vcs.exceptions import RepositoryError
7 7 from rhodecode.lib.vcs.exceptions import ChangesetError
8 8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
9 9 from rhodecode.lib.vcs.exceptions import VCSError
10 10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
12 12 from rhodecode.lib.vcs.backends.base import BaseChangeset
13 13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
14 14 RemovedFileNode, SubModuleNode
15 15 from rhodecode.lib.vcs.utils import safe_unicode
16 16 from rhodecode.lib.vcs.utils import date_fromtimestamp
17 17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 from dulwich.objects import Commit, Tag
18 19
19 20
20 21 class GitChangeset(BaseChangeset):
21 22 """
22 23 Represents state of the repository at single revision.
23 24 """
24 25
25 26 def __init__(self, repository, revision):
26 27 self._stat_modes = {}
27 28 self.repository = repository
28 29 self.raw_id = revision
29 self.revision = repository.revisions.index(revision)
30
31 30 self.short_id = self.raw_id[:12]
32 31 self.id = self.raw_id
33 32 try:
34 33 commit = self.repository._repo.get_object(self.raw_id)
35 34 except KeyError:
36 35 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
37 36 self._commit = commit
37
38 if isinstance(commit, Commit):
38 39 self._tree_id = commit.tree
40 self._commiter_property = 'committer'
41 self._date_property = 'commit_time'
42 self._date_tz_property = 'commit_timezone'
43 self.revision = repository.revisions.index(revision)
44 elif isinstance(commit, Tag):
45 self._commiter_property = 'tagger'
46 self._tree_id = commit.id
47 self._date_property = 'tag_time'
48 self._date_tz_property = 'tag_timezone'
49 self.revision = 'tag'
39 50
40 51 self.message = safe_unicode(commit.message)
41 52 #self.branch = None
42 53 self.tags = []
43 54 self.nodes = {}
44 55 self._paths = {}
45 56
46 57 @LazyProperty
47 58 def author(self):
48 return safe_unicode(self._commit.committer)
59 return safe_unicode(getattr(self._commit, self._commiter_property))
49 60
50 61 @LazyProperty
51 62 def date(self):
52 return date_fromtimestamp(self._commit.commit_time,
53 self._commit.commit_timezone)
63 return date_fromtimestamp(getattr(self._commit, self._date_property),
64 getattr(self._commit, self._date_tz_property))
54 65
55 66 @LazyProperty
56 67 def status(self):
57 68 """
58 69 Returns modified, added, removed, deleted files for current changeset
59 70 """
60 71 return self.changed, self.added, self.removed
61 72
62 73 @LazyProperty
63 74 def branch(self):
64 75
65 76 heads = self.repository._heads(reverse=False)
66 77
67 78 ref = heads.get(self.raw_id)
68 79 if ref:
69 80 return safe_unicode(ref)
70 81
71 82 def _fix_path(self, path):
72 83 """
73 84 Paths are stored without trailing slash so we need to get rid off it if
74 85 needed.
75 86 """
76 87 if path.endswith('/'):
77 88 path = path.rstrip('/')
78 89 return path
79 90
80 91 def _get_id_for_path(self, path):
81 92
82 93 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
83 94 if not path in self._paths:
84 95 path = path.strip('/')
85 96 # set root tree
86 tree = self.repository._repo[self._commit.tree]
97 tree = self.repository._repo[self._tree_id]
87 98 if path == '':
88 99 self._paths[''] = tree.id
89 100 return tree.id
90 101 splitted = path.split('/')
91 102 dirs, name = splitted[:-1], splitted[-1]
92 103 curdir = ''
93 104
94 105 # initially extract things from root dir
95 106 for item, stat, id in tree.iteritems():
96 107 if curdir:
97 108 name = '/'.join((curdir, item))
98 109 else:
99 110 name = item
100 111 self._paths[name] = id
101 112 self._stat_modes[name] = stat
102 113
103 114 for dir in dirs:
104 115 if curdir:
105 116 curdir = '/'.join((curdir, dir))
106 117 else:
107 118 curdir = dir
108 119 dir_id = None
109 120 for item, stat, id in tree.iteritems():
110 121 if dir == item:
111 122 dir_id = id
112 123 if dir_id:
113 124 # Update tree
114 125 tree = self.repository._repo[dir_id]
115 126 if not isinstance(tree, objects.Tree):
116 127 raise ChangesetError('%s is not a directory' % curdir)
117 128 else:
118 129 raise ChangesetError('%s have not been found' % curdir)
119 130
120 131 # cache all items from the given traversed tree
121 132 for item, stat, id in tree.iteritems():
122 133 if curdir:
123 134 name = '/'.join((curdir, item))
124 135 else:
125 136 name = item
126 137 self._paths[name] = id
127 138 self._stat_modes[name] = stat
128 139 if not path in self._paths:
129 140 raise NodeDoesNotExistError("There is no file nor directory "
130 141 "at the given path %r at revision %r"
131 142 % (path, self.short_id))
132 143 return self._paths[path]
133 144
134 145 def _get_kind(self, path):
135 id = self._get_id_for_path(path)
136 obj = self.repository._repo[id]
146 obj = self.repository._repo[self._get_id_for_path(path)]
137 147 if isinstance(obj, objects.Blob):
138 148 return NodeKind.FILE
139 149 elif isinstance(obj, objects.Tree):
140 150 return NodeKind.DIR
141 151
142 152 def _get_file_nodes(self):
143 153 return chain(*(t[2] for t in self.walk()))
144 154
145 155 @LazyProperty
146 156 def parents(self):
147 157 """
148 158 Returns list of parents changesets.
149 159 """
150 160 return [self.repository.get_changeset(parent)
151 161 for parent in self._commit.parents]
152 162
153 163 def next(self, branch=None):
154 164
155 165 if branch and self.branch != branch:
156 166 raise VCSError('Branch option used on changeset not belonging '
157 167 'to that branch')
158 168
159 169 def _next(changeset, branch):
160 170 try:
161 171 next_ = changeset.revision + 1
162 172 next_rev = changeset.repository.revisions[next_]
163 173 except IndexError:
164 174 raise ChangesetDoesNotExistError
165 175 cs = changeset.repository.get_changeset(next_rev)
166 176
167 177 if branch and branch != cs.branch:
168 178 return _next(cs, branch)
169 179
170 180 return cs
171 181
172 182 return _next(self, branch)
173 183
174 184 def prev(self, branch=None):
175 185 if branch and self.branch != branch:
176 186 raise VCSError('Branch option used on changeset not belonging '
177 187 'to that branch')
178 188
179 189 def _prev(changeset, branch):
180 190 try:
181 191 prev_ = changeset.revision - 1
182 192 if prev_ < 0:
183 193 raise IndexError
184 194 prev_rev = changeset.repository.revisions[prev_]
185 195 except IndexError:
186 196 raise ChangesetDoesNotExistError
187 197
188 198 cs = changeset.repository.get_changeset(prev_rev)
189 199
190 200 if branch and branch != cs.branch:
191 201 return _prev(cs, branch)
192 202
193 203 return cs
194 204
195 205 return _prev(self, branch)
196 206
197 207 def diff(self, ignore_whitespace=True, context=3):
198 208 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
199 209 rev2 = self
200 210 return ''.join(self.repository.get_diff(rev1, rev2,
201 211 ignore_whitespace=ignore_whitespace,
202 212 context=context))
203 213
204 214 def get_file_mode(self, path):
205 215 """
206 216 Returns stat mode of the file at the given ``path``.
207 217 """
208 218 # ensure path is traversed
209 219 self._get_id_for_path(path)
210 220 return self._stat_modes[path]
211 221
212 222 def get_file_content(self, path):
213 223 """
214 224 Returns content of the file at given ``path``.
215 225 """
216 226 id = self._get_id_for_path(path)
217 227 blob = self.repository._repo[id]
218 228 return blob.as_pretty_string()
219 229
220 230 def get_file_size(self, path):
221 231 """
222 232 Returns size of the file at given ``path``.
223 233 """
224 234 id = self._get_id_for_path(path)
225 235 blob = self.repository._repo[id]
226 236 return blob.raw_length()
227 237
228 238 def get_file_changeset(self, path):
229 239 """
230 240 Returns last commit of the file at the given ``path``.
231 241 """
232 242 node = self.get_node(path)
233 243 return node.history[0]
234 244
235 245 def get_file_history(self, path):
236 246 """
237 247 Returns history of file as reversed list of ``Changeset`` objects for
238 248 which file at given ``path`` has been modified.
239 249
240 250 TODO: This function now uses os underlying 'git' and 'grep' commands
241 251 which is generally not good. Should be replaced with algorithm
242 252 iterating commits.
243 253 """
244 254 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
245 255 self.id, path
246 256 )
247 257 so, se = self.repository.run_git_command(cmd)
248 258 ids = re.findall(r'[0-9a-fA-F]{40}', so)
249 259 return [self.repository.get_changeset(id) for id in ids]
250 260
251 261 def get_file_annotate(self, path):
252 262 """
253 263 Returns a list of three element tuples with lineno,changeset and line
254 264
255 265 TODO: This function now uses os underlying 'git' command which is
256 266 generally not good. Should be replaced with algorithm iterating
257 267 commits.
258 268 """
259 269 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
260 270 # -l ==> outputs long shas (and we need all 40 characters)
261 271 # --root ==> doesn't put '^' character for bounderies
262 272 # -r sha ==> blames for the given revision
263 273 so, se = self.repository.run_git_command(cmd)
264 274
265 275 annotate = []
266 276 for i, blame_line in enumerate(so.split('\n')[:-1]):
267 277 ln_no = i + 1
268 278 id, line = re.split(r' ', blame_line, 1)
269 279 annotate.append((ln_no, self.repository.get_changeset(id), line))
270 280 return annotate
271 281
272 282 def fill_archive(self, stream=None, kind='tgz', prefix=None,
273 283 subrepos=False):
274 284 """
275 285 Fills up given stream.
276 286
277 287 :param stream: file like object.
278 288 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
279 289 Default: ``tgz``.
280 290 :param prefix: name of root directory in archive.
281 291 Default is repository name and changeset's raw_id joined with dash
282 292 (``repo-tip.<KIND>``).
283 293 :param subrepos: include subrepos in this archive.
284 294
285 295 :raise ImproperArchiveTypeError: If given kind is wrong.
286 296 :raise VcsError: If given stream is None
287 297
288 298 """
289 299 allowed_kinds = settings.ARCHIVE_SPECS.keys()
290 300 if kind not in allowed_kinds:
291 301 raise ImproperArchiveTypeError('Archive kind not supported use one'
292 302 'of %s', allowed_kinds)
293 303
294 304 if prefix is None:
295 305 prefix = '%s-%s' % (self.repository.name, self.short_id)
296 306 elif prefix.startswith('/'):
297 307 raise VCSError("Prefix cannot start with leading slash")
298 308 elif prefix.strip() == '':
299 309 raise VCSError("Prefix cannot be empty")
300 310
301 311 if kind == 'zip':
302 312 frmt = 'zip'
303 313 else:
304 314 frmt = 'tar'
305 315 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
306 316 self.raw_id)
307 317 if kind == 'tgz':
308 318 cmd += ' | gzip -9'
309 319 elif kind == 'tbz2':
310 320 cmd += ' | bzip2 -9'
311 321
312 322 if stream is None:
313 323 raise VCSError('You need to pass in a valid stream for filling'
314 324 ' with archival data')
315 325 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
316 326 cwd=self.repository.path)
317 327
318 328 buffer_size = 1024 * 8
319 329 chunk = popen.stdout.read(buffer_size)
320 330 while chunk:
321 331 stream.write(chunk)
322 332 chunk = popen.stdout.read(buffer_size)
323 333 # Make sure all descriptors would be read
324 334 popen.communicate()
325 335
326 336 def get_nodes(self, path):
327 337 if self._get_kind(path) != NodeKind.DIR:
328 338 raise ChangesetError("Directory does not exist for revision %r at "
329 339 " %r" % (self.revision, path))
330 340 path = self._fix_path(path)
331 341 id = self._get_id_for_path(path)
332 342 tree = self.repository._repo[id]
333 343 dirnodes = []
334 344 filenodes = []
335 345 als = self.repository.alias
336 346 for name, stat, id in tree.iteritems():
337 347 if objects.S_ISGITLINK(stat):
338 348 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
339 349 alias=als))
340 350 continue
341 351
342 352 obj = self.repository._repo.get_object(id)
343 353 if path != '':
344 354 obj_path = '/'.join((path, name))
345 355 else:
346 356 obj_path = name
347 357 if obj_path not in self._stat_modes:
348 358 self._stat_modes[obj_path] = stat
349 359 if isinstance(obj, objects.Tree):
350 360 dirnodes.append(DirNode(obj_path, changeset=self))
351 361 elif isinstance(obj, objects.Blob):
352 362 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
353 363 else:
354 364 raise ChangesetError("Requested object should be Tree "
355 365 "or Blob, is %r" % type(obj))
356 366 nodes = dirnodes + filenodes
357 367 for node in nodes:
358 368 if not node.path in self.nodes:
359 369 self.nodes[node.path] = node
360 370 nodes.sort()
361 371 return nodes
362 372
363 373 def get_node(self, path):
364 374 if isinstance(path, unicode):
365 375 path = path.encode('utf-8')
366 376 path = self._fix_path(path)
367 377 if not path in self.nodes:
368 378 try:
369 379 id_ = self._get_id_for_path(path)
370 380 except ChangesetError:
371 381 raise NodeDoesNotExistError("Cannot find one of parents' "
372 382 "directories for a given path: %s" % path)
373 383
374 als = self.repository.alias
375 384 _GL = lambda m: m and objects.S_ISGITLINK(m)
376 385 if _GL(self._stat_modes.get(path)):
377 node = SubModuleNode(path, url=None, changeset=id_, alias=als)
386 node = SubModuleNode(path, url=None, changeset=id_,
387 alias=self.repository.alias)
378 388 else:
379 389 obj = self.repository._repo.get_object(id_)
380 390
381 if isinstance(obj, objects.Tree):
391 if isinstance(obj, (objects.Tree, objects.Tag)):
382 392 if path == '':
383 393 node = RootNode(changeset=self)
384 394 else:
385 395 node = DirNode(path, changeset=self)
386 396 node._tree = obj
387 397 elif isinstance(obj, objects.Blob):
388 398 node = FileNode(path, changeset=self)
389 399 node._blob = obj
390 400 else:
391 401 raise NodeDoesNotExistError("There is no file nor directory "
392 402 "at the given path %r at revision %r"
393 403 % (path, self.short_id))
394 404 # cache node
395 405 self.nodes[path] = node
396 406 return self.nodes[path]
397 407
398 408 @LazyProperty
399 409 def affected_files(self):
400 410 """
401 411 Get's a fast accessible file changes for given changeset
402 412 """
403 413
404 414 return self.added + self.changed
405 415
406 416 @LazyProperty
407 417 def _diff_name_status(self):
408 418 output = []
409 419 for parent in self.parents:
410 420 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
411 421 so, se = self.repository.run_git_command(cmd)
412 422 output.append(so.strip())
413 423 return '\n'.join(output)
414 424
415 425 def _get_paths_for_status(self, status):
416 426 """
417 427 Returns sorted list of paths for given ``status``.
418 428
419 429 :param status: one of: *added*, *modified* or *deleted*
420 430 """
421 431 paths = set()
422 432 char = status[0].upper()
423 433 for line in self._diff_name_status.splitlines():
424 434 if not line:
425 435 continue
426 436
427 437 if line.startswith(char):
428 438 splitted = line.split(char, 1)
429 439 if not len(splitted) == 2:
430 440 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
431 441 "particularly that line: %s" % (self._diff_name_status,
432 442 line))
433 443 _path = splitted[1].strip()
434 444 paths.add(_path)
435 445 return sorted(paths)
436 446
437 447 @LazyProperty
438 448 def added(self):
439 449 """
440 450 Returns list of added ``FileNode`` objects.
441 451 """
442 452 if not self.parents:
443 453 return list(self._get_file_nodes())
444 454 return [self.get_node(path) for path in self._get_paths_for_status('added')]
445 455
446 456 @LazyProperty
447 457 def changed(self):
448 458 """
449 459 Returns list of modified ``FileNode`` objects.
450 460 """
451 461 if not self.parents:
452 462 return []
453 463 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
454 464
455 465 @LazyProperty
456 466 def removed(self):
457 467 """
458 468 Returns list of removed ``FileNode`` objects.
459 469 """
460 470 if not self.parents:
461 471 return []
462 472 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -1,593 +1,599
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.git
4 4 ~~~~~~~~~~~~~~~~
5 5
6 6 Git backend implementation.
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12 import os
13 13 import re
14 14 import time
15 15 import posixpath
16 16 from dulwich.repo import Repo, NotGitRepository
17 17 #from dulwich.config import ConfigFile
18 18 from string import Template
19 19 from subprocess import Popen, PIPE
20 20 from rhodecode.lib.vcs.backends.base import BaseRepository
21 21 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
22 22 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
23 23 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
24 24 from rhodecode.lib.vcs.exceptions import RepositoryError
25 25 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
26 26 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
27 27 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
28 28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29 29 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
30 30 from rhodecode.lib.vcs.utils.paths import abspath
31 31 from rhodecode.lib.vcs.utils.paths import get_user_home
32 32 from .workdir import GitWorkdir
33 33 from .changeset import GitChangeset
34 34 from .inmemory import GitInMemoryChangeset
35 35 from .config import ConfigFile
36 36
37 37
38 38 class GitRepository(BaseRepository):
39 39 """
40 40 Git repository backend.
41 41 """
42 42 DEFAULT_BRANCH_NAME = 'master'
43 43 scm = 'git'
44 44
45 45 def __init__(self, repo_path, create=False, src_url=None,
46 46 update_after_clone=False, bare=False):
47 47
48 48 self.path = abspath(repo_path)
49 49 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
50 50 #temporary set that to now at later we will move it to constructor
51 51 baseui = None
52 52 if baseui is None:
53 53 from mercurial.ui import ui
54 54 baseui = ui()
55 55 # patch the instance of GitRepo with an "FAKE" ui object to add
56 56 # compatibility layer with Mercurial
57 57 setattr(self._repo, 'ui', baseui)
58 58
59 59 try:
60 60 self.head = self._repo.head()
61 61 except KeyError:
62 62 self.head = None
63 63
64 64 self._config_files = [
65 65 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
66 66 'config'),
67 67 abspath(get_user_home(), '.gitconfig'),
68 68 ]
69 69 self.bare = self._repo.bare
70 70
71 71 @LazyProperty
72 72 def revisions(self):
73 73 """
74 74 Returns list of revisions' ids, in ascending order. Being lazy
75 75 attribute allows external tools to inject shas from cache.
76 76 """
77 77 return self._get_all_revisions()
78 78
79 79 def run_git_command(self, cmd):
80 80 """
81 81 Runs given ``cmd`` as git command and returns tuple
82 82 (returncode, stdout, stderr).
83 83
84 84 .. note::
85 85 This method exists only until log/blame functionality is implemented
86 86 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
87 87 os command's output is road to hell...
88 88
89 89 :param cmd: git command to be executed
90 90 """
91 91
92 92 _copts = ['-c', 'core.quotepath=false', ]
93 93 _str_cmd = False
94 94 if isinstance(cmd, basestring):
95 95 cmd = [cmd]
96 96 _str_cmd = True
97 97
98 98 gitenv = os.environ
99 99 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
100 100
101 101 cmd = ['git'] + _copts + cmd
102 102 if _str_cmd:
103 103 cmd = ' '.join(cmd)
104 104 try:
105 105 opts = dict(
106 106 shell=isinstance(cmd, basestring),
107 107 stdout=PIPE,
108 108 stderr=PIPE,
109 109 env=gitenv,
110 110 )
111 111 if os.path.isdir(self.path):
112 112 opts['cwd'] = self.path
113 113 p = Popen(cmd, **opts)
114 114 except OSError, err:
115 115 raise RepositoryError("Couldn't run git command (%s).\n"
116 116 "Original error was:%s" % (cmd, err))
117 117 so, se = p.communicate()
118 118 if not se.startswith("fatal: bad default revision 'HEAD'") and \
119 119 p.returncode != 0:
120 120 raise RepositoryError("Couldn't run git command (%s).\n"
121 121 "stderr:\n%s" % (cmd, se))
122 122 return so, se
123 123
124 124 def _check_url(self, url):
125 125 """
126 126 Functon will check given url and try to verify if it's a valid
127 127 link. Sometimes it may happened that mercurial will issue basic
128 128 auth request that can cause whole API to hang when used from python
129 129 or other external calls.
130 130
131 131 On failures it'll raise urllib2.HTTPError
132 132 """
133 133
134 134 #TODO: implement this
135 135 pass
136 136
137 137 def _get_repo(self, create, src_url=None, update_after_clone=False,
138 138 bare=False):
139 139 if create and os.path.exists(self.path):
140 140 raise RepositoryError("Location already exist")
141 141 if src_url and not create:
142 142 raise RepositoryError("Create should be set to True if src_url is "
143 143 "given (clone operation creates repository)")
144 144 try:
145 145 if create and src_url:
146 146 self._check_url(src_url)
147 147 self.clone(src_url, update_after_clone, bare)
148 148 return Repo(self.path)
149 149 elif create:
150 150 os.mkdir(self.path)
151 151 if bare:
152 152 return Repo.init_bare(self.path)
153 153 else:
154 154 return Repo.init(self.path)
155 155 else:
156 156 return Repo(self.path)
157 157 except (NotGitRepository, OSError), err:
158 158 raise RepositoryError(err)
159 159
160 160 def _get_all_revisions(self):
161 161 cmd = 'rev-list --all --reverse --date-order'
162 162 try:
163 163 so, se = self.run_git_command(cmd)
164 164 except RepositoryError:
165 165 # Can be raised for empty repositories
166 166 return []
167 167 return so.splitlines()
168 168
169 169 def _get_all_revisions2(self):
170 170 #alternate implementation using dulwich
171 171 includes = [x[1][0] for x in self._parsed_refs.iteritems()
172 172 if x[1][1] != 'T']
173 173 return [c.commit.id for c in self._repo.get_walker(include=includes)]
174 174
175 175 def _get_revision(self, revision):
176 176 """
177 177 For git backend we always return integer here. This way we ensure
178 178 that changset's revision attribute would become integer.
179 179 """
180 180 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
181 181 is_bstr = lambda o: isinstance(o, (str, unicode))
182 182 is_null = lambda o: len(o) == revision.count('0')
183 183
184 184 if len(self.revisions) == 0:
185 185 raise EmptyRepositoryError("There are no changesets yet")
186 186
187 187 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
188 188 revision = self.revisions[-1]
189 189
190 190 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
191 191 or isinstance(revision, int) or is_null(revision)):
192 192 try:
193 193 revision = self.revisions[int(revision)]
194 194 except:
195 195 raise ChangesetDoesNotExistError("Revision %r does not exist "
196 196 "for this repository %s" % (revision, self))
197 197
198 198 elif is_bstr(revision):
199 # get by branch/tag name
199 200 _ref_revision = self._parsed_refs.get(revision)
201 _tags_shas = self.tags.values()
200 202 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
201 203 return _ref_revision[0]
202 204
205 # maybe it's a tag ? we don't have them in self.revisions
206 elif revision in _tags_shas:
207 return _tags_shas[_tags_shas.index(revision)]
208
203 209 elif not pattern.match(revision) or revision not in self.revisions:
204 210 raise ChangesetDoesNotExistError("Revision %r does not exist "
205 211 "for this repository %s" % (revision, self))
206 212
207 213 # Ensure we return full id
208 214 if not pattern.match(str(revision)):
209 215 raise ChangesetDoesNotExistError("Given revision %r not recognized"
210 216 % revision)
211 217 return revision
212 218
213 219 def _get_archives(self, archive_name='tip'):
214 220
215 221 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
216 222 yield {"type": i[0], "extension": i[1], "node": archive_name}
217 223
218 224 def _get_url(self, url):
219 225 """
220 226 Returns normalized url. If schema is not given, would fall to
221 227 filesystem (``file:///``) schema.
222 228 """
223 229 url = str(url)
224 230 if url != 'default' and not '://' in url:
225 231 url = ':///'.join(('file', url))
226 232 return url
227 233
228 234 @LazyProperty
229 235 def name(self):
230 236 return os.path.basename(self.path)
231 237
232 238 @LazyProperty
233 239 def last_change(self):
234 240 """
235 241 Returns last change made on this repository as datetime object
236 242 """
237 243 return date_fromtimestamp(self._get_mtime(), makedate()[1])
238 244
239 245 def _get_mtime(self):
240 246 try:
241 247 return time.mktime(self.get_changeset().date.timetuple())
242 248 except RepositoryError:
243 249 idx_loc = '' if self.bare else '.git'
244 250 # fallback to filesystem
245 251 in_path = os.path.join(self.path, idx_loc, "index")
246 252 he_path = os.path.join(self.path, idx_loc, "HEAD")
247 253 if os.path.exists(in_path):
248 254 return os.stat(in_path).st_mtime
249 255 else:
250 256 return os.stat(he_path).st_mtime
251 257
252 258 @LazyProperty
253 259 def description(self):
254 260 idx_loc = '' if self.bare else '.git'
255 261 undefined_description = u'unknown'
256 262 description_path = os.path.join(self.path, idx_loc, 'description')
257 263 if os.path.isfile(description_path):
258 264 return safe_unicode(open(description_path).read())
259 265 else:
260 266 return undefined_description
261 267
262 268 @LazyProperty
263 269 def contact(self):
264 270 undefined_contact = u'Unknown'
265 271 return undefined_contact
266 272
267 273 @property
268 274 def branches(self):
269 275 if not self.revisions:
270 276 return {}
271 277 sortkey = lambda ctx: ctx[0]
272 278 _branches = [(x[0], x[1][0])
273 279 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
274 280 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
275 281
276 282 @LazyProperty
277 283 def tags(self):
278 284 return self._get_tags()
279 285
280 286 def _get_tags(self):
281 287 if not self.revisions:
282 288 return {}
283 289
284 290 sortkey = lambda ctx: ctx[0]
285 291 _tags = [(x[0], x[1][0])
286 292 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
287 293 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
288 294
289 295 def tag(self, name, user, revision=None, message=None, date=None,
290 296 **kwargs):
291 297 """
292 298 Creates and returns a tag for the given ``revision``.
293 299
294 300 :param name: name for new tag
295 301 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
296 302 :param revision: changeset id for which new tag would be created
297 303 :param message: message of the tag's commit
298 304 :param date: date of tag's commit
299 305
300 306 :raises TagAlreadyExistError: if tag with same name already exists
301 307 """
302 308 if name in self.tags:
303 309 raise TagAlreadyExistError("Tag %s already exists" % name)
304 310 changeset = self.get_changeset(revision)
305 311 message = message or "Added tag %s for commit %s" % (name,
306 312 changeset.raw_id)
307 313 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
308 314
309 315 self.tags = self._get_tags()
310 316 return changeset
311 317
312 318 def remove_tag(self, name, user, message=None, date=None):
313 319 """
314 320 Removes tag with the given ``name``.
315 321
316 322 :param name: name of the tag to be removed
317 323 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
318 324 :param message: message of the tag's removal commit
319 325 :param date: date of tag's removal commit
320 326
321 327 :raises TagDoesNotExistError: if tag with given name does not exists
322 328 """
323 329 if name not in self.tags:
324 330 raise TagDoesNotExistError("Tag %s does not exist" % name)
325 331 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
326 332 try:
327 333 os.remove(tagpath)
328 334 self.tags = self._get_tags()
329 335 except OSError, e:
330 336 raise RepositoryError(e.strerror)
331 337
332 338 @LazyProperty
333 339 def _parsed_refs(self):
334 340 refs = self._repo.get_refs()
335 341 keys = [('refs/heads/', 'H'),
336 342 ('refs/remotes/origin/', 'RH'),
337 343 ('refs/tags/', 'T')]
338 344 _refs = {}
339 345 for ref, sha in refs.iteritems():
340 346 for k, type_ in keys:
341 347 if ref.startswith(k):
342 348 _key = ref[len(k):]
343 349 _refs[_key] = [sha, type_]
344 350 break
345 351 return _refs
346 352
347 353 def _heads(self, reverse=False):
348 354 refs = self._repo.get_refs()
349 355 heads = {}
350 356
351 357 for key, val in refs.items():
352 358 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
353 359 if key.startswith(ref_key):
354 360 n = key[len(ref_key):]
355 361 if n not in ['HEAD']:
356 362 heads[n] = val
357 363
358 364 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
359 365
360 366 def get_changeset(self, revision=None):
361 367 """
362 368 Returns ``GitChangeset`` object representing commit from git repository
363 369 at the given revision or head (most recent commit) if None given.
364 370 """
365 371 if isinstance(revision, GitChangeset):
366 372 return revision
367 373 revision = self._get_revision(revision)
368 374 changeset = GitChangeset(repository=self, revision=revision)
369 375 return changeset
370 376
371 377 def get_changesets(self, start=None, end=None, start_date=None,
372 378 end_date=None, branch_name=None, reverse=False):
373 379 """
374 380 Returns iterator of ``GitChangeset`` objects from start to end (both
375 381 are inclusive), in ascending date order (unless ``reverse`` is set).
376 382
377 383 :param start: changeset ID, as str; first returned changeset
378 384 :param end: changeset ID, as str; last returned changeset
379 385 :param start_date: if specified, changesets with commit date less than
380 386 ``start_date`` would be filtered out from returned set
381 387 :param end_date: if specified, changesets with commit date greater than
382 388 ``end_date`` would be filtered out from returned set
383 389 :param branch_name: if specified, changesets not reachable from given
384 390 branch would be filtered out from returned set
385 391 :param reverse: if ``True``, returned generator would be reversed
386 392 (meaning that returned changesets would have descending date order)
387 393
388 394 :raise BranchDoesNotExistError: If given ``branch_name`` does not
389 395 exist.
390 396 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
391 397 ``end`` could not be found.
392 398
393 399 """
394 400 if branch_name and branch_name not in self.branches:
395 401 raise BranchDoesNotExistError("Branch '%s' not found" \
396 402 % branch_name)
397 403 # %H at format means (full) commit hash, initial hashes are retrieved
398 404 # in ascending date order
399 405 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
400 406 cmd_params = {}
401 407 if start_date:
402 408 cmd_template += ' --since "$since"'
403 409 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
404 410 if end_date:
405 411 cmd_template += ' --until "$until"'
406 412 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
407 413 if branch_name:
408 414 cmd_template += ' $branch_name'
409 415 cmd_params['branch_name'] = branch_name
410 416 else:
411 417 cmd_template += ' --all'
412 418
413 419 cmd = Template(cmd_template).safe_substitute(**cmd_params)
414 420 revs = self.run_git_command(cmd)[0].splitlines()
415 421 start_pos = 0
416 422 end_pos = len(revs)
417 423 if start:
418 424 _start = self._get_revision(start)
419 425 try:
420 426 start_pos = revs.index(_start)
421 427 except ValueError:
422 428 pass
423 429
424 430 if end is not None:
425 431 _end = self._get_revision(end)
426 432 try:
427 433 end_pos = revs.index(_end)
428 434 except ValueError:
429 435 pass
430 436
431 437 if None not in [start, end] and start_pos > end_pos:
432 438 raise RepositoryError('start cannot be after end')
433 439
434 440 if end_pos is not None:
435 441 end_pos += 1
436 442
437 443 revs = revs[start_pos:end_pos]
438 444 if reverse:
439 445 revs = reversed(revs)
440 446 for rev in revs:
441 447 yield self.get_changeset(rev)
442 448
443 449 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
444 450 context=3):
445 451 """
446 452 Returns (git like) *diff*, as plain text. Shows changes introduced by
447 453 ``rev2`` since ``rev1``.
448 454
449 455 :param rev1: Entry point from which diff is shown. Can be
450 456 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
451 457 the changes since empty state of the repository until ``rev2``
452 458 :param rev2: Until which revision changes should be shown.
453 459 :param ignore_whitespace: If set to ``True``, would not show whitespace
454 460 changes. Defaults to ``False``.
455 461 :param context: How many lines before/after changed lines should be
456 462 shown. Defaults to ``3``.
457 463 """
458 464 flags = ['-U%s' % context]
459 465 if ignore_whitespace:
460 466 flags.append('-w')
461 467
462 468 if hasattr(rev1, 'raw_id'):
463 469 rev1 = getattr(rev1, 'raw_id')
464 470
465 471 if hasattr(rev2, 'raw_id'):
466 472 rev2 = getattr(rev2, 'raw_id')
467 473
468 474 if rev1 == self.EMPTY_CHANGESET:
469 475 rev2 = self.get_changeset(rev2).raw_id
470 476 cmd = ' '.join(['show'] + flags + [rev2])
471 477 else:
472 478 rev1 = self.get_changeset(rev1).raw_id
473 479 rev2 = self.get_changeset(rev2).raw_id
474 480 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
475 481
476 482 if path:
477 483 cmd += ' -- "%s"' % path
478 484 stdout, stderr = self.run_git_command(cmd)
479 485 # If we used 'show' command, strip first few lines (until actual diff
480 486 # starts)
481 487 if rev1 == self.EMPTY_CHANGESET:
482 488 lines = stdout.splitlines()
483 489 x = 0
484 490 for line in lines:
485 491 if line.startswith('diff'):
486 492 break
487 493 x += 1
488 494 # Append new line just like 'diff' command do
489 495 stdout = '\n'.join(lines[x:]) + '\n'
490 496 return stdout
491 497
492 498 @LazyProperty
493 499 def in_memory_changeset(self):
494 500 """
495 501 Returns ``GitInMemoryChangeset`` object for this repository.
496 502 """
497 503 return GitInMemoryChangeset(self)
498 504
499 505 def clone(self, url, update_after_clone=True, bare=False):
500 506 """
501 507 Tries to clone changes from external location.
502 508
503 509 :param update_after_clone: If set to ``False``, git won't checkout
504 510 working directory
505 511 :param bare: If set to ``True``, repository would be cloned into
506 512 *bare* git repository (no working directory at all).
507 513 """
508 514 url = self._get_url(url)
509 515 cmd = ['clone']
510 516 if bare:
511 517 cmd.append('--bare')
512 518 elif not update_after_clone:
513 519 cmd.append('--no-checkout')
514 520 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
515 521 cmd = ' '.join(cmd)
516 522 # If error occurs run_git_command raises RepositoryError already
517 523 self.run_git_command(cmd)
518 524
519 525 def pull(self, url):
520 526 """
521 527 Tries to pull changes from external location.
522 528 """
523 529 url = self._get_url(url)
524 530 cmd = ['pull']
525 531 cmd.append("--ff-only")
526 532 cmd.append(url)
527 533 cmd = ' '.join(cmd)
528 534 # If error occurs run_git_command raises RepositoryError already
529 535 self.run_git_command(cmd)
530 536
531 537 def fetch(self, url):
532 538 """
533 539 Tries to pull changes from external location.
534 540 """
535 541 url = self._get_url(url)
536 542 cmd = ['fetch']
537 543 cmd.append(url)
538 544 cmd = ' '.join(cmd)
539 545 # If error occurs run_git_command raises RepositoryError already
540 546 self.run_git_command(cmd)
541 547
542 548 @LazyProperty
543 549 def workdir(self):
544 550 """
545 551 Returns ``Workdir`` instance for this repository.
546 552 """
547 553 return GitWorkdir(self)
548 554
549 555 def get_config_value(self, section, name, config_file=None):
550 556 """
551 557 Returns configuration value for a given [``section``] and ``name``.
552 558
553 559 :param section: Section we want to retrieve value from
554 560 :param name: Name of configuration we want to retrieve
555 561 :param config_file: A path to file which should be used to retrieve
556 562 configuration from (might also be a list of file paths)
557 563 """
558 564 if config_file is None:
559 565 config_file = []
560 566 elif isinstance(config_file, basestring):
561 567 config_file = [config_file]
562 568
563 569 def gen_configs():
564 570 for path in config_file + self._config_files:
565 571 try:
566 572 yield ConfigFile.from_path(path)
567 573 except (IOError, OSError, ValueError):
568 574 continue
569 575
570 576 for config in gen_configs():
571 577 try:
572 578 return config.get(section, name)
573 579 except KeyError:
574 580 continue
575 581 return None
576 582
577 583 def get_user_name(self, config_file=None):
578 584 """
579 585 Returns user's name from global configuration file.
580 586
581 587 :param config_file: A path to file which should be used to retrieve
582 588 configuration from (might also be a list of file paths)
583 589 """
584 590 return self.get_config_value('user', 'name', config_file)
585 591
586 592 def get_user_email(self, config_file=None):
587 593 """
588 594 Returns user's email from global configuration file.
589 595
590 596 :param config_file: A path to file which should be used to retrieve
591 597 configuration from (might also be a list of file paths)
592 598 """
593 599 return self.get_config_value('user', 'email', config_file)
General Comments 0
You need to be logged in to leave comments. Login now