##// END OF EJS Templates
Invalidate gits parsed_refs cache after commit, tagging or tag remove
marcink -
r2539:da18c423 beta
parent child Browse files
Show More
@@ -1,194 +1,196 b''
1 1 import time
2 2 import datetime
3 3 import posixpath
4 4 from dulwich import objects
5 5 from dulwich.repo import Repo
6 6 from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset
7 7 from rhodecode.lib.vcs.exceptions import RepositoryError
8 8 from rhodecode.lib.vcs.utils import safe_str
9 9
10 10
11 11 class GitInMemoryChangeset(BaseInMemoryChangeset):
12 12
13 13 def commit(self, message, author, parents=None, branch=None, date=None,
14 14 **kwargs):
15 15 """
16 16 Performs in-memory commit (doesn't check workdir in any way) and
17 17 returns newly created ``Changeset``. Updates repository's
18 18 ``revisions``.
19 19
20 20 :param message: message of the commit
21 21 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
22 22 :param parents: single parent or sequence of parents from which commit
23 23 would be derieved
24 24 :param date: ``datetime.datetime`` instance. Defaults to
25 25 ``datetime.datetime.now()``.
26 26 :param branch: branch name, as string. If none given, default backend's
27 27 branch would be used.
28 28
29 29 :raises ``CommitError``: if any error occurs while committing
30 30 """
31 31 self.check_integrity(parents)
32 32
33 33 from .repository import GitRepository
34 34 if branch is None:
35 35 branch = GitRepository.DEFAULT_BRANCH_NAME
36 36
37 37 repo = self.repository._repo
38 38 object_store = repo.object_store
39 39
40 40 ENCODING = "UTF-8"
41 41 DIRMOD = 040000
42 42
43 43 # Create tree and populates it with blobs
44 44 commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or\
45 45 objects.Tree()
46 46 for node in self.added + self.changed:
47 47 # Compute subdirs if needed
48 48 dirpath, nodename = posixpath.split(node.path)
49 49 dirnames = dirpath and dirpath.split('/') or []
50 50 parent = commit_tree
51 51 ancestors = [('', parent)]
52 52
53 53 # Tries to dig for the deepest existing tree
54 54 while dirnames:
55 55 curdir = dirnames.pop(0)
56 56 try:
57 57 dir_id = parent[curdir][1]
58 58 except KeyError:
59 59 # put curdir back into dirnames and stops
60 60 dirnames.insert(0, curdir)
61 61 break
62 62 else:
63 63 # If found, updates parent
64 64 parent = self.repository._repo[dir_id]
65 65 ancestors.append((curdir, parent))
66 66 # Now parent is deepest exising tree and we need to create subtrees
67 67 # for dirnames (in reverse order) [this only applies for nodes from added]
68 68 new_trees = []
69 69 blob = objects.Blob.from_string(node.content.encode(ENCODING))
70 70 node_path = node.name.encode(ENCODING)
71 71 if dirnames:
72 72 # If there are trees which should be created we need to build
73 73 # them now (in reverse order)
74 74 reversed_dirnames = list(reversed(dirnames))
75 75 curtree = objects.Tree()
76 76 curtree[node_path] = node.mode, blob.id
77 77 new_trees.append(curtree)
78 78 for dirname in reversed_dirnames[:-1]:
79 79 newtree = objects.Tree()
80 80 #newtree.add(DIRMOD, dirname, curtree.id)
81 81 newtree[dirname] = DIRMOD, curtree.id
82 82 new_trees.append(newtree)
83 83 curtree = newtree
84 84 parent[reversed_dirnames[-1]] = DIRMOD, curtree.id
85 85 else:
86 86 parent.add(name=node_path, mode=node.mode, hexsha=blob.id)
87 87
88 88 new_trees.append(parent)
89 89 # Update ancestors
90 90 for parent, tree, path in reversed([(a[1], b[1], b[0]) for a, b in
91 91 zip(ancestors, ancestors[1:])]):
92 92 parent[path] = DIRMOD, tree.id
93 93 object_store.add_object(tree)
94 94
95 95 object_store.add_object(blob)
96 96 for tree in new_trees:
97 97 object_store.add_object(tree)
98 98 for node in self.removed:
99 99 paths = node.path.split('/')
100 100 tree = commit_tree
101 101 trees = [tree]
102 102 # Traverse deep into the forest...
103 103 for path in paths:
104 104 try:
105 105 obj = self.repository._repo[tree[path][1]]
106 106 if isinstance(obj, objects.Tree):
107 107 trees.append(obj)
108 108 tree = obj
109 109 except KeyError:
110 110 break
111 111 # Cut down the blob and all rotten trees on the way back...
112 112 for path, tree in reversed(zip(paths, trees)):
113 113 del tree[path]
114 114 if tree:
115 115 # This tree still has elements - don't remove it or any
116 116 # of it's parents
117 117 break
118 118
119 119 object_store.add_object(commit_tree)
120 120
121 121 # Create commit
122 122 commit = objects.Commit()
123 123 commit.tree = commit_tree.id
124 124 commit.parents = [p._commit.id for p in self.parents if p]
125 125 commit.author = commit.committer = safe_str(author)
126 126 commit.encoding = ENCODING
127 127 commit.message = safe_str(message)
128 128
129 129 # Compute date
130 130 if date is None:
131 131 date = time.time()
132 132 elif isinstance(date, datetime.datetime):
133 133 date = time.mktime(date.timetuple())
134 134
135 135 author_time = kwargs.pop('author_time', date)
136 136 commit.commit_time = int(date)
137 137 commit.author_time = int(author_time)
138 138 tz = time.timezone
139 139 author_tz = kwargs.pop('author_timezone', tz)
140 140 commit.commit_timezone = tz
141 141 commit.author_timezone = author_tz
142 142
143 143 object_store.add_object(commit)
144 144
145 145 ref = 'refs/heads/%s' % branch
146 146 repo.refs[ref] = commit.id
147 147 repo.refs.set_symbolic_ref('HEAD', ref)
148 148
149 149 # Update vcs repository object & recreate dulwich repo
150 150 self.repository.revisions.append(commit.id)
151 151 self.repository._repo = Repo(self.repository.path)
152 # invalidate parsed refs after commit
153 self.repository._parsed_refs = self.repository._get_parsed_refs()
152 154 tip = self.repository.get_changeset()
153 155 self.reset()
154 156 return tip
155 157
156 158 def _get_missing_trees(self, path, root_tree):
157 159 """
158 160 Creates missing ``Tree`` objects for the given path.
159 161
160 162 :param path: path given as a string. It may be a path to a file node
161 163 (i.e. ``foo/bar/baz.txt``) or directory path - in that case it must
162 164 end with slash (i.e. ``foo/bar/``).
163 165 :param root_tree: ``dulwich.objects.Tree`` object from which we start
164 166 traversing (should be commit's root tree)
165 167 """
166 168 dirpath = posixpath.split(path)[0]
167 169 dirs = dirpath.split('/')
168 170 if not dirs or dirs == ['']:
169 171 return []
170 172
171 173 def get_tree_for_dir(tree, dirname):
172 174 for name, mode, id in tree.iteritems():
173 175 if name == dirname:
174 176 obj = self.repository._repo[id]
175 177 if isinstance(obj, objects.Tree):
176 178 return obj
177 179 else:
178 180 raise RepositoryError("Cannot create directory %s "
179 181 "at tree %s as path is occupied and is not a "
180 182 "Tree" % (dirname, tree))
181 183 return None
182 184
183 185 trees = []
184 186 parent = root_tree
185 187 for dirname in dirs:
186 188 tree = get_tree_for_dir(parent, dirname)
187 189 if tree is None:
188 190 tree = objects.Tree()
189 191 dirmode = 040000
190 192 parent.add(dirmode, dirname, tree.id)
191 193 parent = tree
192 194 # Always append tree
193 195 trees.append(tree)
194 196 return trees
@@ -1,599 +1,604 b''
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 199 # get by branch/tag name
200 200 _ref_revision = self._parsed_refs.get(revision)
201 201 _tags_shas = self.tags.values()
202 202 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
203 203 return _ref_revision[0]
204 204
205 205 # maybe it's a tag ? we don't have them in self.revisions
206 206 elif revision in _tags_shas:
207 207 return _tags_shas[_tags_shas.index(revision)]
208 208
209 209 elif not pattern.match(revision) or revision not in self.revisions:
210 210 raise ChangesetDoesNotExistError("Revision %r does not exist "
211 211 "for this repository %s" % (revision, self))
212 212
213 213 # Ensure we return full id
214 214 if not pattern.match(str(revision)):
215 215 raise ChangesetDoesNotExistError("Given revision %r not recognized"
216 216 % revision)
217 217 return revision
218 218
219 219 def _get_archives(self, archive_name='tip'):
220 220
221 221 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
222 222 yield {"type": i[0], "extension": i[1], "node": archive_name}
223 223
224 224 def _get_url(self, url):
225 225 """
226 226 Returns normalized url. If schema is not given, would fall to
227 227 filesystem (``file:///``) schema.
228 228 """
229 229 url = str(url)
230 230 if url != 'default' and not '://' in url:
231 231 url = ':///'.join(('file', url))
232 232 return url
233 233
234 234 @LazyProperty
235 235 def name(self):
236 236 return os.path.basename(self.path)
237 237
238 238 @LazyProperty
239 239 def last_change(self):
240 240 """
241 241 Returns last change made on this repository as datetime object
242 242 """
243 243 return date_fromtimestamp(self._get_mtime(), makedate()[1])
244 244
245 245 def _get_mtime(self):
246 246 try:
247 247 return time.mktime(self.get_changeset().date.timetuple())
248 248 except RepositoryError:
249 249 idx_loc = '' if self.bare else '.git'
250 250 # fallback to filesystem
251 251 in_path = os.path.join(self.path, idx_loc, "index")
252 252 he_path = os.path.join(self.path, idx_loc, "HEAD")
253 253 if os.path.exists(in_path):
254 254 return os.stat(in_path).st_mtime
255 255 else:
256 256 return os.stat(he_path).st_mtime
257 257
258 258 @LazyProperty
259 259 def description(self):
260 260 idx_loc = '' if self.bare else '.git'
261 261 undefined_description = u'unknown'
262 262 description_path = os.path.join(self.path, idx_loc, 'description')
263 263 if os.path.isfile(description_path):
264 264 return safe_unicode(open(description_path).read())
265 265 else:
266 266 return undefined_description
267 267
268 268 @LazyProperty
269 269 def contact(self):
270 270 undefined_contact = u'Unknown'
271 271 return undefined_contact
272 272
273 273 @property
274 274 def branches(self):
275 275 if not self.revisions:
276 276 return {}
277 277 sortkey = lambda ctx: ctx[0]
278 278 _branches = [(x[0], x[1][0])
279 279 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
280 280 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
281 281
282 282 @LazyProperty
283 283 def tags(self):
284 284 return self._get_tags()
285 285
286 286 def _get_tags(self):
287 287 if not self.revisions:
288 288 return {}
289 289
290 290 sortkey = lambda ctx: ctx[0]
291 291 _tags = [(x[0], x[1][0])
292 292 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
293 293 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
294 294
295 295 def tag(self, name, user, revision=None, message=None, date=None,
296 296 **kwargs):
297 297 """
298 298 Creates and returns a tag for the given ``revision``.
299 299
300 300 :param name: name for new tag
301 301 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
302 302 :param revision: changeset id for which new tag would be created
303 303 :param message: message of the tag's commit
304 304 :param date: date of tag's commit
305 305
306 306 :raises TagAlreadyExistError: if tag with same name already exists
307 307 """
308 308 if name in self.tags:
309 309 raise TagAlreadyExistError("Tag %s already exists" % name)
310 310 changeset = self.get_changeset(revision)
311 311 message = message or "Added tag %s for commit %s" % (name,
312 312 changeset.raw_id)
313 313 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
314 314
315 self._parsed_refs = self._get_parsed_refs()
315 316 self.tags = self._get_tags()
316 317 return changeset
317 318
318 319 def remove_tag(self, name, user, message=None, date=None):
319 320 """
320 321 Removes tag with the given ``name``.
321 322
322 323 :param name: name of the tag to be removed
323 324 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
324 325 :param message: message of the tag's removal commit
325 326 :param date: date of tag's removal commit
326 327
327 328 :raises TagDoesNotExistError: if tag with given name does not exists
328 329 """
329 330 if name not in self.tags:
330 331 raise TagDoesNotExistError("Tag %s does not exist" % name)
331 332 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
332 333 try:
333 334 os.remove(tagpath)
335 self._parsed_refs = self._get_parsed_refs()
334 336 self.tags = self._get_tags()
335 337 except OSError, e:
336 338 raise RepositoryError(e.strerror)
337 339
338 340 @LazyProperty
339 341 def _parsed_refs(self):
342 return self._get_parsed_refs()
343
344 def _get_parsed_refs(self):
340 345 refs = self._repo.get_refs()
341 346 keys = [('refs/heads/', 'H'),
342 347 ('refs/remotes/origin/', 'RH'),
343 348 ('refs/tags/', 'T')]
344 349 _refs = {}
345 350 for ref, sha in refs.iteritems():
346 351 for k, type_ in keys:
347 352 if ref.startswith(k):
348 353 _key = ref[len(k):]
349 354 _refs[_key] = [sha, type_]
350 355 break
351 356 return _refs
352 357
353 358 def _heads(self, reverse=False):
354 359 refs = self._repo.get_refs()
355 360 heads = {}
356 361
357 362 for key, val in refs.items():
358 363 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
359 364 if key.startswith(ref_key):
360 365 n = key[len(ref_key):]
361 366 if n not in ['HEAD']:
362 367 heads[n] = val
363 368
364 369 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
365 370
366 371 def get_changeset(self, revision=None):
367 372 """
368 373 Returns ``GitChangeset`` object representing commit from git repository
369 374 at the given revision or head (most recent commit) if None given.
370 375 """
371 376 if isinstance(revision, GitChangeset):
372 377 return revision
373 378 revision = self._get_revision(revision)
374 379 changeset = GitChangeset(repository=self, revision=revision)
375 380 return changeset
376 381
377 382 def get_changesets(self, start=None, end=None, start_date=None,
378 383 end_date=None, branch_name=None, reverse=False):
379 384 """
380 385 Returns iterator of ``GitChangeset`` objects from start to end (both
381 386 are inclusive), in ascending date order (unless ``reverse`` is set).
382 387
383 388 :param start: changeset ID, as str; first returned changeset
384 389 :param end: changeset ID, as str; last returned changeset
385 390 :param start_date: if specified, changesets with commit date less than
386 391 ``start_date`` would be filtered out from returned set
387 392 :param end_date: if specified, changesets with commit date greater than
388 393 ``end_date`` would be filtered out from returned set
389 394 :param branch_name: if specified, changesets not reachable from given
390 395 branch would be filtered out from returned set
391 396 :param reverse: if ``True``, returned generator would be reversed
392 397 (meaning that returned changesets would have descending date order)
393 398
394 399 :raise BranchDoesNotExistError: If given ``branch_name`` does not
395 400 exist.
396 401 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
397 402 ``end`` could not be found.
398 403
399 404 """
400 405 if branch_name and branch_name not in self.branches:
401 406 raise BranchDoesNotExistError("Branch '%s' not found" \
402 407 % branch_name)
403 408 # %H at format means (full) commit hash, initial hashes are retrieved
404 409 # in ascending date order
405 410 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
406 411 cmd_params = {}
407 412 if start_date:
408 413 cmd_template += ' --since "$since"'
409 414 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
410 415 if end_date:
411 416 cmd_template += ' --until "$until"'
412 417 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
413 418 if branch_name:
414 419 cmd_template += ' $branch_name'
415 420 cmd_params['branch_name'] = branch_name
416 421 else:
417 422 cmd_template += ' --all'
418 423
419 424 cmd = Template(cmd_template).safe_substitute(**cmd_params)
420 425 revs = self.run_git_command(cmd)[0].splitlines()
421 426 start_pos = 0
422 427 end_pos = len(revs)
423 428 if start:
424 429 _start = self._get_revision(start)
425 430 try:
426 431 start_pos = revs.index(_start)
427 432 except ValueError:
428 433 pass
429 434
430 435 if end is not None:
431 436 _end = self._get_revision(end)
432 437 try:
433 438 end_pos = revs.index(_end)
434 439 except ValueError:
435 440 pass
436 441
437 442 if None not in [start, end] and start_pos > end_pos:
438 443 raise RepositoryError('start cannot be after end')
439 444
440 445 if end_pos is not None:
441 446 end_pos += 1
442 447
443 448 revs = revs[start_pos:end_pos]
444 449 if reverse:
445 450 revs = reversed(revs)
446 451 for rev in revs:
447 452 yield self.get_changeset(rev)
448 453
449 454 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
450 455 context=3):
451 456 """
452 457 Returns (git like) *diff*, as plain text. Shows changes introduced by
453 458 ``rev2`` since ``rev1``.
454 459
455 460 :param rev1: Entry point from which diff is shown. Can be
456 461 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
457 462 the changes since empty state of the repository until ``rev2``
458 463 :param rev2: Until which revision changes should be shown.
459 464 :param ignore_whitespace: If set to ``True``, would not show whitespace
460 465 changes. Defaults to ``False``.
461 466 :param context: How many lines before/after changed lines should be
462 467 shown. Defaults to ``3``.
463 468 """
464 469 flags = ['-U%s' % context]
465 470 if ignore_whitespace:
466 471 flags.append('-w')
467 472
468 473 if hasattr(rev1, 'raw_id'):
469 474 rev1 = getattr(rev1, 'raw_id')
470 475
471 476 if hasattr(rev2, 'raw_id'):
472 477 rev2 = getattr(rev2, 'raw_id')
473 478
474 479 if rev1 == self.EMPTY_CHANGESET:
475 480 rev2 = self.get_changeset(rev2).raw_id
476 481 cmd = ' '.join(['show'] + flags + [rev2])
477 482 else:
478 483 rev1 = self.get_changeset(rev1).raw_id
479 484 rev2 = self.get_changeset(rev2).raw_id
480 485 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
481 486
482 487 if path:
483 488 cmd += ' -- "%s"' % path
484 489 stdout, stderr = self.run_git_command(cmd)
485 490 # If we used 'show' command, strip first few lines (until actual diff
486 491 # starts)
487 492 if rev1 == self.EMPTY_CHANGESET:
488 493 lines = stdout.splitlines()
489 494 x = 0
490 495 for line in lines:
491 496 if line.startswith('diff'):
492 497 break
493 498 x += 1
494 499 # Append new line just like 'diff' command do
495 500 stdout = '\n'.join(lines[x:]) + '\n'
496 501 return stdout
497 502
498 503 @LazyProperty
499 504 def in_memory_changeset(self):
500 505 """
501 506 Returns ``GitInMemoryChangeset`` object for this repository.
502 507 """
503 508 return GitInMemoryChangeset(self)
504 509
505 510 def clone(self, url, update_after_clone=True, bare=False):
506 511 """
507 512 Tries to clone changes from external location.
508 513
509 514 :param update_after_clone: If set to ``False``, git won't checkout
510 515 working directory
511 516 :param bare: If set to ``True``, repository would be cloned into
512 517 *bare* git repository (no working directory at all).
513 518 """
514 519 url = self._get_url(url)
515 520 cmd = ['clone']
516 521 if bare:
517 522 cmd.append('--bare')
518 523 elif not update_after_clone:
519 524 cmd.append('--no-checkout')
520 525 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
521 526 cmd = ' '.join(cmd)
522 527 # If error occurs run_git_command raises RepositoryError already
523 528 self.run_git_command(cmd)
524 529
525 530 def pull(self, url):
526 531 """
527 532 Tries to pull changes from external location.
528 533 """
529 534 url = self._get_url(url)
530 535 cmd = ['pull']
531 536 cmd.append("--ff-only")
532 537 cmd.append(url)
533 538 cmd = ' '.join(cmd)
534 539 # If error occurs run_git_command raises RepositoryError already
535 540 self.run_git_command(cmd)
536 541
537 542 def fetch(self, url):
538 543 """
539 544 Tries to pull changes from external location.
540 545 """
541 546 url = self._get_url(url)
542 547 cmd = ['fetch']
543 548 cmd.append(url)
544 549 cmd = ' '.join(cmd)
545 550 # If error occurs run_git_command raises RepositoryError already
546 551 self.run_git_command(cmd)
547 552
548 553 @LazyProperty
549 554 def workdir(self):
550 555 """
551 556 Returns ``Workdir`` instance for this repository.
552 557 """
553 558 return GitWorkdir(self)
554 559
555 560 def get_config_value(self, section, name, config_file=None):
556 561 """
557 562 Returns configuration value for a given [``section``] and ``name``.
558 563
559 564 :param section: Section we want to retrieve value from
560 565 :param name: Name of configuration we want to retrieve
561 566 :param config_file: A path to file which should be used to retrieve
562 567 configuration from (might also be a list of file paths)
563 568 """
564 569 if config_file is None:
565 570 config_file = []
566 571 elif isinstance(config_file, basestring):
567 572 config_file = [config_file]
568 573
569 574 def gen_configs():
570 575 for path in config_file + self._config_files:
571 576 try:
572 577 yield ConfigFile.from_path(path)
573 578 except (IOError, OSError, ValueError):
574 579 continue
575 580
576 581 for config in gen_configs():
577 582 try:
578 583 return config.get(section, name)
579 584 except KeyError:
580 585 continue
581 586 return None
582 587
583 588 def get_user_name(self, config_file=None):
584 589 """
585 590 Returns user's name from global configuration file.
586 591
587 592 :param config_file: A path to file which should be used to retrieve
588 593 configuration from (might also be a list of file paths)
589 594 """
590 595 return self.get_config_value('user', 'name', config_file)
591 596
592 597 def get_user_email(self, config_file=None):
593 598 """
594 599 Returns user's email from global configuration file.
595 600
596 601 :param config_file: A path to file which should be used to retrieve
597 602 configuration from (might also be a list of file paths)
598 603 """
599 604 return self.get_config_value('user', 'email', config_file)
General Comments 0
You need to be logged in to leave comments. Login now