##// END OF EJS Templates
changelog pagination with branch filtering now uses...
marcink -
r3747:600ffde2 beta
parent child Browse files
Show More
@@ -1,125 +1,123 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.changelog
3 rhodecode.controllers.changelog
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 changelog controller for rhodecode
6 changelog controller for rhodecode
7
7
8 :created_on: Apr 21, 2010
8 :created_on: Apr 21, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 from pylons import request, url, session, tmpl_context as c
29 from pylons import request, url, session, tmpl_context as c
30 from pylons.controllers.util import redirect
30 from pylons.controllers.util import redirect
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32
32
33 import rhodecode.lib.helpers as h
33 import rhodecode.lib.helpers as h
34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 from rhodecode.lib.base import BaseRepoController, render
35 from rhodecode.lib.base import BaseRepoController, render
36 from rhodecode.lib.helpers import RepoPage
36 from rhodecode.lib.helpers import RepoPage
37 from rhodecode.lib.compat import json
37 from rhodecode.lib.compat import json
38 from rhodecode.lib.graphmod import _colored, _dagwalker
38 from rhodecode.lib.graphmod import _colored, _dagwalker
39 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
39 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
40 from rhodecode.lib.utils2 import safe_int
40 from rhodecode.lib.utils2 import safe_int
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 class ChangelogController(BaseRepoController):
45 class ChangelogController(BaseRepoController):
46
46
47 @LoginRequired()
47 @LoginRequired()
48 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
48 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
49 'repository.admin')
49 'repository.admin')
50 def __before__(self):
50 def __before__(self):
51 super(ChangelogController, self).__before__()
51 super(ChangelogController, self).__before__()
52 c.affected_files_cut_off = 60
52 c.affected_files_cut_off = 60
53
53
54 def index(self):
54 def index(self):
55 limit = 100
55 limit = 100
56 default = 20
56 default = 20
57 if request.params.get('size'):
57 if request.params.get('size'):
58 try:
58 try:
59 int_size = int(request.params.get('size'))
59 int_size = int(request.params.get('size'))
60 except ValueError:
60 except ValueError:
61 int_size = default
61 int_size = default
62 c.size = max(min(int_size, limit), 1)
62 c.size = max(min(int_size, limit), 1)
63 session['changelog_size'] = c.size
63 session['changelog_size'] = c.size
64 session.save()
64 session.save()
65 else:
65 else:
66 c.size = int(session.get('changelog_size', default))
66 c.size = int(session.get('changelog_size', default))
67 # min size must be 1
67 # min size must be 1
68 c.size = max(c.size, 1)
68 c.size = max(c.size, 1)
69 p = safe_int(request.params.get('page', 1), 1)
69 p = safe_int(request.params.get('page', 1), 1)
70 branch_name = request.params.get('branch', None)
70 branch_name = request.params.get('branch', None)
71 try:
71 try:
72 if branch_name:
72 collection = c.rhodecode_repo.get_changesets(start=0,
73 collection = [z for z in
73 branch_name=branch_name)
74 c.rhodecode_repo.get_changesets(start=0,
74 c.total_cs = len(collection)
75 branch_name=branch_name)]
76 c.total_cs = len(collection)
77 else:
78 collection = c.rhodecode_repo
79 c.total_cs = len(c.rhodecode_repo)
80
75
81 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
76 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
82 items_per_page=c.size, branch=branch_name)
77 items_per_page=c.size, branch=branch_name)
83 collection = list(c.pagination)
78 collection = list(c.pagination)
84 page_revisions = [x.raw_id for x in collection]
79 page_revisions = [x.raw_id for x in c.pagination]
85 c.comments = c.rhodecode_db_repo.get_comments(page_revisions)
80 c.comments = c.rhodecode_db_repo.get_comments(page_revisions)
86 c.statuses = c.rhodecode_db_repo.statuses(page_revisions)
81 c.statuses = c.rhodecode_db_repo.statuses(page_revisions)
87 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
82 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
88 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
89 h.flash(str(e), category='error')
84 h.flash(str(e), category='error')
90 return redirect(url('changelog_home', repo_name=c.repo_name))
85 return redirect(url('changelog_home', repo_name=c.repo_name))
91
86
92 self._graph(c.rhodecode_repo, collection, c.total_cs, c.size, p)
93
94 c.branch_name = branch_name
87 c.branch_name = branch_name
95 c.branch_filters = [('', _('All Branches'))] + \
88 c.branch_filters = [('', _('All Branches'))] + \
96 [(k, k) for k in c.rhodecode_repo.branches.keys()]
89 [(k, k) for k in c.rhodecode_repo.branches.keys()]
97
90
91 self._graph(c.rhodecode_repo, [x.revision for x in c.pagination],
92 c.total_cs, c.size, p)
93
98 return render('changelog/changelog.html')
94 return render('changelog/changelog.html')
99
95
100 def changelog_details(self, cs):
96 def changelog_details(self, cs):
101 if request.environ.get('HTTP_X_PARTIAL_XHR'):
97 if request.environ.get('HTTP_X_PARTIAL_XHR'):
102 c.cs = c.rhodecode_repo.get_changeset(cs)
98 c.cs = c.rhodecode_repo.get_changeset(cs)
103 return render('changelog/changelog_details.html')
99 return render('changelog/changelog_details.html')
104
100
105 def _graph(self, repo, collection, repo_size, size, p):
101 def _graph(self, repo, revs_int, repo_size, size, p):
106 """
102 """
107 Generates a DAG graph for mercurial
103 Generates a DAG graph for repo
108
104
109 :param repo: repo instance
105 :param repo:
110 :param size: number of commits to show
106 :param revs_int:
111 :param p: page number
107 :param repo_size:
108 :param size:
109 :param p:
112 """
110 """
113 if not collection:
111 if not revs_int:
114 c.jsdata = json.dumps([])
112 c.jsdata = json.dumps([])
115 return
113 return
116
114
117 data = []
115 data = []
118 revs = [x.revision for x in collection]
116 revs = revs_int
119
117
120 dag = _dagwalker(repo, revs, repo.alias)
118 dag = _dagwalker(repo, revs, repo.alias)
121 dag = _colored(dag)
119 dag = _colored(dag)
122 for (id, type, ctx, vtx, edges) in dag:
120 for (id, type, ctx, vtx, edges) in dag:
123 data.append(['', vtx, edges])
121 data.append(['', vtx, edges])
124
122
125 c.jsdata = json.dumps(data)
123 c.jsdata = json.dumps(data)
@@ -1,1004 +1,1028 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.base
3 vcs.backends.base
4 ~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~
5
5
6 Base for all available scm backends
6 Base for all available scm backends
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 datetime
12 import datetime
13 from itertools import chain
13 from itertools import chain
14 from rhodecode.lib.vcs.utils import author_name, author_email
14 from rhodecode.lib.vcs.utils import author_name, author_email
15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
16 from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs
16 from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs
17 from rhodecode.lib.vcs.conf import settings
17 from rhodecode.lib.vcs.conf import settings
18
18
19 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
19 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
20 NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \
20 NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \
21 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \
21 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \
22 RepositoryError
22 RepositoryError
23
23
24
24
25 class BaseRepository(object):
25 class BaseRepository(object):
26 """
26 """
27 Base Repository for final backends
27 Base Repository for final backends
28
28
29 **Attributes**
29 **Attributes**
30
30
31 ``DEFAULT_BRANCH_NAME``
31 ``DEFAULT_BRANCH_NAME``
32 name of default branch (i.e. "trunk" for svn, "master" for git etc.
32 name of default branch (i.e. "trunk" for svn, "master" for git etc.
33
33
34 ``scm``
34 ``scm``
35 alias of scm, i.e. *git* or *hg*
35 alias of scm, i.e. *git* or *hg*
36
36
37 ``repo``
37 ``repo``
38 object from external api
38 object from external api
39
39
40 ``revisions``
40 ``revisions``
41 list of all available revisions' ids, in ascending order
41 list of all available revisions' ids, in ascending order
42
42
43 ``changesets``
43 ``changesets``
44 storage dict caching returned changesets
44 storage dict caching returned changesets
45
45
46 ``path``
46 ``path``
47 absolute path to the repository
47 absolute path to the repository
48
48
49 ``branches``
49 ``branches``
50 branches as list of changesets
50 branches as list of changesets
51
51
52 ``tags``
52 ``tags``
53 tags as list of changesets
53 tags as list of changesets
54 """
54 """
55 scm = None
55 scm = None
56 DEFAULT_BRANCH_NAME = None
56 DEFAULT_BRANCH_NAME = None
57 EMPTY_CHANGESET = '0' * 40
57 EMPTY_CHANGESET = '0' * 40
58
58
59 def __init__(self, repo_path, create=False, **kwargs):
59 def __init__(self, repo_path, create=False, **kwargs):
60 """
60 """
61 Initializes repository. Raises RepositoryError if repository could
61 Initializes repository. Raises RepositoryError if repository could
62 not be find at the given ``repo_path`` or directory at ``repo_path``
62 not be find at the given ``repo_path`` or directory at ``repo_path``
63 exists and ``create`` is set to True.
63 exists and ``create`` is set to True.
64
64
65 :param repo_path: local path of the repository
65 :param repo_path: local path of the repository
66 :param create=False: if set to True, would try to craete repository.
66 :param create=False: if set to True, would try to craete repository.
67 :param src_url=None: if set, should be proper url from which repository
67 :param src_url=None: if set, should be proper url from which repository
68 would be cloned; requires ``create`` parameter to be set to True -
68 would be cloned; requires ``create`` parameter to be set to True -
69 raises RepositoryError if src_url is set and create evaluates to
69 raises RepositoryError if src_url is set and create evaluates to
70 False
70 False
71 """
71 """
72 raise NotImplementedError
72 raise NotImplementedError
73
73
74 def __str__(self):
74 def __str__(self):
75 return '<%s at %s>' % (self.__class__.__name__, self.path)
75 return '<%s at %s>' % (self.__class__.__name__, self.path)
76
76
77 def __repr__(self):
77 def __repr__(self):
78 return self.__str__()
78 return self.__str__()
79
79
80 def __len__(self):
80 def __len__(self):
81 return self.count()
81 return self.count()
82
82
83 def __eq__(self, other):
83 def __eq__(self, other):
84 same_instance = isinstance(other, self.__class__)
84 same_instance = isinstance(other, self.__class__)
85 return same_instance and getattr(other, 'path', None) == self.path
85 return same_instance and getattr(other, 'path', None) == self.path
86
86
87 def __ne__(self, other):
87 def __ne__(self, other):
88 return not self.__eq__(other)
88 return not self.__eq__(other)
89
89
90 @LazyProperty
90 @LazyProperty
91 def alias(self):
91 def alias(self):
92 for k, v in settings.BACKENDS.items():
92 for k, v in settings.BACKENDS.items():
93 if v.split('.')[-1] == str(self.__class__.__name__):
93 if v.split('.')[-1] == str(self.__class__.__name__):
94 return k
94 return k
95
95
96 @LazyProperty
96 @LazyProperty
97 def name(self):
97 def name(self):
98 raise NotImplementedError
98 raise NotImplementedError
99
99
100 @LazyProperty
100 @LazyProperty
101 def owner(self):
101 def owner(self):
102 raise NotImplementedError
102 raise NotImplementedError
103
103
104 @LazyProperty
104 @LazyProperty
105 def description(self):
105 def description(self):
106 raise NotImplementedError
106 raise NotImplementedError
107
107
108 @LazyProperty
108 @LazyProperty
109 def size(self):
109 def size(self):
110 """
110 """
111 Returns combined size in bytes for all repository files
111 Returns combined size in bytes for all repository files
112 """
112 """
113
113
114 size = 0
114 size = 0
115 try:
115 try:
116 tip = self.get_changeset()
116 tip = self.get_changeset()
117 for topnode, dirs, files in tip.walk('/'):
117 for topnode, dirs, files in tip.walk('/'):
118 for f in files:
118 for f in files:
119 size += tip.get_file_size(f.path)
119 size += tip.get_file_size(f.path)
120 for dir in dirs:
120 for dir in dirs:
121 for f in files:
121 for f in files:
122 size += tip.get_file_size(f.path)
122 size += tip.get_file_size(f.path)
123
123
124 except RepositoryError, e:
124 except RepositoryError, e:
125 pass
125 pass
126 return size
126 return size
127
127
128 def is_valid(self):
128 def is_valid(self):
129 """
129 """
130 Validates repository.
130 Validates repository.
131 """
131 """
132 raise NotImplementedError
132 raise NotImplementedError
133
133
134 def get_last_change(self):
134 def get_last_change(self):
135 self.get_changesets()
135 self.get_changesets()
136
136
137 #==========================================================================
137 #==========================================================================
138 # CHANGESETS
138 # CHANGESETS
139 #==========================================================================
139 #==========================================================================
140
140
141 def get_changeset(self, revision=None):
141 def get_changeset(self, revision=None):
142 """
142 """
143 Returns instance of ``Changeset`` class. If ``revision`` is None, most
143 Returns instance of ``Changeset`` class. If ``revision`` is None, most
144 recent changeset is returned.
144 recent changeset is returned.
145
145
146 :raises ``EmptyRepositoryError``: if there are no revisions
146 :raises ``EmptyRepositoryError``: if there are no revisions
147 """
147 """
148 raise NotImplementedError
148 raise NotImplementedError
149
149
150 def __iter__(self):
150 def __iter__(self):
151 """
151 """
152 Allows Repository objects to be iterated.
152 Allows Repository objects to be iterated.
153
153
154 *Requires* implementation of ``__getitem__`` method.
154 *Requires* implementation of ``__getitem__`` method.
155 """
155 """
156 for revision in self.revisions:
156 for revision in self.revisions:
157 yield self.get_changeset(revision)
157 yield self.get_changeset(revision)
158
158
159 def get_changesets(self, start=None, end=None, start_date=None,
159 def get_changesets(self, start=None, end=None, start_date=None,
160 end_date=None, branch_name=None, reverse=False):
160 end_date=None, branch_name=None, reverse=False):
161 """
161 """
162 Returns iterator of ``MercurialChangeset`` objects from start to end
162 Returns iterator of ``MercurialChangeset`` objects from start to end
163 not inclusive This should behave just like a list, ie. end is not
163 not inclusive This should behave just like a list, ie. end is not
164 inclusive
164 inclusive
165
165
166 :param start: None or str
166 :param start: None or str
167 :param end: None or str
167 :param end: None or str
168 :param start_date:
168 :param start_date:
169 :param end_date:
169 :param end_date:
170 :param branch_name:
170 :param branch_name:
171 :param reversed:
171 :param reversed:
172 """
172 """
173 raise NotImplementedError
173 raise NotImplementedError
174
174
175 def __getslice__(self, i, j):
175 def __getslice__(self, i, j):
176 """
176 """
177 Returns a iterator of sliced repository
177 Returns a iterator of sliced repository
178 """
178 """
179 for rev in self.revisions[i:j]:
179 for rev in self.revisions[i:j]:
180 yield self.get_changeset(rev)
180 yield self.get_changeset(rev)
181
181
182 def __getitem__(self, key):
182 def __getitem__(self, key):
183 return self.get_changeset(key)
183 return self.get_changeset(key)
184
184
185 def count(self):
185 def count(self):
186 return len(self.revisions)
186 return len(self.revisions)
187
187
188 def tag(self, name, user, revision=None, message=None, date=None, **opts):
188 def tag(self, name, user, revision=None, message=None, date=None, **opts):
189 """
189 """
190 Creates and returns a tag for the given ``revision``.
190 Creates and returns a tag for the given ``revision``.
191
191
192 :param name: name for new tag
192 :param name: name for new tag
193 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
193 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
194 :param revision: changeset id for which new tag would be created
194 :param revision: changeset id for which new tag would be created
195 :param message: message of the tag's commit
195 :param message: message of the tag's commit
196 :param date: date of tag's commit
196 :param date: date of tag's commit
197
197
198 :raises TagAlreadyExistError: if tag with same name already exists
198 :raises TagAlreadyExistError: if tag with same name already exists
199 """
199 """
200 raise NotImplementedError
200 raise NotImplementedError
201
201
202 def remove_tag(self, name, user, message=None, date=None):
202 def remove_tag(self, name, user, message=None, date=None):
203 """
203 """
204 Removes tag with the given ``name``.
204 Removes tag with the given ``name``.
205
205
206 :param name: name of the tag to be removed
206 :param name: name of the tag to be removed
207 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
207 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
208 :param message: message of the tag's removal commit
208 :param message: message of the tag's removal commit
209 :param date: date of tag's removal commit
209 :param date: date of tag's removal commit
210
210
211 :raises TagDoesNotExistError: if tag with given name does not exists
211 :raises TagDoesNotExistError: if tag with given name does not exists
212 """
212 """
213 raise NotImplementedError
213 raise NotImplementedError
214
214
215 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
215 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
216 context=3):
216 context=3):
217 """
217 """
218 Returns (git like) *diff*, as plain text. Shows changes introduced by
218 Returns (git like) *diff*, as plain text. Shows changes introduced by
219 ``rev2`` since ``rev1``.
219 ``rev2`` since ``rev1``.
220
220
221 :param rev1: Entry point from which diff is shown. Can be
221 :param rev1: Entry point from which diff is shown. Can be
222 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
222 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
223 the changes since empty state of the repository until ``rev2``
223 the changes since empty state of the repository until ``rev2``
224 :param rev2: Until which revision changes should be shown.
224 :param rev2: Until which revision changes should be shown.
225 :param ignore_whitespace: If set to ``True``, would not show whitespace
225 :param ignore_whitespace: If set to ``True``, would not show whitespace
226 changes. Defaults to ``False``.
226 changes. Defaults to ``False``.
227 :param context: How many lines before/after changed lines should be
227 :param context: How many lines before/after changed lines should be
228 shown. Defaults to ``3``.
228 shown. Defaults to ``3``.
229 """
229 """
230 raise NotImplementedError
230 raise NotImplementedError
231
231
232 # ========== #
232 # ========== #
233 # COMMIT API #
233 # COMMIT API #
234 # ========== #
234 # ========== #
235
235
236 @LazyProperty
236 @LazyProperty
237 def in_memory_changeset(self):
237 def in_memory_changeset(self):
238 """
238 """
239 Returns ``InMemoryChangeset`` object for this repository.
239 Returns ``InMemoryChangeset`` object for this repository.
240 """
240 """
241 raise NotImplementedError
241 raise NotImplementedError
242
242
243 def add(self, filenode, **kwargs):
243 def add(self, filenode, **kwargs):
244 """
244 """
245 Commit api function that will add given ``FileNode`` into this
245 Commit api function that will add given ``FileNode`` into this
246 repository.
246 repository.
247
247
248 :raises ``NodeAlreadyExistsError``: if there is a file with same path
248 :raises ``NodeAlreadyExistsError``: if there is a file with same path
249 already in repository
249 already in repository
250 :raises ``NodeAlreadyAddedError``: if given node is already marked as
250 :raises ``NodeAlreadyAddedError``: if given node is already marked as
251 *added*
251 *added*
252 """
252 """
253 raise NotImplementedError
253 raise NotImplementedError
254
254
255 def remove(self, filenode, **kwargs):
255 def remove(self, filenode, **kwargs):
256 """
256 """
257 Commit api function that will remove given ``FileNode`` into this
257 Commit api function that will remove given ``FileNode`` into this
258 repository.
258 repository.
259
259
260 :raises ``EmptyRepositoryError``: if there are no changesets yet
260 :raises ``EmptyRepositoryError``: if there are no changesets yet
261 :raises ``NodeDoesNotExistError``: if there is no file with given path
261 :raises ``NodeDoesNotExistError``: if there is no file with given path
262 """
262 """
263 raise NotImplementedError
263 raise NotImplementedError
264
264
265 def commit(self, message, **kwargs):
265 def commit(self, message, **kwargs):
266 """
266 """
267 Persists current changes made on this repository and returns newly
267 Persists current changes made on this repository and returns newly
268 created changeset.
268 created changeset.
269
269
270 :raises ``NothingChangedError``: if no changes has been made
270 :raises ``NothingChangedError``: if no changes has been made
271 """
271 """
272 raise NotImplementedError
272 raise NotImplementedError
273
273
274 def get_state(self):
274 def get_state(self):
275 """
275 """
276 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
276 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
277 containing ``FileNode`` objects.
277 containing ``FileNode`` objects.
278 """
278 """
279 raise NotImplementedError
279 raise NotImplementedError
280
280
281 def get_config_value(self, section, name, config_file=None):
281 def get_config_value(self, section, name, config_file=None):
282 """
282 """
283 Returns configuration value for a given [``section``] and ``name``.
283 Returns configuration value for a given [``section``] and ``name``.
284
284
285 :param section: Section we want to retrieve value from
285 :param section: Section we want to retrieve value from
286 :param name: Name of configuration we want to retrieve
286 :param name: Name of configuration we want to retrieve
287 :param config_file: A path to file which should be used to retrieve
287 :param config_file: A path to file which should be used to retrieve
288 configuration from (might also be a list of file paths)
288 configuration from (might also be a list of file paths)
289 """
289 """
290 raise NotImplementedError
290 raise NotImplementedError
291
291
292 def get_user_name(self, config_file=None):
292 def get_user_name(self, config_file=None):
293 """
293 """
294 Returns user's name from global configuration file.
294 Returns user's name from global configuration file.
295
295
296 :param config_file: A path to file which should be used to retrieve
296 :param config_file: A path to file which should be used to retrieve
297 configuration from (might also be a list of file paths)
297 configuration from (might also be a list of file paths)
298 """
298 """
299 raise NotImplementedError
299 raise NotImplementedError
300
300
301 def get_user_email(self, config_file=None):
301 def get_user_email(self, config_file=None):
302 """
302 """
303 Returns user's email from global configuration file.
303 Returns user's email from global configuration file.
304
304
305 :param config_file: A path to file which should be used to retrieve
305 :param config_file: A path to file which should be used to retrieve
306 configuration from (might also be a list of file paths)
306 configuration from (might also be a list of file paths)
307 """
307 """
308 raise NotImplementedError
308 raise NotImplementedError
309
309
310 # =========== #
310 # =========== #
311 # WORKDIR API #
311 # WORKDIR API #
312 # =========== #
312 # =========== #
313
313
314 @LazyProperty
314 @LazyProperty
315 def workdir(self):
315 def workdir(self):
316 """
316 """
317 Returns ``Workdir`` instance for this repository.
317 Returns ``Workdir`` instance for this repository.
318 """
318 """
319 raise NotImplementedError
319 raise NotImplementedError
320
320
321
321
322 class BaseChangeset(object):
322 class BaseChangeset(object):
323 """
323 """
324 Each backend should implement it's changeset representation.
324 Each backend should implement it's changeset representation.
325
325
326 **Attributes**
326 **Attributes**
327
327
328 ``repository``
328 ``repository``
329 repository object within which changeset exists
329 repository object within which changeset exists
330
330
331 ``id``
331 ``id``
332 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
332 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
333
333
334 ``raw_id``
334 ``raw_id``
335 raw changeset representation (i.e. full 40 length sha for git
335 raw changeset representation (i.e. full 40 length sha for git
336 backend)
336 backend)
337
337
338 ``short_id``
338 ``short_id``
339 shortened (if apply) version of ``raw_id``; it would be simple
339 shortened (if apply) version of ``raw_id``; it would be simple
340 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
340 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
341 as ``raw_id`` for subversion
341 as ``raw_id`` for subversion
342
342
343 ``revision``
343 ``revision``
344 revision number as integer
344 revision number as integer
345
345
346 ``files``
346 ``files``
347 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
347 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
348
348
349 ``dirs``
349 ``dirs``
350 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
350 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
351
351
352 ``nodes``
352 ``nodes``
353 combined list of ``Node`` objects
353 combined list of ``Node`` objects
354
354
355 ``author``
355 ``author``
356 author of the changeset, as unicode
356 author of the changeset, as unicode
357
357
358 ``message``
358 ``message``
359 message of the changeset, as unicode
359 message of the changeset, as unicode
360
360
361 ``parents``
361 ``parents``
362 list of parent changesets
362 list of parent changesets
363
363
364 ``last``
364 ``last``
365 ``True`` if this is last changeset in repository, ``False``
365 ``True`` if this is last changeset in repository, ``False``
366 otherwise; trying to access this attribute while there is no
366 otherwise; trying to access this attribute while there is no
367 changesets would raise ``EmptyRepositoryError``
367 changesets would raise ``EmptyRepositoryError``
368 """
368 """
369 def __str__(self):
369 def __str__(self):
370 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
370 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
371 self.short_id)
371 self.short_id)
372
372
373 def __repr__(self):
373 def __repr__(self):
374 return self.__str__()
374 return self.__str__()
375
375
376 def __unicode__(self):
376 def __unicode__(self):
377 return u'%s:%s' % (self.revision, self.short_id)
377 return u'%s:%s' % (self.revision, self.short_id)
378
378
379 def __eq__(self, other):
379 def __eq__(self, other):
380 return self.raw_id == other.raw_id
380 return self.raw_id == other.raw_id
381
381
382 def __json__(self):
382 def __json__(self):
383 return dict(
383 return dict(
384 short_id=self.short_id,
384 short_id=self.short_id,
385 raw_id=self.raw_id,
385 raw_id=self.raw_id,
386 revision=self.revision,
386 revision=self.revision,
387 message=self.message,
387 message=self.message,
388 date=self.date,
388 date=self.date,
389 author=self.author,
389 author=self.author,
390 )
390 )
391
391
392 @LazyProperty
392 @LazyProperty
393 def last(self):
393 def last(self):
394 if self.repository is None:
394 if self.repository is None:
395 raise ChangesetError("Cannot check if it's most recent revision")
395 raise ChangesetError("Cannot check if it's most recent revision")
396 return self.raw_id == self.repository.revisions[-1]
396 return self.raw_id == self.repository.revisions[-1]
397
397
398 @LazyProperty
398 @LazyProperty
399 def parents(self):
399 def parents(self):
400 """
400 """
401 Returns list of parents changesets.
401 Returns list of parents changesets.
402 """
402 """
403 raise NotImplementedError
403 raise NotImplementedError
404
404
405 @LazyProperty
405 @LazyProperty
406 def children(self):
406 def children(self):
407 """
407 """
408 Returns list of children changesets.
408 Returns list of children changesets.
409 """
409 """
410 raise NotImplementedError
410 raise NotImplementedError
411
411
412 @LazyProperty
412 @LazyProperty
413 def id(self):
413 def id(self):
414 """
414 """
415 Returns string identifying this changeset.
415 Returns string identifying this changeset.
416 """
416 """
417 raise NotImplementedError
417 raise NotImplementedError
418
418
419 @LazyProperty
419 @LazyProperty
420 def raw_id(self):
420 def raw_id(self):
421 """
421 """
422 Returns raw string identifying this changeset.
422 Returns raw string identifying this changeset.
423 """
423 """
424 raise NotImplementedError
424 raise NotImplementedError
425
425
426 @LazyProperty
426 @LazyProperty
427 def short_id(self):
427 def short_id(self):
428 """
428 """
429 Returns shortened version of ``raw_id`` attribute, as string,
429 Returns shortened version of ``raw_id`` attribute, as string,
430 identifying this changeset, useful for web representation.
430 identifying this changeset, useful for web representation.
431 """
431 """
432 raise NotImplementedError
432 raise NotImplementedError
433
433
434 @LazyProperty
434 @LazyProperty
435 def revision(self):
435 def revision(self):
436 """
436 """
437 Returns integer identifying this changeset.
437 Returns integer identifying this changeset.
438
438
439 """
439 """
440 raise NotImplementedError
440 raise NotImplementedError
441
441
442 @LazyProperty
442 @LazyProperty
443 def committer(self):
443 def committer(self):
444 """
444 """
445 Returns Committer for given commit
445 Returns Committer for given commit
446 """
446 """
447
447
448 raise NotImplementedError
448 raise NotImplementedError
449
449
450 @LazyProperty
450 @LazyProperty
451 def committer_name(self):
451 def committer_name(self):
452 """
452 """
453 Returns Author name for given commit
453 Returns Author name for given commit
454 """
454 """
455
455
456 return author_name(self.committer)
456 return author_name(self.committer)
457
457
458 @LazyProperty
458 @LazyProperty
459 def committer_email(self):
459 def committer_email(self):
460 """
460 """
461 Returns Author email address for given commit
461 Returns Author email address for given commit
462 """
462 """
463
463
464 return author_email(self.committer)
464 return author_email(self.committer)
465
465
466 @LazyProperty
466 @LazyProperty
467 def author(self):
467 def author(self):
468 """
468 """
469 Returns Author for given commit
469 Returns Author for given commit
470 """
470 """
471
471
472 raise NotImplementedError
472 raise NotImplementedError
473
473
474 @LazyProperty
474 @LazyProperty
475 def author_name(self):
475 def author_name(self):
476 """
476 """
477 Returns Author name for given commit
477 Returns Author name for given commit
478 """
478 """
479
479
480 return author_name(self.author)
480 return author_name(self.author)
481
481
482 @LazyProperty
482 @LazyProperty
483 def author_email(self):
483 def author_email(self):
484 """
484 """
485 Returns Author email address for given commit
485 Returns Author email address for given commit
486 """
486 """
487
487
488 return author_email(self.author)
488 return author_email(self.author)
489
489
490 def get_file_mode(self, path):
490 def get_file_mode(self, path):
491 """
491 """
492 Returns stat mode of the file at the given ``path``.
492 Returns stat mode of the file at the given ``path``.
493 """
493 """
494 raise NotImplementedError
494 raise NotImplementedError
495
495
496 def get_file_content(self, path):
496 def get_file_content(self, path):
497 """
497 """
498 Returns content of the file at the given ``path``.
498 Returns content of the file at the given ``path``.
499 """
499 """
500 raise NotImplementedError
500 raise NotImplementedError
501
501
502 def get_file_size(self, path):
502 def get_file_size(self, path):
503 """
503 """
504 Returns size of the file at the given ``path``.
504 Returns size of the file at the given ``path``.
505 """
505 """
506 raise NotImplementedError
506 raise NotImplementedError
507
507
508 def get_file_changeset(self, path):
508 def get_file_changeset(self, path):
509 """
509 """
510 Returns last commit of the file at the given ``path``.
510 Returns last commit of the file at the given ``path``.
511 """
511 """
512 raise NotImplementedError
512 raise NotImplementedError
513
513
514 def get_file_history(self, path):
514 def get_file_history(self, path):
515 """
515 """
516 Returns history of file as reversed list of ``Changeset`` objects for
516 Returns history of file as reversed list of ``Changeset`` objects for
517 which file at given ``path`` has been modified.
517 which file at given ``path`` has been modified.
518 """
518 """
519 raise NotImplementedError
519 raise NotImplementedError
520
520
521 def get_nodes(self, path):
521 def get_nodes(self, path):
522 """
522 """
523 Returns combined ``DirNode`` and ``FileNode`` objects list representing
523 Returns combined ``DirNode`` and ``FileNode`` objects list representing
524 state of changeset at the given ``path``.
524 state of changeset at the given ``path``.
525
525
526 :raises ``ChangesetError``: if node at the given ``path`` is not
526 :raises ``ChangesetError``: if node at the given ``path`` is not
527 instance of ``DirNode``
527 instance of ``DirNode``
528 """
528 """
529 raise NotImplementedError
529 raise NotImplementedError
530
530
531 def get_node(self, path):
531 def get_node(self, path):
532 """
532 """
533 Returns ``Node`` object from the given ``path``.
533 Returns ``Node`` object from the given ``path``.
534
534
535 :raises ``NodeDoesNotExistError``: if there is no node at the given
535 :raises ``NodeDoesNotExistError``: if there is no node at the given
536 ``path``
536 ``path``
537 """
537 """
538 raise NotImplementedError
538 raise NotImplementedError
539
539
540 def fill_archive(self, stream=None, kind='tgz', prefix=None):
540 def fill_archive(self, stream=None, kind='tgz', prefix=None):
541 """
541 """
542 Fills up given stream.
542 Fills up given stream.
543
543
544 :param stream: file like object.
544 :param stream: file like object.
545 :param kind: one of following: ``zip``, ``tar``, ``tgz``
545 :param kind: one of following: ``zip``, ``tar``, ``tgz``
546 or ``tbz2``. Default: ``tgz``.
546 or ``tbz2``. Default: ``tgz``.
547 :param prefix: name of root directory in archive.
547 :param prefix: name of root directory in archive.
548 Default is repository name and changeset's raw_id joined with dash.
548 Default is repository name and changeset's raw_id joined with dash.
549
549
550 repo-tip.<kind>
550 repo-tip.<kind>
551 """
551 """
552
552
553 raise NotImplementedError
553 raise NotImplementedError
554
554
555 def get_chunked_archive(self, **kwargs):
555 def get_chunked_archive(self, **kwargs):
556 """
556 """
557 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
557 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
558
558
559 :param chunk_size: extra parameter which controls size of returned
559 :param chunk_size: extra parameter which controls size of returned
560 chunks. Default:8k.
560 chunks. Default:8k.
561 """
561 """
562
562
563 chunk_size = kwargs.pop('chunk_size', 8192)
563 chunk_size = kwargs.pop('chunk_size', 8192)
564 stream = kwargs.get('stream')
564 stream = kwargs.get('stream')
565 self.fill_archive(**kwargs)
565 self.fill_archive(**kwargs)
566 while True:
566 while True:
567 data = stream.read(chunk_size)
567 data = stream.read(chunk_size)
568 if not data:
568 if not data:
569 break
569 break
570 yield data
570 yield data
571
571
572 @LazyProperty
572 @LazyProperty
573 def root(self):
573 def root(self):
574 """
574 """
575 Returns ``RootNode`` object for this changeset.
575 Returns ``RootNode`` object for this changeset.
576 """
576 """
577 return self.get_node('')
577 return self.get_node('')
578
578
579 def next(self, branch=None):
579 def next(self, branch=None):
580 """
580 """
581 Returns next changeset from current, if branch is gives it will return
581 Returns next changeset from current, if branch is gives it will return
582 next changeset belonging to this branch
582 next changeset belonging to this branch
583
583
584 :param branch: show changesets within the given named branch
584 :param branch: show changesets within the given named branch
585 """
585 """
586 raise NotImplementedError
586 raise NotImplementedError
587
587
588 def prev(self, branch=None):
588 def prev(self, branch=None):
589 """
589 """
590 Returns previous changeset from current, if branch is gives it will
590 Returns previous changeset from current, if branch is gives it will
591 return previous changeset belonging to this branch
591 return previous changeset belonging to this branch
592
592
593 :param branch: show changesets within the given named branch
593 :param branch: show changesets within the given named branch
594 """
594 """
595 raise NotImplementedError
595 raise NotImplementedError
596
596
597 @LazyProperty
597 @LazyProperty
598 def added(self):
598 def added(self):
599 """
599 """
600 Returns list of added ``FileNode`` objects.
600 Returns list of added ``FileNode`` objects.
601 """
601 """
602 raise NotImplementedError
602 raise NotImplementedError
603
603
604 @LazyProperty
604 @LazyProperty
605 def changed(self):
605 def changed(self):
606 """
606 """
607 Returns list of modified ``FileNode`` objects.
607 Returns list of modified ``FileNode`` objects.
608 """
608 """
609 raise NotImplementedError
609 raise NotImplementedError
610
610
611 @LazyProperty
611 @LazyProperty
612 def removed(self):
612 def removed(self):
613 """
613 """
614 Returns list of removed ``FileNode`` objects.
614 Returns list of removed ``FileNode`` objects.
615 """
615 """
616 raise NotImplementedError
616 raise NotImplementedError
617
617
618 @LazyProperty
618 @LazyProperty
619 def size(self):
619 def size(self):
620 """
620 """
621 Returns total number of bytes from contents of all filenodes.
621 Returns total number of bytes from contents of all filenodes.
622 """
622 """
623 return sum((node.size for node in self.get_filenodes_generator()))
623 return sum((node.size for node in self.get_filenodes_generator()))
624
624
625 def walk(self, topurl=''):
625 def walk(self, topurl=''):
626 """
626 """
627 Similar to os.walk method. Insted of filesystem it walks through
627 Similar to os.walk method. Insted of filesystem it walks through
628 changeset starting at given ``topurl``. Returns generator of tuples
628 changeset starting at given ``topurl``. Returns generator of tuples
629 (topnode, dirnodes, filenodes).
629 (topnode, dirnodes, filenodes).
630 """
630 """
631 topnode = self.get_node(topurl)
631 topnode = self.get_node(topurl)
632 yield (topnode, topnode.dirs, topnode.files)
632 yield (topnode, topnode.dirs, topnode.files)
633 for dirnode in topnode.dirs:
633 for dirnode in topnode.dirs:
634 for tup in self.walk(dirnode.path):
634 for tup in self.walk(dirnode.path):
635 yield tup
635 yield tup
636
636
637 def get_filenodes_generator(self):
637 def get_filenodes_generator(self):
638 """
638 """
639 Returns generator that yields *all* file nodes.
639 Returns generator that yields *all* file nodes.
640 """
640 """
641 for topnode, dirs, files in self.walk():
641 for topnode, dirs, files in self.walk():
642 for node in files:
642 for node in files:
643 yield node
643 yield node
644
644
645 def as_dict(self):
645 def as_dict(self):
646 """
646 """
647 Returns dictionary with changeset's attributes and their values.
647 Returns dictionary with changeset's attributes and their values.
648 """
648 """
649 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
649 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
650 'revision', 'date', 'message'])
650 'revision', 'date', 'message'])
651 data['author'] = {'name': self.author_name, 'email': self.author_email}
651 data['author'] = {'name': self.author_name, 'email': self.author_email}
652 data['added'] = [node.path for node in self.added]
652 data['added'] = [node.path for node in self.added]
653 data['changed'] = [node.path for node in self.changed]
653 data['changed'] = [node.path for node in self.changed]
654 data['removed'] = [node.path for node in self.removed]
654 data['removed'] = [node.path for node in self.removed]
655 return data
655 return data
656
656
657
657
658 class BaseWorkdir(object):
658 class BaseWorkdir(object):
659 """
659 """
660 Working directory representation of single repository.
660 Working directory representation of single repository.
661
661
662 :attribute: repository: repository object of working directory
662 :attribute: repository: repository object of working directory
663 """
663 """
664
664
665 def __init__(self, repository):
665 def __init__(self, repository):
666 self.repository = repository
666 self.repository = repository
667
667
668 def get_branch(self):
668 def get_branch(self):
669 """
669 """
670 Returns name of current branch.
670 Returns name of current branch.
671 """
671 """
672 raise NotImplementedError
672 raise NotImplementedError
673
673
674 def get_changeset(self):
674 def get_changeset(self):
675 """
675 """
676 Returns current changeset.
676 Returns current changeset.
677 """
677 """
678 raise NotImplementedError
678 raise NotImplementedError
679
679
680 def get_added(self):
680 def get_added(self):
681 """
681 """
682 Returns list of ``FileNode`` objects marked as *new* in working
682 Returns list of ``FileNode`` objects marked as *new* in working
683 directory.
683 directory.
684 """
684 """
685 raise NotImplementedError
685 raise NotImplementedError
686
686
687 def get_changed(self):
687 def get_changed(self):
688 """
688 """
689 Returns list of ``FileNode`` objects *changed* in working directory.
689 Returns list of ``FileNode`` objects *changed* in working directory.
690 """
690 """
691 raise NotImplementedError
691 raise NotImplementedError
692
692
693 def get_removed(self):
693 def get_removed(self):
694 """
694 """
695 Returns list of ``RemovedFileNode`` objects marked as *removed* in
695 Returns list of ``RemovedFileNode`` objects marked as *removed* in
696 working directory.
696 working directory.
697 """
697 """
698 raise NotImplementedError
698 raise NotImplementedError
699
699
700 def get_untracked(self):
700 def get_untracked(self):
701 """
701 """
702 Returns list of ``FileNode`` objects which are present within working
702 Returns list of ``FileNode`` objects which are present within working
703 directory however are not tracked by repository.
703 directory however are not tracked by repository.
704 """
704 """
705 raise NotImplementedError
705 raise NotImplementedError
706
706
707 def get_status(self):
707 def get_status(self):
708 """
708 """
709 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
709 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
710 lists.
710 lists.
711 """
711 """
712 raise NotImplementedError
712 raise NotImplementedError
713
713
714 def commit(self, message, **kwargs):
714 def commit(self, message, **kwargs):
715 """
715 """
716 Commits local (from working directory) changes and returns newly
716 Commits local (from working directory) changes and returns newly
717 created
717 created
718 ``Changeset``. Updates repository's ``revisions`` list.
718 ``Changeset``. Updates repository's ``revisions`` list.
719
719
720 :raises ``CommitError``: if any error occurs while committing
720 :raises ``CommitError``: if any error occurs while committing
721 """
721 """
722 raise NotImplementedError
722 raise NotImplementedError
723
723
724 def update(self, revision=None):
724 def update(self, revision=None):
725 """
725 """
726 Fetches content of the given revision and populates it within working
726 Fetches content of the given revision and populates it within working
727 directory.
727 directory.
728 """
728 """
729 raise NotImplementedError
729 raise NotImplementedError
730
730
731 def checkout_branch(self, branch=None):
731 def checkout_branch(self, branch=None):
732 """
732 """
733 Checks out ``branch`` or the backend's default branch.
733 Checks out ``branch`` or the backend's default branch.
734
734
735 Raises ``BranchDoesNotExistError`` if the branch does not exist.
735 Raises ``BranchDoesNotExistError`` if the branch does not exist.
736 """
736 """
737 raise NotImplementedError
737 raise NotImplementedError
738
738
739
739
740 class BaseInMemoryChangeset(object):
740 class BaseInMemoryChangeset(object):
741 """
741 """
742 Represents differences between repository's state (most recent head) and
742 Represents differences between repository's state (most recent head) and
743 changes made *in place*.
743 changes made *in place*.
744
744
745 **Attributes**
745 **Attributes**
746
746
747 ``repository``
747 ``repository``
748 repository object for this in-memory-changeset
748 repository object for this in-memory-changeset
749
749
750 ``added``
750 ``added``
751 list of ``FileNode`` objects marked as *added*
751 list of ``FileNode`` objects marked as *added*
752
752
753 ``changed``
753 ``changed``
754 list of ``FileNode`` objects marked as *changed*
754 list of ``FileNode`` objects marked as *changed*
755
755
756 ``removed``
756 ``removed``
757 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
757 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
758 *removed*
758 *removed*
759
759
760 ``parents``
760 ``parents``
761 list of ``Changeset`` representing parents of in-memory changeset.
761 list of ``Changeset`` representing parents of in-memory changeset.
762 Should always be 2-element sequence.
762 Should always be 2-element sequence.
763
763
764 """
764 """
765
765
766 def __init__(self, repository):
766 def __init__(self, repository):
767 self.repository = repository
767 self.repository = repository
768 self.added = []
768 self.added = []
769 self.changed = []
769 self.changed = []
770 self.removed = []
770 self.removed = []
771 self.parents = []
771 self.parents = []
772
772
773 def add(self, *filenodes):
773 def add(self, *filenodes):
774 """
774 """
775 Marks given ``FileNode`` objects as *to be committed*.
775 Marks given ``FileNode`` objects as *to be committed*.
776
776
777 :raises ``NodeAlreadyExistsError``: if node with same path exists at
777 :raises ``NodeAlreadyExistsError``: if node with same path exists at
778 latest changeset
778 latest changeset
779 :raises ``NodeAlreadyAddedError``: if node with same path is already
779 :raises ``NodeAlreadyAddedError``: if node with same path is already
780 marked as *added*
780 marked as *added*
781 """
781 """
782 # Check if not already marked as *added* first
782 # Check if not already marked as *added* first
783 for node in filenodes:
783 for node in filenodes:
784 if node.path in (n.path for n in self.added):
784 if node.path in (n.path for n in self.added):
785 raise NodeAlreadyAddedError("Such FileNode %s is already "
785 raise NodeAlreadyAddedError("Such FileNode %s is already "
786 "marked for addition" % node.path)
786 "marked for addition" % node.path)
787 for node in filenodes:
787 for node in filenodes:
788 self.added.append(node)
788 self.added.append(node)
789
789
790 def change(self, *filenodes):
790 def change(self, *filenodes):
791 """
791 """
792 Marks given ``FileNode`` objects to be *changed* in next commit.
792 Marks given ``FileNode`` objects to be *changed* in next commit.
793
793
794 :raises ``EmptyRepositoryError``: if there are no changesets yet
794 :raises ``EmptyRepositoryError``: if there are no changesets yet
795 :raises ``NodeAlreadyExistsError``: if node with same path is already
795 :raises ``NodeAlreadyExistsError``: if node with same path is already
796 marked to be *changed*
796 marked to be *changed*
797 :raises ``NodeAlreadyRemovedError``: if node with same path is already
797 :raises ``NodeAlreadyRemovedError``: if node with same path is already
798 marked to be *removed*
798 marked to be *removed*
799 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
799 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
800 changeset
800 changeset
801 :raises ``NodeNotChangedError``: if node hasn't really be changed
801 :raises ``NodeNotChangedError``: if node hasn't really be changed
802 """
802 """
803 for node in filenodes:
803 for node in filenodes:
804 if node.path in (n.path for n in self.removed):
804 if node.path in (n.path for n in self.removed):
805 raise NodeAlreadyRemovedError("Node at %s is already marked "
805 raise NodeAlreadyRemovedError("Node at %s is already marked "
806 "as removed" % node.path)
806 "as removed" % node.path)
807 try:
807 try:
808 self.repository.get_changeset()
808 self.repository.get_changeset()
809 except EmptyRepositoryError:
809 except EmptyRepositoryError:
810 raise EmptyRepositoryError("Nothing to change - try to *add* new "
810 raise EmptyRepositoryError("Nothing to change - try to *add* new "
811 "nodes rather than changing them")
811 "nodes rather than changing them")
812 for node in filenodes:
812 for node in filenodes:
813 if node.path in (n.path for n in self.changed):
813 if node.path in (n.path for n in self.changed):
814 raise NodeAlreadyChangedError("Node at '%s' is already "
814 raise NodeAlreadyChangedError("Node at '%s' is already "
815 "marked as changed" % node.path)
815 "marked as changed" % node.path)
816 self.changed.append(node)
816 self.changed.append(node)
817
817
818 def remove(self, *filenodes):
818 def remove(self, *filenodes):
819 """
819 """
820 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
820 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
821 *removed* in next commit.
821 *removed* in next commit.
822
822
823 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
823 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
824 be *removed*
824 be *removed*
825 :raises ``NodeAlreadyChangedError``: if node has been already marked to
825 :raises ``NodeAlreadyChangedError``: if node has been already marked to
826 be *changed*
826 be *changed*
827 """
827 """
828 for node in filenodes:
828 for node in filenodes:
829 if node.path in (n.path for n in self.removed):
829 if node.path in (n.path for n in self.removed):
830 raise NodeAlreadyRemovedError("Node is already marked to "
830 raise NodeAlreadyRemovedError("Node is already marked to "
831 "for removal at %s" % node.path)
831 "for removal at %s" % node.path)
832 if node.path in (n.path for n in self.changed):
832 if node.path in (n.path for n in self.changed):
833 raise NodeAlreadyChangedError("Node is already marked to "
833 raise NodeAlreadyChangedError("Node is already marked to "
834 "be changed at %s" % node.path)
834 "be changed at %s" % node.path)
835 # We only mark node as *removed* - real removal is done by
835 # We only mark node as *removed* - real removal is done by
836 # commit method
836 # commit method
837 self.removed.append(node)
837 self.removed.append(node)
838
838
839 def reset(self):
839 def reset(self):
840 """
840 """
841 Resets this instance to initial state (cleans ``added``, ``changed``
841 Resets this instance to initial state (cleans ``added``, ``changed``
842 and ``removed`` lists).
842 and ``removed`` lists).
843 """
843 """
844 self.added = []
844 self.added = []
845 self.changed = []
845 self.changed = []
846 self.removed = []
846 self.removed = []
847 self.parents = []
847 self.parents = []
848
848
849 def get_ipaths(self):
849 def get_ipaths(self):
850 """
850 """
851 Returns generator of paths from nodes marked as added, changed or
851 Returns generator of paths from nodes marked as added, changed or
852 removed.
852 removed.
853 """
853 """
854 for node in chain(self.added, self.changed, self.removed):
854 for node in chain(self.added, self.changed, self.removed):
855 yield node.path
855 yield node.path
856
856
857 def get_paths(self):
857 def get_paths(self):
858 """
858 """
859 Returns list of paths from nodes marked as added, changed or removed.
859 Returns list of paths from nodes marked as added, changed or removed.
860 """
860 """
861 return list(self.get_ipaths())
861 return list(self.get_ipaths())
862
862
863 def check_integrity(self, parents=None):
863 def check_integrity(self, parents=None):
864 """
864 """
865 Checks in-memory changeset's integrity. Also, sets parents if not
865 Checks in-memory changeset's integrity. Also, sets parents if not
866 already set.
866 already set.
867
867
868 :raises CommitError: if any error occurs (i.e.
868 :raises CommitError: if any error occurs (i.e.
869 ``NodeDoesNotExistError``).
869 ``NodeDoesNotExistError``).
870 """
870 """
871 if not self.parents:
871 if not self.parents:
872 parents = parents or []
872 parents = parents or []
873 if len(parents) == 0:
873 if len(parents) == 0:
874 try:
874 try:
875 parents = [self.repository.get_changeset(), None]
875 parents = [self.repository.get_changeset(), None]
876 except EmptyRepositoryError:
876 except EmptyRepositoryError:
877 parents = [None, None]
877 parents = [None, None]
878 elif len(parents) == 1:
878 elif len(parents) == 1:
879 parents += [None]
879 parents += [None]
880 self.parents = parents
880 self.parents = parents
881
881
882 # Local parents, only if not None
882 # Local parents, only if not None
883 parents = [p for p in self.parents if p]
883 parents = [p for p in self.parents if p]
884
884
885 # Check nodes marked as added
885 # Check nodes marked as added
886 for p in parents:
886 for p in parents:
887 for node in self.added:
887 for node in self.added:
888 try:
888 try:
889 p.get_node(node.path)
889 p.get_node(node.path)
890 except NodeDoesNotExistError:
890 except NodeDoesNotExistError:
891 pass
891 pass
892 else:
892 else:
893 raise NodeAlreadyExistsError("Node at %s already exists "
893 raise NodeAlreadyExistsError("Node at %s already exists "
894 "at %s" % (node.path, p))
894 "at %s" % (node.path, p))
895
895
896 # Check nodes marked as changed
896 # Check nodes marked as changed
897 missing = set(self.changed)
897 missing = set(self.changed)
898 not_changed = set(self.changed)
898 not_changed = set(self.changed)
899 if self.changed and not parents:
899 if self.changed and not parents:
900 raise NodeDoesNotExistError(str(self.changed[0].path))
900 raise NodeDoesNotExistError(str(self.changed[0].path))
901 for p in parents:
901 for p in parents:
902 for node in self.changed:
902 for node in self.changed:
903 try:
903 try:
904 old = p.get_node(node.path)
904 old = p.get_node(node.path)
905 missing.remove(node)
905 missing.remove(node)
906 if old.content != node.content:
906 if old.content != node.content:
907 not_changed.remove(node)
907 not_changed.remove(node)
908 except NodeDoesNotExistError:
908 except NodeDoesNotExistError:
909 pass
909 pass
910 if self.changed and missing:
910 if self.changed and missing:
911 raise NodeDoesNotExistError("Node at %s is missing "
911 raise NodeDoesNotExistError("Node at %s is missing "
912 "(parents: %s)" % (node.path, parents))
912 "(parents: %s)" % (node.path, parents))
913
913
914 if self.changed and not_changed:
914 if self.changed and not_changed:
915 raise NodeNotChangedError("Node at %s wasn't actually changed "
915 raise NodeNotChangedError("Node at %s wasn't actually changed "
916 "since parents' changesets: %s" % (not_changed.pop().path,
916 "since parents' changesets: %s" % (not_changed.pop().path,
917 parents)
917 parents)
918 )
918 )
919
919
920 # Check nodes marked as removed
920 # Check nodes marked as removed
921 if self.removed and not parents:
921 if self.removed and not parents:
922 raise NodeDoesNotExistError("Cannot remove node at %s as there "
922 raise NodeDoesNotExistError("Cannot remove node at %s as there "
923 "were no parents specified" % self.removed[0].path)
923 "were no parents specified" % self.removed[0].path)
924 really_removed = set()
924 really_removed = set()
925 for p in parents:
925 for p in parents:
926 for node in self.removed:
926 for node in self.removed:
927 try:
927 try:
928 p.get_node(node.path)
928 p.get_node(node.path)
929 really_removed.add(node)
929 really_removed.add(node)
930 except ChangesetError:
930 except ChangesetError:
931 pass
931 pass
932 not_removed = set(self.removed) - really_removed
932 not_removed = set(self.removed) - really_removed
933 if not_removed:
933 if not_removed:
934 raise NodeDoesNotExistError("Cannot remove node at %s from "
934 raise NodeDoesNotExistError("Cannot remove node at %s from "
935 "following parents: %s" % (not_removed[0], parents))
935 "following parents: %s" % (not_removed[0], parents))
936
936
937 def commit(self, message, author, parents=None, branch=None, date=None,
937 def commit(self, message, author, parents=None, branch=None, date=None,
938 **kwargs):
938 **kwargs):
939 """
939 """
940 Performs in-memory commit (doesn't check workdir in any way) and
940 Performs in-memory commit (doesn't check workdir in any way) and
941 returns newly created ``Changeset``. Updates repository's
941 returns newly created ``Changeset``. Updates repository's
942 ``revisions``.
942 ``revisions``.
943
943
944 .. note::
944 .. note::
945 While overriding this method each backend's should call
945 While overriding this method each backend's should call
946 ``self.check_integrity(parents)`` in the first place.
946 ``self.check_integrity(parents)`` in the first place.
947
947
948 :param message: message of the commit
948 :param message: message of the commit
949 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
949 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
950 :param parents: single parent or sequence of parents from which commit
950 :param parents: single parent or sequence of parents from which commit
951 would be derieved
951 would be derieved
952 :param date: ``datetime.datetime`` instance. Defaults to
952 :param date: ``datetime.datetime`` instance. Defaults to
953 ``datetime.datetime.now()``.
953 ``datetime.datetime.now()``.
954 :param branch: branch name, as string. If none given, default backend's
954 :param branch: branch name, as string. If none given, default backend's
955 branch would be used.
955 branch would be used.
956
956
957 :raises ``CommitError``: if any error occurs while committing
957 :raises ``CommitError``: if any error occurs while committing
958 """
958 """
959 raise NotImplementedError
959 raise NotImplementedError
960
960
961
961
962 class EmptyChangeset(BaseChangeset):
962 class EmptyChangeset(BaseChangeset):
963 """
963 """
964 An dummy empty changeset. It's possible to pass hash when creating
964 An dummy empty changeset. It's possible to pass hash when creating
965 an EmptyChangeset
965 an EmptyChangeset
966 """
966 """
967
967
968 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
968 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
969 alias=None, revision=-1, message='', author='', date=None):
969 alias=None, revision=-1, message='', author='', date=None):
970 self._empty_cs = cs
970 self._empty_cs = cs
971 self.revision = revision
971 self.revision = revision
972 self.message = message
972 self.message = message
973 self.author = author
973 self.author = author
974 self.date = date or datetime.datetime.fromtimestamp(0)
974 self.date = date or datetime.datetime.fromtimestamp(0)
975 self.repository = repo
975 self.repository = repo
976 self.requested_revision = requested_revision
976 self.requested_revision = requested_revision
977 self.alias = alias
977 self.alias = alias
978
978
979 @LazyProperty
979 @LazyProperty
980 def raw_id(self):
980 def raw_id(self):
981 """
981 """
982 Returns raw string identifying this changeset, useful for web
982 Returns raw string identifying this changeset, useful for web
983 representation.
983 representation.
984 """
984 """
985
985
986 return self._empty_cs
986 return self._empty_cs
987
987
988 @LazyProperty
988 @LazyProperty
989 def branch(self):
989 def branch(self):
990 from rhodecode.lib.vcs.backends import get_backend
990 from rhodecode.lib.vcs.backends import get_backend
991 return get_backend(self.alias).DEFAULT_BRANCH_NAME
991 return get_backend(self.alias).DEFAULT_BRANCH_NAME
992
992
993 @LazyProperty
993 @LazyProperty
994 def short_id(self):
994 def short_id(self):
995 return self.raw_id[:12]
995 return self.raw_id[:12]
996
996
997 def get_file_changeset(self, path):
997 def get_file_changeset(self, path):
998 return self
998 return self
999
999
1000 def get_file_content(self, path):
1000 def get_file_content(self, path):
1001 return u''
1001 return u''
1002
1002
1003 def get_file_size(self, path):
1003 def get_file_size(self, path):
1004 return 0
1004 return 0
1005
1006
1007 class CollectionGenerator(object):
1008
1009 def __init__(self, repo, revs):
1010 self.repo = repo
1011 self.revs = revs
1012
1013 def __len__(self):
1014 return len(self.revs)
1015
1016 def __iter__(self):
1017 for rev in self.revs:
1018 yield self.repo.get_changeset(rev)
1019
1020 def __getslice__(self, i, j):
1021 """
1022 Returns a iterator of sliced repository
1023 """
1024 sliced_revs = self.revs[i:j]
1025 return CollectionGenerator(self.repo, sliced_revs)
1026
1027 def __repr__(self):
1028 return 'CollectionGenerator<%s>' % (len(self))
@@ -1,693 +1,692 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 import logging
16 import logging
17 import traceback
17 import traceback
18 import urllib
18 import urllib
19 import urllib2
19 import urllib2
20 from dulwich.repo import Repo, NotGitRepository
20 from dulwich.repo import Repo, NotGitRepository
21 from dulwich.objects import Tag
21 from dulwich.objects import Tag
22 from string import Template
22 from string import Template
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.lib.vcs.backends.base import BaseRepository
25 from rhodecode.lib.vcs.backends.base import BaseRepository, CollectionGenerator
26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
29 from rhodecode.lib.vcs.exceptions import RepositoryError
29 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
35 from rhodecode.lib.vcs.utils.paths import abspath
35 from rhodecode.lib.vcs.utils.paths import abspath
36 from rhodecode.lib.vcs.utils.paths import get_user_home
36 from rhodecode.lib.vcs.utils.paths import get_user_home
37 from .workdir import GitWorkdir
37 from .workdir import GitWorkdir
38 from .changeset import GitChangeset
38 from .changeset import GitChangeset
39 from .inmemory import GitInMemoryChangeset
39 from .inmemory import GitInMemoryChangeset
40 from .config import ConfigFile
40 from .config import ConfigFile
41 from rhodecode.lib import subprocessio
41 from rhodecode.lib import subprocessio
42
42
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class GitRepository(BaseRepository):
47 class GitRepository(BaseRepository):
48 """
48 """
49 Git repository backend.
49 Git repository backend.
50 """
50 """
51 DEFAULT_BRANCH_NAME = 'master'
51 DEFAULT_BRANCH_NAME = 'master'
52 scm = 'git'
52 scm = 'git'
53
53
54 def __init__(self, repo_path, create=False, src_url=None,
54 def __init__(self, repo_path, create=False, src_url=None,
55 update_after_clone=False, bare=False):
55 update_after_clone=False, bare=False):
56
56
57 self.path = abspath(repo_path)
57 self.path = abspath(repo_path)
58 repo = self._get_repo(create, src_url, update_after_clone, bare)
58 repo = self._get_repo(create, src_url, update_after_clone, bare)
59 self.bare = repo.bare
59 self.bare = repo.bare
60
60
61 self._config_files = [
61 self._config_files = [
62 bare and abspath(self.path, 'config')
62 bare and abspath(self.path, 'config')
63 or abspath(self.path, '.git', 'config'),
63 or abspath(self.path, '.git', 'config'),
64 abspath(get_user_home(), '.gitconfig'),
64 abspath(get_user_home(), '.gitconfig'),
65 ]
65 ]
66
66
67 @property
67 @property
68 def _repo(self):
68 def _repo(self):
69 return Repo(self.path)
69 return Repo(self.path)
70
70
71 @property
71 @property
72 def head(self):
72 def head(self):
73 try:
73 try:
74 return self._repo.head()
74 return self._repo.head()
75 except KeyError:
75 except KeyError:
76 return None
76 return None
77
77
78 @LazyProperty
78 @LazyProperty
79 def revisions(self):
79 def revisions(self):
80 """
80 """
81 Returns list of revisions' ids, in ascending order. Being lazy
81 Returns list of revisions' ids, in ascending order. Being lazy
82 attribute allows external tools to inject shas from cache.
82 attribute allows external tools to inject shas from cache.
83 """
83 """
84 return self._get_all_revisions()
84 return self._get_all_revisions()
85
85
86 @classmethod
86 @classmethod
87 def _run_git_command(cls, cmd, **opts):
87 def _run_git_command(cls, cmd, **opts):
88 """
88 """
89 Runs given ``cmd`` as git command and returns tuple
89 Runs given ``cmd`` as git command and returns tuple
90 (stdout, stderr).
90 (stdout, stderr).
91
91
92 :param cmd: git command to be executed
92 :param cmd: git command to be executed
93 :param opts: env options to pass into Subprocess command
93 :param opts: env options to pass into Subprocess command
94 """
94 """
95
95
96 if '_bare' in opts:
96 if '_bare' in opts:
97 _copts = []
97 _copts = []
98 del opts['_bare']
98 del opts['_bare']
99 else:
99 else:
100 _copts = ['-c', 'core.quotepath=false', ]
100 _copts = ['-c', 'core.quotepath=false', ]
101 safe_call = False
101 safe_call = False
102 if '_safe' in opts:
102 if '_safe' in opts:
103 #no exc on failure
103 #no exc on failure
104 del opts['_safe']
104 del opts['_safe']
105 safe_call = True
105 safe_call = True
106
106
107 _str_cmd = False
107 _str_cmd = False
108 if isinstance(cmd, basestring):
108 if isinstance(cmd, basestring):
109 cmd = [cmd]
109 cmd = [cmd]
110 _str_cmd = True
110 _str_cmd = True
111
111
112 gitenv = os.environ
112 gitenv = os.environ
113 # need to clean fix GIT_DIR !
113 # need to clean fix GIT_DIR !
114 if 'GIT_DIR' in gitenv:
114 if 'GIT_DIR' in gitenv:
115 del gitenv['GIT_DIR']
115 del gitenv['GIT_DIR']
116 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
116 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
117
117
118 _git_path = rhodecode.CONFIG.get('git_path', 'git')
118 _git_path = rhodecode.CONFIG.get('git_path', 'git')
119 cmd = [_git_path] + _copts + cmd
119 cmd = [_git_path] + _copts + cmd
120 if _str_cmd:
120 if _str_cmd:
121 cmd = ' '.join(cmd)
121 cmd = ' '.join(cmd)
122 try:
122 try:
123 _opts = dict(
123 _opts = dict(
124 env=gitenv,
124 env=gitenv,
125 shell=False,
125 shell=False,
126 )
126 )
127 _opts.update(opts)
127 _opts.update(opts)
128 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
128 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
129 except (EnvironmentError, OSError), err:
129 except (EnvironmentError, OSError), err:
130 tb_err = ("Couldn't run git command (%s).\n"
130 tb_err = ("Couldn't run git command (%s).\n"
131 "Original error was:%s\n" % (cmd, err))
131 "Original error was:%s\n" % (cmd, err))
132 log.error(tb_err)
132 log.error(tb_err)
133 if safe_call:
133 if safe_call:
134 return '', err
134 return '', err
135 else:
135 else:
136 raise RepositoryError(tb_err)
136 raise RepositoryError(tb_err)
137
137
138 return ''.join(p.output), ''.join(p.error)
138 return ''.join(p.output), ''.join(p.error)
139
139
140 def run_git_command(self, cmd):
140 def run_git_command(self, cmd):
141 opts = {}
141 opts = {}
142 if os.path.isdir(self.path):
142 if os.path.isdir(self.path):
143 opts['cwd'] = self.path
143 opts['cwd'] = self.path
144 return self._run_git_command(cmd, **opts)
144 return self._run_git_command(cmd, **opts)
145
145
146 @classmethod
146 @classmethod
147 def _check_url(cls, url):
147 def _check_url(cls, url):
148 """
148 """
149 Functon will check given url and try to verify if it's a valid
149 Functon will check given url and try to verify if it's a valid
150 link. Sometimes it may happened that mercurial will issue basic
150 link. Sometimes it may happened that mercurial will issue basic
151 auth request that can cause whole API to hang when used from python
151 auth request that can cause whole API to hang when used from python
152 or other external calls.
152 or other external calls.
153
153
154 On failures it'll raise urllib2.HTTPError
154 On failures it'll raise urllib2.HTTPError
155 """
155 """
156 from mercurial.util import url as Url
156 from mercurial.util import url as Url
157
157
158 # those authnadlers are patched for python 2.6.5 bug an
158 # those authnadlers are patched for python 2.6.5 bug an
159 # infinit looping when given invalid resources
159 # infinit looping when given invalid resources
160 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
160 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
161
161
162 # check first if it's not an local url
162 # check first if it's not an local url
163 if os.path.isdir(url) or url.startswith('file:'):
163 if os.path.isdir(url) or url.startswith('file:'):
164 return True
164 return True
165
165
166 if('+' in url[:url.find('://')]):
166 if('+' in url[:url.find('://')]):
167 url = url[url.find('+') + 1:]
167 url = url[url.find('+') + 1:]
168
168
169 handlers = []
169 handlers = []
170 test_uri, authinfo = Url(url).authinfo()
170 test_uri, authinfo = Url(url).authinfo()
171 if not test_uri.endswith('info/refs'):
171 if not test_uri.endswith('info/refs'):
172 test_uri = test_uri.rstrip('/') + '/info/refs'
172 test_uri = test_uri.rstrip('/') + '/info/refs'
173 if authinfo:
173 if authinfo:
174 #create a password manager
174 #create a password manager
175 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
175 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
176 passmgr.add_password(*authinfo)
176 passmgr.add_password(*authinfo)
177
177
178 handlers.extend((httpbasicauthhandler(passmgr),
178 handlers.extend((httpbasicauthhandler(passmgr),
179 httpdigestauthhandler(passmgr)))
179 httpdigestauthhandler(passmgr)))
180
180
181 o = urllib2.build_opener(*handlers)
181 o = urllib2.build_opener(*handlers)
182 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
182 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
183
183
184 q = {"service": 'git-upload-pack'}
184 q = {"service": 'git-upload-pack'}
185 qs = '?%s' % urllib.urlencode(q)
185 qs = '?%s' % urllib.urlencode(q)
186 cu = "%s%s" % (test_uri, qs)
186 cu = "%s%s" % (test_uri, qs)
187 req = urllib2.Request(cu, None, {})
187 req = urllib2.Request(cu, None, {})
188
188
189 try:
189 try:
190 resp = o.open(req)
190 resp = o.open(req)
191 return resp.code == 200
191 return resp.code == 200
192 except Exception, e:
192 except Exception, e:
193 # means it cannot be cloned
193 # means it cannot be cloned
194 raise urllib2.URLError("[%s] %s" % (url, e))
194 raise urllib2.URLError("[%s] %s" % (url, e))
195
195
196 def _get_repo(self, create, src_url=None, update_after_clone=False,
196 def _get_repo(self, create, src_url=None, update_after_clone=False,
197 bare=False):
197 bare=False):
198 if create and os.path.exists(self.path):
198 if create and os.path.exists(self.path):
199 raise RepositoryError("Location already exist")
199 raise RepositoryError("Location already exist")
200 if src_url and not create:
200 if src_url and not create:
201 raise RepositoryError("Create should be set to True if src_url is "
201 raise RepositoryError("Create should be set to True if src_url is "
202 "given (clone operation creates repository)")
202 "given (clone operation creates repository)")
203 try:
203 try:
204 if create and src_url:
204 if create and src_url:
205 GitRepository._check_url(src_url)
205 GitRepository._check_url(src_url)
206 self.clone(src_url, update_after_clone, bare)
206 self.clone(src_url, update_after_clone, bare)
207 return Repo(self.path)
207 return Repo(self.path)
208 elif create:
208 elif create:
209 os.mkdir(self.path)
209 os.mkdir(self.path)
210 if bare:
210 if bare:
211 return Repo.init_bare(self.path)
211 return Repo.init_bare(self.path)
212 else:
212 else:
213 return Repo.init(self.path)
213 return Repo.init(self.path)
214 else:
214 else:
215 return self._repo
215 return self._repo
216 except (NotGitRepository, OSError), err:
216 except (NotGitRepository, OSError), err:
217 raise RepositoryError(err)
217 raise RepositoryError(err)
218
218
219 def _get_all_revisions(self):
219 def _get_all_revisions(self):
220 # we must check if this repo is not empty, since later command
220 # we must check if this repo is not empty, since later command
221 # fails if it is. And it's cheaper to ask than throw the subprocess
221 # fails if it is. And it's cheaper to ask than throw the subprocess
222 # errors
222 # errors
223 try:
223 try:
224 self._repo.head()
224 self._repo.head()
225 except KeyError:
225 except KeyError:
226 return []
226 return []
227 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
227 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
228 '--all').strip()
228 '--all').strip()
229 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
229 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
230 try:
230 try:
231 so, se = self.run_git_command(cmd)
231 so, se = self.run_git_command(cmd)
232 except RepositoryError:
232 except RepositoryError:
233 # Can be raised for empty repositories
233 # Can be raised for empty repositories
234 return []
234 return []
235 return so.splitlines()
235 return so.splitlines()
236
236
237 def _get_all_revisions2(self):
237 def _get_all_revisions2(self):
238 #alternate implementation using dulwich
238 #alternate implementation using dulwich
239 includes = [x[1][0] for x in self._parsed_refs.iteritems()
239 includes = [x[1][0] for x in self._parsed_refs.iteritems()
240 if x[1][1] != 'T']
240 if x[1][1] != 'T']
241 return [c.commit.id for c in self._repo.get_walker(include=includes)]
241 return [c.commit.id for c in self._repo.get_walker(include=includes)]
242
242
243 def _get_revision(self, revision):
243 def _get_revision(self, revision):
244 """
244 """
245 For git backend we always return integer here. This way we ensure
245 For git backend we always return integer here. This way we ensure
246 that changset's revision attribute would become integer.
246 that changset's revision attribute would become integer.
247 """
247 """
248 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
248 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
249 is_bstr = lambda o: isinstance(o, (str, unicode))
249 is_bstr = lambda o: isinstance(o, (str, unicode))
250 is_null = lambda o: len(o) == revision.count('0')
250 is_null = lambda o: len(o) == revision.count('0')
251
251
252 if len(self.revisions) == 0:
252 if len(self.revisions) == 0:
253 raise EmptyRepositoryError("There are no changesets yet")
253 raise EmptyRepositoryError("There are no changesets yet")
254
254
255 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
255 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
256 revision = self.revisions[-1]
256 revision = self.revisions[-1]
257
257
258 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
258 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
259 or isinstance(revision, int) or is_null(revision)):
259 or isinstance(revision, int) or is_null(revision)):
260 try:
260 try:
261 revision = self.revisions[int(revision)]
261 revision = self.revisions[int(revision)]
262 except Exception:
262 except Exception:
263 raise ChangesetDoesNotExistError("Revision %s does not exist "
263 raise ChangesetDoesNotExistError("Revision %s does not exist "
264 "for this repository" % (revision))
264 "for this repository" % (revision))
265
265
266 elif is_bstr(revision):
266 elif is_bstr(revision):
267 # get by branch/tag name
267 # get by branch/tag name
268 _ref_revision = self._parsed_refs.get(revision)
268 _ref_revision = self._parsed_refs.get(revision)
269 _tags_shas = self.tags.values()
269 _tags_shas = self.tags.values()
270 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
270 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
271 return _ref_revision[0]
271 return _ref_revision[0]
272
272
273 # maybe it's a tag ? we don't have them in self.revisions
273 # maybe it's a tag ? we don't have them in self.revisions
274 elif revision in _tags_shas:
274 elif revision in _tags_shas:
275 return _tags_shas[_tags_shas.index(revision)]
275 return _tags_shas[_tags_shas.index(revision)]
276
276
277 elif not pattern.match(revision) or revision not in self.revisions:
277 elif not pattern.match(revision) or revision not in self.revisions:
278 raise ChangesetDoesNotExistError("Revision %s does not exist "
278 raise ChangesetDoesNotExistError("Revision %s does not exist "
279 "for this repository" % (revision))
279 "for this repository" % (revision))
280
280
281 # Ensure we return full id
281 # Ensure we return full id
282 if not pattern.match(str(revision)):
282 if not pattern.match(str(revision)):
283 raise ChangesetDoesNotExistError("Given revision %s not recognized"
283 raise ChangesetDoesNotExistError("Given revision %s not recognized"
284 % revision)
284 % revision)
285 return revision
285 return revision
286
286
287 def _get_archives(self, archive_name='tip'):
287 def _get_archives(self, archive_name='tip'):
288
288
289 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
289 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
290 yield {"type": i[0], "extension": i[1], "node": archive_name}
290 yield {"type": i[0], "extension": i[1], "node": archive_name}
291
291
292 def _get_url(self, url):
292 def _get_url(self, url):
293 """
293 """
294 Returns normalized url. If schema is not given, would fall to
294 Returns normalized url. If schema is not given, would fall to
295 filesystem (``file:///``) schema.
295 filesystem (``file:///``) schema.
296 """
296 """
297 url = str(url)
297 url = str(url)
298 if url != 'default' and not '://' in url:
298 if url != 'default' and not '://' in url:
299 url = ':///'.join(('file', url))
299 url = ':///'.join(('file', url))
300 return url
300 return url
301
301
302 def get_hook_location(self):
302 def get_hook_location(self):
303 """
303 """
304 returns absolute path to location where hooks are stored
304 returns absolute path to location where hooks are stored
305 """
305 """
306 loc = os.path.join(self.path, 'hooks')
306 loc = os.path.join(self.path, 'hooks')
307 if not self.bare:
307 if not self.bare:
308 loc = os.path.join(self.path, '.git', 'hooks')
308 loc = os.path.join(self.path, '.git', 'hooks')
309 return loc
309 return loc
310
310
311 @LazyProperty
311 @LazyProperty
312 def name(self):
312 def name(self):
313 return os.path.basename(self.path)
313 return os.path.basename(self.path)
314
314
315 @LazyProperty
315 @LazyProperty
316 def last_change(self):
316 def last_change(self):
317 """
317 """
318 Returns last change made on this repository as datetime object
318 Returns last change made on this repository as datetime object
319 """
319 """
320 return date_fromtimestamp(self._get_mtime(), makedate()[1])
320 return date_fromtimestamp(self._get_mtime(), makedate()[1])
321
321
322 def _get_mtime(self):
322 def _get_mtime(self):
323 try:
323 try:
324 return time.mktime(self.get_changeset().date.timetuple())
324 return time.mktime(self.get_changeset().date.timetuple())
325 except RepositoryError:
325 except RepositoryError:
326 idx_loc = '' if self.bare else '.git'
326 idx_loc = '' if self.bare else '.git'
327 # fallback to filesystem
327 # fallback to filesystem
328 in_path = os.path.join(self.path, idx_loc, "index")
328 in_path = os.path.join(self.path, idx_loc, "index")
329 he_path = os.path.join(self.path, idx_loc, "HEAD")
329 he_path = os.path.join(self.path, idx_loc, "HEAD")
330 if os.path.exists(in_path):
330 if os.path.exists(in_path):
331 return os.stat(in_path).st_mtime
331 return os.stat(in_path).st_mtime
332 else:
332 else:
333 return os.stat(he_path).st_mtime
333 return os.stat(he_path).st_mtime
334
334
335 @LazyProperty
335 @LazyProperty
336 def description(self):
336 def description(self):
337 idx_loc = '' if self.bare else '.git'
337 idx_loc = '' if self.bare else '.git'
338 undefined_description = u'unknown'
338 undefined_description = u'unknown'
339 description_path = os.path.join(self.path, idx_loc, 'description')
339 description_path = os.path.join(self.path, idx_loc, 'description')
340 if os.path.isfile(description_path):
340 if os.path.isfile(description_path):
341 return safe_unicode(open(description_path).read())
341 return safe_unicode(open(description_path).read())
342 else:
342 else:
343 return undefined_description
343 return undefined_description
344
344
345 @LazyProperty
345 @LazyProperty
346 def contact(self):
346 def contact(self):
347 undefined_contact = u'Unknown'
347 undefined_contact = u'Unknown'
348 return undefined_contact
348 return undefined_contact
349
349
350 @property
350 @property
351 def branches(self):
351 def branches(self):
352 if not self.revisions:
352 if not self.revisions:
353 return {}
353 return {}
354 sortkey = lambda ctx: ctx[0]
354 sortkey = lambda ctx: ctx[0]
355 _branches = [(x[0], x[1][0])
355 _branches = [(x[0], x[1][0])
356 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
356 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
357 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
357 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
358
358
359 @LazyProperty
359 @LazyProperty
360 def tags(self):
360 def tags(self):
361 return self._get_tags()
361 return self._get_tags()
362
362
363 def _get_tags(self):
363 def _get_tags(self):
364 if not self.revisions:
364 if not self.revisions:
365 return {}
365 return {}
366
366
367 sortkey = lambda ctx: ctx[0]
367 sortkey = lambda ctx: ctx[0]
368 _tags = [(x[0], x[1][0])
368 _tags = [(x[0], x[1][0])
369 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
369 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
370 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
370 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
371
371
372 def tag(self, name, user, revision=None, message=None, date=None,
372 def tag(self, name, user, revision=None, message=None, date=None,
373 **kwargs):
373 **kwargs):
374 """
374 """
375 Creates and returns a tag for the given ``revision``.
375 Creates and returns a tag for the given ``revision``.
376
376
377 :param name: name for new tag
377 :param name: name for new tag
378 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
378 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
379 :param revision: changeset id for which new tag would be created
379 :param revision: changeset id for which new tag would be created
380 :param message: message of the tag's commit
380 :param message: message of the tag's commit
381 :param date: date of tag's commit
381 :param date: date of tag's commit
382
382
383 :raises TagAlreadyExistError: if tag with same name already exists
383 :raises TagAlreadyExistError: if tag with same name already exists
384 """
384 """
385 if name in self.tags:
385 if name in self.tags:
386 raise TagAlreadyExistError("Tag %s already exists" % name)
386 raise TagAlreadyExistError("Tag %s already exists" % name)
387 changeset = self.get_changeset(revision)
387 changeset = self.get_changeset(revision)
388 message = message or "Added tag %s for commit %s" % (name,
388 message = message or "Added tag %s for commit %s" % (name,
389 changeset.raw_id)
389 changeset.raw_id)
390 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
390 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
391
391
392 self._parsed_refs = self._get_parsed_refs()
392 self._parsed_refs = self._get_parsed_refs()
393 self.tags = self._get_tags()
393 self.tags = self._get_tags()
394 return changeset
394 return changeset
395
395
396 def remove_tag(self, name, user, message=None, date=None):
396 def remove_tag(self, name, user, message=None, date=None):
397 """
397 """
398 Removes tag with the given ``name``.
398 Removes tag with the given ``name``.
399
399
400 :param name: name of the tag to be removed
400 :param name: name of the tag to be removed
401 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
401 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
402 :param message: message of the tag's removal commit
402 :param message: message of the tag's removal commit
403 :param date: date of tag's removal commit
403 :param date: date of tag's removal commit
404
404
405 :raises TagDoesNotExistError: if tag with given name does not exists
405 :raises TagDoesNotExistError: if tag with given name does not exists
406 """
406 """
407 if name not in self.tags:
407 if name not in self.tags:
408 raise TagDoesNotExistError("Tag %s does not exist" % name)
408 raise TagDoesNotExistError("Tag %s does not exist" % name)
409 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
409 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
410 try:
410 try:
411 os.remove(tagpath)
411 os.remove(tagpath)
412 self._parsed_refs = self._get_parsed_refs()
412 self._parsed_refs = self._get_parsed_refs()
413 self.tags = self._get_tags()
413 self.tags = self._get_tags()
414 except OSError, e:
414 except OSError, e:
415 raise RepositoryError(e.strerror)
415 raise RepositoryError(e.strerror)
416
416
417 @LazyProperty
417 @LazyProperty
418 def _parsed_refs(self):
418 def _parsed_refs(self):
419 return self._get_parsed_refs()
419 return self._get_parsed_refs()
420
420
421 def _get_parsed_refs(self):
421 def _get_parsed_refs(self):
422 # cache the property
422 # cache the property
423 _repo = self._repo
423 _repo = self._repo
424 refs = _repo.get_refs()
424 refs = _repo.get_refs()
425 keys = [('refs/heads/', 'H'),
425 keys = [('refs/heads/', 'H'),
426 ('refs/remotes/origin/', 'RH'),
426 ('refs/remotes/origin/', 'RH'),
427 ('refs/tags/', 'T')]
427 ('refs/tags/', 'T')]
428 _refs = {}
428 _refs = {}
429 for ref, sha in refs.iteritems():
429 for ref, sha in refs.iteritems():
430 for k, type_ in keys:
430 for k, type_ in keys:
431 if ref.startswith(k):
431 if ref.startswith(k):
432 _key = ref[len(k):]
432 _key = ref[len(k):]
433 if type_ == 'T':
433 if type_ == 'T':
434 obj = _repo.get_object(sha)
434 obj = _repo.get_object(sha)
435 if isinstance(obj, Tag):
435 if isinstance(obj, Tag):
436 sha = _repo.get_object(sha).object[1]
436 sha = _repo.get_object(sha).object[1]
437 _refs[_key] = [sha, type_]
437 _refs[_key] = [sha, type_]
438 break
438 break
439 return _refs
439 return _refs
440
440
441 def _heads(self, reverse=False):
441 def _heads(self, reverse=False):
442 refs = self._repo.get_refs()
442 refs = self._repo.get_refs()
443 heads = {}
443 heads = {}
444
444
445 for key, val in refs.items():
445 for key, val in refs.items():
446 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
446 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
447 if key.startswith(ref_key):
447 if key.startswith(ref_key):
448 n = key[len(ref_key):]
448 n = key[len(ref_key):]
449 if n not in ['HEAD']:
449 if n not in ['HEAD']:
450 heads[n] = val
450 heads[n] = val
451
451
452 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
452 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
453
453
454 def get_changeset(self, revision=None):
454 def get_changeset(self, revision=None):
455 """
455 """
456 Returns ``GitChangeset`` object representing commit from git repository
456 Returns ``GitChangeset`` object representing commit from git repository
457 at the given revision or head (most recent commit) if None given.
457 at the given revision or head (most recent commit) if None given.
458 """
458 """
459 if isinstance(revision, GitChangeset):
459 if isinstance(revision, GitChangeset):
460 return revision
460 return revision
461 revision = self._get_revision(revision)
461 revision = self._get_revision(revision)
462 changeset = GitChangeset(repository=self, revision=revision)
462 changeset = GitChangeset(repository=self, revision=revision)
463 return changeset
463 return changeset
464
464
465 def get_changesets(self, start=None, end=None, start_date=None,
465 def get_changesets(self, start=None, end=None, start_date=None,
466 end_date=None, branch_name=None, reverse=False):
466 end_date=None, branch_name=None, reverse=False):
467 """
467 """
468 Returns iterator of ``GitChangeset`` objects from start to end (both
468 Returns iterator of ``GitChangeset`` objects from start to end (both
469 are inclusive), in ascending date order (unless ``reverse`` is set).
469 are inclusive), in ascending date order (unless ``reverse`` is set).
470
470
471 :param start: changeset ID, as str; first returned changeset
471 :param start: changeset ID, as str; first returned changeset
472 :param end: changeset ID, as str; last returned changeset
472 :param end: changeset ID, as str; last returned changeset
473 :param start_date: if specified, changesets with commit date less than
473 :param start_date: if specified, changesets with commit date less than
474 ``start_date`` would be filtered out from returned set
474 ``start_date`` would be filtered out from returned set
475 :param end_date: if specified, changesets with commit date greater than
475 :param end_date: if specified, changesets with commit date greater than
476 ``end_date`` would be filtered out from returned set
476 ``end_date`` would be filtered out from returned set
477 :param branch_name: if specified, changesets not reachable from given
477 :param branch_name: if specified, changesets not reachable from given
478 branch would be filtered out from returned set
478 branch would be filtered out from returned set
479 :param reverse: if ``True``, returned generator would be reversed
479 :param reverse: if ``True``, returned generator would be reversed
480 (meaning that returned changesets would have descending date order)
480 (meaning that returned changesets would have descending date order)
481
481
482 :raise BranchDoesNotExistError: If given ``branch_name`` does not
482 :raise BranchDoesNotExistError: If given ``branch_name`` does not
483 exist.
483 exist.
484 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
484 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
485 ``end`` could not be found.
485 ``end`` could not be found.
486
486
487 """
487 """
488 if branch_name and branch_name not in self.branches:
488 if branch_name and branch_name not in self.branches:
489 raise BranchDoesNotExistError("Branch '%s' not found" \
489 raise BranchDoesNotExistError("Branch '%s' not found" \
490 % branch_name)
490 % branch_name)
491 # %H at format means (full) commit hash, initial hashes are retrieved
491 # %H at format means (full) commit hash, initial hashes are retrieved
492 # in ascending date order
492 # in ascending date order
493 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
493 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
494 cmd_params = {}
494 cmd_params = {}
495 if start_date:
495 if start_date:
496 cmd_template += ' --since "$since"'
496 cmd_template += ' --since "$since"'
497 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
497 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
498 if end_date:
498 if end_date:
499 cmd_template += ' --until "$until"'
499 cmd_template += ' --until "$until"'
500 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
500 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
501 if branch_name:
501 if branch_name:
502 cmd_template += ' $branch_name'
502 cmd_template += ' $branch_name'
503 cmd_params['branch_name'] = branch_name
503 cmd_params['branch_name'] = branch_name
504 else:
504 else:
505 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
505 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
506 '--all').strip()
506 '--all').strip()
507 cmd_template += ' %s' % (rev_filter)
507 cmd_template += ' %s' % (rev_filter)
508
508
509 cmd = Template(cmd_template).safe_substitute(**cmd_params)
509 cmd = Template(cmd_template).safe_substitute(**cmd_params)
510 revs = self.run_git_command(cmd)[0].splitlines()
510 revs = self.run_git_command(cmd)[0].splitlines()
511 start_pos = 0
511 start_pos = 0
512 end_pos = len(revs)
512 end_pos = len(revs)
513 if start:
513 if start:
514 _start = self._get_revision(start)
514 _start = self._get_revision(start)
515 try:
515 try:
516 start_pos = revs.index(_start)
516 start_pos = revs.index(_start)
517 except ValueError:
517 except ValueError:
518 pass
518 pass
519
519
520 if end is not None:
520 if end is not None:
521 _end = self._get_revision(end)
521 _end = self._get_revision(end)
522 try:
522 try:
523 end_pos = revs.index(_end)
523 end_pos = revs.index(_end)
524 except ValueError:
524 except ValueError:
525 pass
525 pass
526
526
527 if None not in [start, end] and start_pos > end_pos:
527 if None not in [start, end] and start_pos > end_pos:
528 raise RepositoryError('start cannot be after end')
528 raise RepositoryError('start cannot be after end')
529
529
530 if end_pos is not None:
530 if end_pos is not None:
531 end_pos += 1
531 end_pos += 1
532
532
533 revs = revs[start_pos:end_pos]
533 revs = revs[start_pos:end_pos]
534 if reverse:
534 if reverse:
535 revs = reversed(revs)
535 revs = reversed(revs)
536 for rev in revs:
536 return CollectionGenerator(self, revs)
537 yield self.get_changeset(rev)
538
537
539 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
538 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
540 context=3):
539 context=3):
541 """
540 """
542 Returns (git like) *diff*, as plain text. Shows changes introduced by
541 Returns (git like) *diff*, as plain text. Shows changes introduced by
543 ``rev2`` since ``rev1``.
542 ``rev2`` since ``rev1``.
544
543
545 :param rev1: Entry point from which diff is shown. Can be
544 :param rev1: Entry point from which diff is shown. Can be
546 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
545 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
547 the changes since empty state of the repository until ``rev2``
546 the changes since empty state of the repository until ``rev2``
548 :param rev2: Until which revision changes should be shown.
547 :param rev2: Until which revision changes should be shown.
549 :param ignore_whitespace: If set to ``True``, would not show whitespace
548 :param ignore_whitespace: If set to ``True``, would not show whitespace
550 changes. Defaults to ``False``.
549 changes. Defaults to ``False``.
551 :param context: How many lines before/after changed lines should be
550 :param context: How many lines before/after changed lines should be
552 shown. Defaults to ``3``.
551 shown. Defaults to ``3``.
553 """
552 """
554 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
553 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
555 if ignore_whitespace:
554 if ignore_whitespace:
556 flags.append('-w')
555 flags.append('-w')
557
556
558 if hasattr(rev1, 'raw_id'):
557 if hasattr(rev1, 'raw_id'):
559 rev1 = getattr(rev1, 'raw_id')
558 rev1 = getattr(rev1, 'raw_id')
560
559
561 if hasattr(rev2, 'raw_id'):
560 if hasattr(rev2, 'raw_id'):
562 rev2 = getattr(rev2, 'raw_id')
561 rev2 = getattr(rev2, 'raw_id')
563
562
564 if rev1 == self.EMPTY_CHANGESET:
563 if rev1 == self.EMPTY_CHANGESET:
565 rev2 = self.get_changeset(rev2).raw_id
564 rev2 = self.get_changeset(rev2).raw_id
566 cmd = ' '.join(['show'] + flags + [rev2])
565 cmd = ' '.join(['show'] + flags + [rev2])
567 else:
566 else:
568 rev1 = self.get_changeset(rev1).raw_id
567 rev1 = self.get_changeset(rev1).raw_id
569 rev2 = self.get_changeset(rev2).raw_id
568 rev2 = self.get_changeset(rev2).raw_id
570 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
569 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
571
570
572 if path:
571 if path:
573 cmd += ' -- "%s"' % path
572 cmd += ' -- "%s"' % path
574
573
575 stdout, stderr = self.run_git_command(cmd)
574 stdout, stderr = self.run_git_command(cmd)
576 # If we used 'show' command, strip first few lines (until actual diff
575 # If we used 'show' command, strip first few lines (until actual diff
577 # starts)
576 # starts)
578 if rev1 == self.EMPTY_CHANGESET:
577 if rev1 == self.EMPTY_CHANGESET:
579 lines = stdout.splitlines()
578 lines = stdout.splitlines()
580 x = 0
579 x = 0
581 for line in lines:
580 for line in lines:
582 if line.startswith('diff'):
581 if line.startswith('diff'):
583 break
582 break
584 x += 1
583 x += 1
585 # Append new line just like 'diff' command do
584 # Append new line just like 'diff' command do
586 stdout = '\n'.join(lines[x:]) + '\n'
585 stdout = '\n'.join(lines[x:]) + '\n'
587 return stdout
586 return stdout
588
587
589 @LazyProperty
588 @LazyProperty
590 def in_memory_changeset(self):
589 def in_memory_changeset(self):
591 """
590 """
592 Returns ``GitInMemoryChangeset`` object for this repository.
591 Returns ``GitInMemoryChangeset`` object for this repository.
593 """
592 """
594 return GitInMemoryChangeset(self)
593 return GitInMemoryChangeset(self)
595
594
596 def clone(self, url, update_after_clone=True, bare=False):
595 def clone(self, url, update_after_clone=True, bare=False):
597 """
596 """
598 Tries to clone changes from external location.
597 Tries to clone changes from external location.
599
598
600 :param update_after_clone: If set to ``False``, git won't checkout
599 :param update_after_clone: If set to ``False``, git won't checkout
601 working directory
600 working directory
602 :param bare: If set to ``True``, repository would be cloned into
601 :param bare: If set to ``True``, repository would be cloned into
603 *bare* git repository (no working directory at all).
602 *bare* git repository (no working directory at all).
604 """
603 """
605 url = self._get_url(url)
604 url = self._get_url(url)
606 cmd = ['clone']
605 cmd = ['clone']
607 if bare:
606 if bare:
608 cmd.append('--bare')
607 cmd.append('--bare')
609 elif not update_after_clone:
608 elif not update_after_clone:
610 cmd.append('--no-checkout')
609 cmd.append('--no-checkout')
611 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
610 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
612 cmd = ' '.join(cmd)
611 cmd = ' '.join(cmd)
613 # If error occurs run_git_command raises RepositoryError already
612 # If error occurs run_git_command raises RepositoryError already
614 self.run_git_command(cmd)
613 self.run_git_command(cmd)
615
614
616 def pull(self, url):
615 def pull(self, url):
617 """
616 """
618 Tries to pull changes from external location.
617 Tries to pull changes from external location.
619 """
618 """
620 url = self._get_url(url)
619 url = self._get_url(url)
621 cmd = ['pull']
620 cmd = ['pull']
622 cmd.append("--ff-only")
621 cmd.append("--ff-only")
623 cmd.append(url)
622 cmd.append(url)
624 cmd = ' '.join(cmd)
623 cmd = ' '.join(cmd)
625 # If error occurs run_git_command raises RepositoryError already
624 # If error occurs run_git_command raises RepositoryError already
626 self.run_git_command(cmd)
625 self.run_git_command(cmd)
627
626
628 def fetch(self, url):
627 def fetch(self, url):
629 """
628 """
630 Tries to pull changes from external location.
629 Tries to pull changes from external location.
631 """
630 """
632 url = self._get_url(url)
631 url = self._get_url(url)
633 so, se = self.run_git_command('ls-remote -h %s' % url)
632 so, se = self.run_git_command('ls-remote -h %s' % url)
634 refs = []
633 refs = []
635 for line in (x for x in so.splitlines()):
634 for line in (x for x in so.splitlines()):
636 sha, ref = line.split('\t')
635 sha, ref = line.split('\t')
637 refs.append(ref)
636 refs.append(ref)
638 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
637 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
639 cmd = '''fetch %s -- %s''' % (url, refs)
638 cmd = '''fetch %s -- %s''' % (url, refs)
640 self.run_git_command(cmd)
639 self.run_git_command(cmd)
641
640
642 @LazyProperty
641 @LazyProperty
643 def workdir(self):
642 def workdir(self):
644 """
643 """
645 Returns ``Workdir`` instance for this repository.
644 Returns ``Workdir`` instance for this repository.
646 """
645 """
647 return GitWorkdir(self)
646 return GitWorkdir(self)
648
647
649 def get_config_value(self, section, name, config_file=None):
648 def get_config_value(self, section, name, config_file=None):
650 """
649 """
651 Returns configuration value for a given [``section``] and ``name``.
650 Returns configuration value for a given [``section``] and ``name``.
652
651
653 :param section: Section we want to retrieve value from
652 :param section: Section we want to retrieve value from
654 :param name: Name of configuration we want to retrieve
653 :param name: Name of configuration we want to retrieve
655 :param config_file: A path to file which should be used to retrieve
654 :param config_file: A path to file which should be used to retrieve
656 configuration from (might also be a list of file paths)
655 configuration from (might also be a list of file paths)
657 """
656 """
658 if config_file is None:
657 if config_file is None:
659 config_file = []
658 config_file = []
660 elif isinstance(config_file, basestring):
659 elif isinstance(config_file, basestring):
661 config_file = [config_file]
660 config_file = [config_file]
662
661
663 def gen_configs():
662 def gen_configs():
664 for path in config_file + self._config_files:
663 for path in config_file + self._config_files:
665 try:
664 try:
666 yield ConfigFile.from_path(path)
665 yield ConfigFile.from_path(path)
667 except (IOError, OSError, ValueError):
666 except (IOError, OSError, ValueError):
668 continue
667 continue
669
668
670 for config in gen_configs():
669 for config in gen_configs():
671 try:
670 try:
672 return config.get(section, name)
671 return config.get(section, name)
673 except KeyError:
672 except KeyError:
674 continue
673 continue
675 return None
674 return None
676
675
677 def get_user_name(self, config_file=None):
676 def get_user_name(self, config_file=None):
678 """
677 """
679 Returns user's name from global configuration file.
678 Returns user's name from global configuration file.
680
679
681 :param config_file: A path to file which should be used to retrieve
680 :param config_file: A path to file which should be used to retrieve
682 configuration from (might also be a list of file paths)
681 configuration from (might also be a list of file paths)
683 """
682 """
684 return self.get_config_value('user', 'name', config_file)
683 return self.get_config_value('user', 'name', config_file)
685
684
686 def get_user_email(self, config_file=None):
685 def get_user_email(self, config_file=None):
687 """
686 """
688 Returns user's email from global configuration file.
687 Returns user's email from global configuration file.
689
688
690 :param config_file: A path to file which should be used to retrieve
689 :param config_file: A path to file which should be used to retrieve
691 configuration from (might also be a list of file paths)
690 configuration from (might also be a list of file paths)
692 """
691 """
693 return self.get_config_value('user', 'email', config_file)
692 return self.get_config_value('user', 'email', config_file)
@@ -1,555 +1,553 b''
1 import os
1 import os
2 import time
2 import time
3 import datetime
3 import datetime
4 import urllib
4 import urllib
5 import urllib2
5 import urllib2
6
6
7 from rhodecode.lib.vcs.backends.base import BaseRepository
7 from rhodecode.lib.vcs.backends.base import BaseRepository, CollectionGenerator
8 from .workdir import MercurialWorkdir
8 from .workdir import MercurialWorkdir
9 from .changeset import MercurialChangeset
9 from .changeset import MercurialChangeset
10 from .inmemory import MercurialInMemoryChangeset
10 from .inmemory import MercurialInMemoryChangeset
11
11
12 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \
12 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \
13 ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \
13 ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \
14 VCSError, TagAlreadyExistError, TagDoesNotExistError
14 VCSError, TagAlreadyExistError, TagDoesNotExistError
15 from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \
15 from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \
16 makedate, safe_unicode
16 makedate, safe_unicode
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
18 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
19 from rhodecode.lib.vcs.utils.paths import abspath
19 from rhodecode.lib.vcs.utils.paths import abspath
20
20
21 from rhodecode.lib.vcs.utils.hgcompat import ui, nullid, match, patch, \
21 from rhodecode.lib.vcs.utils.hgcompat import ui, nullid, match, patch, \
22 diffopts, clone, get_contact, pull, localrepository, RepoLookupError, \
22 diffopts, clone, get_contact, pull, localrepository, RepoLookupError, \
23 Abort, RepoError, hex, scmutil
23 Abort, RepoError, hex, scmutil
24
24
25
25
26 class MercurialRepository(BaseRepository):
26 class MercurialRepository(BaseRepository):
27 """
27 """
28 Mercurial repository backend
28 Mercurial repository backend
29 """
29 """
30 DEFAULT_BRANCH_NAME = 'default'
30 DEFAULT_BRANCH_NAME = 'default'
31 scm = 'hg'
31 scm = 'hg'
32
32
33 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
33 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
34 update_after_clone=False):
34 update_after_clone=False):
35 """
35 """
36 Raises RepositoryError if repository could not be find at the given
36 Raises RepositoryError if repository could not be find at the given
37 ``repo_path``.
37 ``repo_path``.
38
38
39 :param repo_path: local path of the repository
39 :param repo_path: local path of the repository
40 :param create=False: if set to True, would try to create repository if
40 :param create=False: if set to True, would try to create repository if
41 it does not exist rather than raising exception
41 it does not exist rather than raising exception
42 :param baseui=None: user data
42 :param baseui=None: user data
43 :param src_url=None: would try to clone repository from given location
43 :param src_url=None: would try to clone repository from given location
44 :param update_after_clone=False: sets update of working copy after
44 :param update_after_clone=False: sets update of working copy after
45 making a clone
45 making a clone
46 """
46 """
47
47
48 if not isinstance(repo_path, str):
48 if not isinstance(repo_path, str):
49 raise VCSError('Mercurial backend requires repository path to '
49 raise VCSError('Mercurial backend requires repository path to '
50 'be instance of <str> got %s instead' %
50 'be instance of <str> got %s instead' %
51 type(repo_path))
51 type(repo_path))
52
52
53 self.path = abspath(repo_path)
53 self.path = abspath(repo_path)
54 self.baseui = baseui or ui.ui()
54 self.baseui = baseui or ui.ui()
55 # We've set path and ui, now we can set _repo itself
55 # We've set path and ui, now we can set _repo itself
56 self._repo = self._get_repo(create, src_url, update_after_clone)
56 self._repo = self._get_repo(create, src_url, update_after_clone)
57
57
58 @property
58 @property
59 def _empty(self):
59 def _empty(self):
60 """
60 """
61 Checks if repository is empty without any changesets
61 Checks if repository is empty without any changesets
62 """
62 """
63 # TODO: Following raises errors when using InMemoryChangeset...
63 # TODO: Following raises errors when using InMemoryChangeset...
64 # return len(self._repo.changelog) == 0
64 # return len(self._repo.changelog) == 0
65 return len(self.revisions) == 0
65 return len(self.revisions) == 0
66
66
67 @LazyProperty
67 @LazyProperty
68 def revisions(self):
68 def revisions(self):
69 """
69 """
70 Returns list of revisions' ids, in ascending order. Being lazy
70 Returns list of revisions' ids, in ascending order. Being lazy
71 attribute allows external tools to inject shas from cache.
71 attribute allows external tools to inject shas from cache.
72 """
72 """
73 return self._get_all_revisions()
73 return self._get_all_revisions()
74
74
75 @LazyProperty
75 @LazyProperty
76 def name(self):
76 def name(self):
77 return os.path.basename(self.path)
77 return os.path.basename(self.path)
78
78
79 @LazyProperty
79 @LazyProperty
80 def branches(self):
80 def branches(self):
81 return self._get_branches()
81 return self._get_branches()
82
82
83 @LazyProperty
83 @LazyProperty
84 def allbranches(self):
84 def allbranches(self):
85 """
85 """
86 List all branches, including closed branches.
86 List all branches, including closed branches.
87 """
87 """
88 return self._get_branches(closed=True)
88 return self._get_branches(closed=True)
89
89
90 def _get_branches(self, closed=False):
90 def _get_branches(self, closed=False):
91 """
91 """
92 Get's branches for this repository
92 Get's branches for this repository
93 Returns only not closed branches by default
93 Returns only not closed branches by default
94
94
95 :param closed: return also closed branches for mercurial
95 :param closed: return also closed branches for mercurial
96 """
96 """
97
97
98 if self._empty:
98 if self._empty:
99 return {}
99 return {}
100
100
101 def _branchtags(localrepo):
101 def _branchtags(localrepo):
102 """
102 """
103 Patched version of mercurial branchtags to not return the closed
103 Patched version of mercurial branchtags to not return the closed
104 branches
104 branches
105
105
106 :param localrepo: locarepository instance
106 :param localrepo: locarepository instance
107 """
107 """
108
108
109 bt = {}
109 bt = {}
110 bt_closed = {}
110 bt_closed = {}
111 for bn, heads in localrepo.branchmap().iteritems():
111 for bn, heads in localrepo.branchmap().iteritems():
112 tip = heads[-1]
112 tip = heads[-1]
113 if 'close' in localrepo.changelog.read(tip)[5]:
113 if 'close' in localrepo.changelog.read(tip)[5]:
114 bt_closed[bn] = tip
114 bt_closed[bn] = tip
115 else:
115 else:
116 bt[bn] = tip
116 bt[bn] = tip
117
117
118 if closed:
118 if closed:
119 bt.update(bt_closed)
119 bt.update(bt_closed)
120 return bt
120 return bt
121
121
122 sortkey = lambda ctx: ctx[0] # sort by name
122 sortkey = lambda ctx: ctx[0] # sort by name
123 _branches = [(safe_unicode(n), hex(h),) for n, h in
123 _branches = [(safe_unicode(n), hex(h),) for n, h in
124 _branchtags(self._repo).items()]
124 _branchtags(self._repo).items()]
125
125
126 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
126 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
127
127
128 @LazyProperty
128 @LazyProperty
129 def tags(self):
129 def tags(self):
130 """
130 """
131 Get's tags for this repository
131 Get's tags for this repository
132 """
132 """
133 return self._get_tags()
133 return self._get_tags()
134
134
135 def _get_tags(self):
135 def _get_tags(self):
136 if self._empty:
136 if self._empty:
137 return {}
137 return {}
138
138
139 sortkey = lambda ctx: ctx[0] # sort by name
139 sortkey = lambda ctx: ctx[0] # sort by name
140 _tags = [(safe_unicode(n), hex(h),) for n, h in
140 _tags = [(safe_unicode(n), hex(h),) for n, h in
141 self._repo.tags().items()]
141 self._repo.tags().items()]
142
142
143 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
143 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
144
144
145 def tag(self, name, user, revision=None, message=None, date=None,
145 def tag(self, name, user, revision=None, message=None, date=None,
146 **kwargs):
146 **kwargs):
147 """
147 """
148 Creates and returns a tag for the given ``revision``.
148 Creates and returns a tag for the given ``revision``.
149
149
150 :param name: name for new tag
150 :param name: name for new tag
151 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
151 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
152 :param revision: changeset id for which new tag would be created
152 :param revision: changeset id for which new tag would be created
153 :param message: message of the tag's commit
153 :param message: message of the tag's commit
154 :param date: date of tag's commit
154 :param date: date of tag's commit
155
155
156 :raises TagAlreadyExistError: if tag with same name already exists
156 :raises TagAlreadyExistError: if tag with same name already exists
157 """
157 """
158 if name in self.tags:
158 if name in self.tags:
159 raise TagAlreadyExistError("Tag %s already exists" % name)
159 raise TagAlreadyExistError("Tag %s already exists" % name)
160 changeset = self.get_changeset(revision)
160 changeset = self.get_changeset(revision)
161 local = kwargs.setdefault('local', False)
161 local = kwargs.setdefault('local', False)
162
162
163 if message is None:
163 if message is None:
164 message = "Added tag %s for changeset %s" % (name,
164 message = "Added tag %s for changeset %s" % (name,
165 changeset.short_id)
165 changeset.short_id)
166
166
167 if date is None:
167 if date is None:
168 date = datetime.datetime.now().ctime()
168 date = datetime.datetime.now().ctime()
169
169
170 try:
170 try:
171 self._repo.tag(name, changeset._ctx.node(), message, local, user,
171 self._repo.tag(name, changeset._ctx.node(), message, local, user,
172 date)
172 date)
173 except Abort, e:
173 except Abort, e:
174 raise RepositoryError(e.message)
174 raise RepositoryError(e.message)
175
175
176 # Reinitialize tags
176 # Reinitialize tags
177 self.tags = self._get_tags()
177 self.tags = self._get_tags()
178 tag_id = self.tags[name]
178 tag_id = self.tags[name]
179
179
180 return self.get_changeset(revision=tag_id)
180 return self.get_changeset(revision=tag_id)
181
181
182 def remove_tag(self, name, user, message=None, date=None):
182 def remove_tag(self, name, user, message=None, date=None):
183 """
183 """
184 Removes tag with the given ``name``.
184 Removes tag with the given ``name``.
185
185
186 :param name: name of the tag to be removed
186 :param name: name of the tag to be removed
187 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
188 :param message: message of the tag's removal commit
188 :param message: message of the tag's removal commit
189 :param date: date of tag's removal commit
189 :param date: date of tag's removal commit
190
190
191 :raises TagDoesNotExistError: if tag with given name does not exists
191 :raises TagDoesNotExistError: if tag with given name does not exists
192 """
192 """
193 if name not in self.tags:
193 if name not in self.tags:
194 raise TagDoesNotExistError("Tag %s does not exist" % name)
194 raise TagDoesNotExistError("Tag %s does not exist" % name)
195 if message is None:
195 if message is None:
196 message = "Removed tag %s" % name
196 message = "Removed tag %s" % name
197 if date is None:
197 if date is None:
198 date = datetime.datetime.now().ctime()
198 date = datetime.datetime.now().ctime()
199 local = False
199 local = False
200
200
201 try:
201 try:
202 self._repo.tag(name, nullid, message, local, user, date)
202 self._repo.tag(name, nullid, message, local, user, date)
203 self.tags = self._get_tags()
203 self.tags = self._get_tags()
204 except Abort, e:
204 except Abort, e:
205 raise RepositoryError(e.message)
205 raise RepositoryError(e.message)
206
206
207 @LazyProperty
207 @LazyProperty
208 def bookmarks(self):
208 def bookmarks(self):
209 """
209 """
210 Get's bookmarks for this repository
210 Get's bookmarks for this repository
211 """
211 """
212 return self._get_bookmarks()
212 return self._get_bookmarks()
213
213
214 def _get_bookmarks(self):
214 def _get_bookmarks(self):
215 if self._empty:
215 if self._empty:
216 return {}
216 return {}
217
217
218 sortkey = lambda ctx: ctx[0] # sort by name
218 sortkey = lambda ctx: ctx[0] # sort by name
219 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
219 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
220 self._repo._bookmarks.items()]
220 self._repo._bookmarks.items()]
221 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
221 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
222
222
223 def _get_all_revisions(self):
223 def _get_all_revisions(self):
224
224
225 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
225 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
226
226
227 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
227 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
228 context=3):
228 context=3):
229 """
229 """
230 Returns (git like) *diff*, as plain text. Shows changes introduced by
230 Returns (git like) *diff*, as plain text. Shows changes introduced by
231 ``rev2`` since ``rev1``.
231 ``rev2`` since ``rev1``.
232
232
233 :param rev1: Entry point from which diff is shown. Can be
233 :param rev1: Entry point from which diff is shown. Can be
234 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
234 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
235 the changes since empty state of the repository until ``rev2``
235 the changes since empty state of the repository until ``rev2``
236 :param rev2: Until which revision changes should be shown.
236 :param rev2: Until which revision changes should be shown.
237 :param ignore_whitespace: If set to ``True``, would not show whitespace
237 :param ignore_whitespace: If set to ``True``, would not show whitespace
238 changes. Defaults to ``False``.
238 changes. Defaults to ``False``.
239 :param context: How many lines before/after changed lines should be
239 :param context: How many lines before/after changed lines should be
240 shown. Defaults to ``3``.
240 shown. Defaults to ``3``.
241 """
241 """
242 if hasattr(rev1, 'raw_id'):
242 if hasattr(rev1, 'raw_id'):
243 rev1 = getattr(rev1, 'raw_id')
243 rev1 = getattr(rev1, 'raw_id')
244
244
245 if hasattr(rev2, 'raw_id'):
245 if hasattr(rev2, 'raw_id'):
246 rev2 = getattr(rev2, 'raw_id')
246 rev2 = getattr(rev2, 'raw_id')
247
247
248 # Check if given revisions are present at repository (may raise
248 # Check if given revisions are present at repository (may raise
249 # ChangesetDoesNotExistError)
249 # ChangesetDoesNotExistError)
250 if rev1 != self.EMPTY_CHANGESET:
250 if rev1 != self.EMPTY_CHANGESET:
251 self.get_changeset(rev1)
251 self.get_changeset(rev1)
252 self.get_changeset(rev2)
252 self.get_changeset(rev2)
253 if path:
253 if path:
254 file_filter = match(self.path, '', [path])
254 file_filter = match(self.path, '', [path])
255 else:
255 else:
256 file_filter = None
256 file_filter = None
257
257
258 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
258 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
259 opts=diffopts(git=True,
259 opts=diffopts(git=True,
260 ignorews=ignore_whitespace,
260 ignorews=ignore_whitespace,
261 context=context)))
261 context=context)))
262
262
263 @classmethod
263 @classmethod
264 def _check_url(cls, url):
264 def _check_url(cls, url):
265 """
265 """
266 Function will check given url and try to verify if it's a valid
266 Function will check given url and try to verify if it's a valid
267 link. Sometimes it may happened that mercurial will issue basic
267 link. Sometimes it may happened that mercurial will issue basic
268 auth request that can cause whole API to hang when used from python
268 auth request that can cause whole API to hang when used from python
269 or other external calls.
269 or other external calls.
270
270
271 On failures it'll raise urllib2.HTTPError, return code 200 if url
271 On failures it'll raise urllib2.HTTPError, return code 200 if url
272 is valid or True if it's a local path
272 is valid or True if it's a local path
273 """
273 """
274
274
275 from mercurial.util import url as Url
275 from mercurial.util import url as Url
276
276
277 # those authnadlers are patched for python 2.6.5 bug an
277 # those authnadlers are patched for python 2.6.5 bug an
278 # infinit looping when given invalid resources
278 # infinit looping when given invalid resources
279 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
279 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
280
280
281 # check first if it's not an local url
281 # check first if it's not an local url
282 if os.path.isdir(url) or url.startswith('file:'):
282 if os.path.isdir(url) or url.startswith('file:'):
283 return True
283 return True
284
284
285 if('+' in url[:url.find('://')]):
285 if('+' in url[:url.find('://')]):
286 url = url[url.find('+') + 1:]
286 url = url[url.find('+') + 1:]
287
287
288 handlers = []
288 handlers = []
289 test_uri, authinfo = Url(url).authinfo()
289 test_uri, authinfo = Url(url).authinfo()
290
290
291 if authinfo:
291 if authinfo:
292 #create a password manager
292 #create a password manager
293 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
293 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
294 passmgr.add_password(*authinfo)
294 passmgr.add_password(*authinfo)
295
295
296 handlers.extend((httpbasicauthhandler(passmgr),
296 handlers.extend((httpbasicauthhandler(passmgr),
297 httpdigestauthhandler(passmgr)))
297 httpdigestauthhandler(passmgr)))
298
298
299 o = urllib2.build_opener(*handlers)
299 o = urllib2.build_opener(*handlers)
300 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
300 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
301 ('Accept', 'application/mercurial-0.1')]
301 ('Accept', 'application/mercurial-0.1')]
302
302
303 q = {"cmd": 'between'}
303 q = {"cmd": 'between'}
304 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
304 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
305 qs = '?%s' % urllib.urlencode(q)
305 qs = '?%s' % urllib.urlencode(q)
306 cu = "%s%s" % (test_uri, qs)
306 cu = "%s%s" % (test_uri, qs)
307 req = urllib2.Request(cu, None, {})
307 req = urllib2.Request(cu, None, {})
308
308
309 try:
309 try:
310 resp = o.open(req)
310 resp = o.open(req)
311 return resp.code == 200
311 return resp.code == 200
312 except Exception, e:
312 except Exception, e:
313 # means it cannot be cloned
313 # means it cannot be cloned
314 raise urllib2.URLError("[%s] %s" % (url, e))
314 raise urllib2.URLError("[%s] %s" % (url, e))
315
315
316 def _get_repo(self, create, src_url=None, update_after_clone=False):
316 def _get_repo(self, create, src_url=None, update_after_clone=False):
317 """
317 """
318 Function will check for mercurial repository in given path and return
318 Function will check for mercurial repository in given path and return
319 a localrepo object. If there is no repository in that path it will
319 a localrepo object. If there is no repository in that path it will
320 raise an exception unless ``create`` parameter is set to True - in
320 raise an exception unless ``create`` parameter is set to True - in
321 that case repository would be created and returned.
321 that case repository would be created and returned.
322 If ``src_url`` is given, would try to clone repository from the
322 If ``src_url`` is given, would try to clone repository from the
323 location at given clone_point. Additionally it'll make update to
323 location at given clone_point. Additionally it'll make update to
324 working copy accordingly to ``update_after_clone`` flag
324 working copy accordingly to ``update_after_clone`` flag
325 """
325 """
326
326
327 try:
327 try:
328 if src_url:
328 if src_url:
329 url = str(self._get_url(src_url))
329 url = str(self._get_url(src_url))
330 opts = {}
330 opts = {}
331 if not update_after_clone:
331 if not update_after_clone:
332 opts.update({'noupdate': True})
332 opts.update({'noupdate': True})
333 try:
333 try:
334 MercurialRepository._check_url(url)
334 MercurialRepository._check_url(url)
335 clone(self.baseui, url, self.path, **opts)
335 clone(self.baseui, url, self.path, **opts)
336 # except urllib2.URLError:
336 # except urllib2.URLError:
337 # raise Abort("Got HTTP 404 error")
337 # raise Abort("Got HTTP 404 error")
338 except Exception:
338 except Exception:
339 raise
339 raise
340
340
341 # Don't try to create if we've already cloned repo
341 # Don't try to create if we've already cloned repo
342 create = False
342 create = False
343 return localrepository(self.baseui, self.path, create=create)
343 return localrepository(self.baseui, self.path, create=create)
344 except (Abort, RepoError), err:
344 except (Abort, RepoError), err:
345 if create:
345 if create:
346 msg = "Cannot create repository at %s. Original error was %s"\
346 msg = "Cannot create repository at %s. Original error was %s"\
347 % (self.path, err)
347 % (self.path, err)
348 else:
348 else:
349 msg = "Not valid repository at %s. Original error was %s"\
349 msg = "Not valid repository at %s. Original error was %s"\
350 % (self.path, err)
350 % (self.path, err)
351 raise RepositoryError(msg)
351 raise RepositoryError(msg)
352
352
353 @LazyProperty
353 @LazyProperty
354 def in_memory_changeset(self):
354 def in_memory_changeset(self):
355 return MercurialInMemoryChangeset(self)
355 return MercurialInMemoryChangeset(self)
356
356
357 @LazyProperty
357 @LazyProperty
358 def description(self):
358 def description(self):
359 undefined_description = u'unknown'
359 undefined_description = u'unknown'
360 return safe_unicode(self._repo.ui.config('web', 'description',
360 return safe_unicode(self._repo.ui.config('web', 'description',
361 undefined_description, untrusted=True))
361 undefined_description, untrusted=True))
362
362
363 @LazyProperty
363 @LazyProperty
364 def contact(self):
364 def contact(self):
365 undefined_contact = u'Unknown'
365 undefined_contact = u'Unknown'
366 return safe_unicode(get_contact(self._repo.ui.config)
366 return safe_unicode(get_contact(self._repo.ui.config)
367 or undefined_contact)
367 or undefined_contact)
368
368
369 @LazyProperty
369 @LazyProperty
370 def last_change(self):
370 def last_change(self):
371 """
371 """
372 Returns last change made on this repository as datetime object
372 Returns last change made on this repository as datetime object
373 """
373 """
374 return date_fromtimestamp(self._get_mtime(), makedate()[1])
374 return date_fromtimestamp(self._get_mtime(), makedate()[1])
375
375
376 def _get_mtime(self):
376 def _get_mtime(self):
377 try:
377 try:
378 return time.mktime(self.get_changeset().date.timetuple())
378 return time.mktime(self.get_changeset().date.timetuple())
379 except RepositoryError:
379 except RepositoryError:
380 #fallback to filesystem
380 #fallback to filesystem
381 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
381 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
382 st_path = os.path.join(self.path, '.hg', "store")
382 st_path = os.path.join(self.path, '.hg', "store")
383 if os.path.exists(cl_path):
383 if os.path.exists(cl_path):
384 return os.stat(cl_path).st_mtime
384 return os.stat(cl_path).st_mtime
385 else:
385 else:
386 return os.stat(st_path).st_mtime
386 return os.stat(st_path).st_mtime
387
387
388 def _get_hidden(self):
388 def _get_hidden(self):
389 return self._repo.ui.configbool("web", "hidden", untrusted=True)
389 return self._repo.ui.configbool("web", "hidden", untrusted=True)
390
390
391 def _get_revision(self, revision):
391 def _get_revision(self, revision):
392 """
392 """
393 Get's an ID revision given as str. This will always return a fill
393 Get's an ID revision given as str. This will always return a fill
394 40 char revision number
394 40 char revision number
395
395
396 :param revision: str or int or None
396 :param revision: str or int or None
397 """
397 """
398
398
399 if self._empty:
399 if self._empty:
400 raise EmptyRepositoryError("There are no changesets yet")
400 raise EmptyRepositoryError("There are no changesets yet")
401
401
402 if revision in [-1, 'tip', None]:
402 if revision in [-1, 'tip', None]:
403 revision = 'tip'
403 revision = 'tip'
404
404
405 try:
405 try:
406 revision = hex(self._repo.lookup(revision))
406 revision = hex(self._repo.lookup(revision))
407 except (IndexError, ValueError, RepoLookupError, TypeError):
407 except (IndexError, ValueError, RepoLookupError, TypeError):
408 raise ChangesetDoesNotExistError("Revision %s does not "
408 raise ChangesetDoesNotExistError("Revision %s does not "
409 "exist for this repository"
409 "exist for this repository"
410 % (revision))
410 % (revision))
411 return revision
411 return revision
412
412
413 def _get_archives(self, archive_name='tip'):
413 def _get_archives(self, archive_name='tip'):
414 allowed = self.baseui.configlist("web", "allow_archive",
414 allowed = self.baseui.configlist("web", "allow_archive",
415 untrusted=True)
415 untrusted=True)
416 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
416 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
417 if i[0] in allowed or self._repo.ui.configbool("web",
417 if i[0] in allowed or self._repo.ui.configbool("web",
418 "allow" + i[0],
418 "allow" + i[0],
419 untrusted=True):
419 untrusted=True):
420 yield {"type": i[0], "extension": i[1], "node": archive_name}
420 yield {"type": i[0], "extension": i[1], "node": archive_name}
421
421
422 def _get_url(self, url):
422 def _get_url(self, url):
423 """
423 """
424 Returns normalized url. If schema is not given, would fall
424 Returns normalized url. If schema is not given, would fall
425 to filesystem
425 to filesystem
426 (``file:///``) schema.
426 (``file:///``) schema.
427 """
427 """
428 url = str(url)
428 url = str(url)
429 if url != 'default' and not '://' in url:
429 if url != 'default' and not '://' in url:
430 url = "file:" + urllib.pathname2url(url)
430 url = "file:" + urllib.pathname2url(url)
431 return url
431 return url
432
432
433 def get_hook_location(self):
433 def get_hook_location(self):
434 """
434 """
435 returns absolute path to location where hooks are stored
435 returns absolute path to location where hooks are stored
436 """
436 """
437 return os.path.join(self.path, '.hg', '.hgrc')
437 return os.path.join(self.path, '.hg', '.hgrc')
438
438
439 def get_changeset(self, revision=None):
439 def get_changeset(self, revision=None):
440 """
440 """
441 Returns ``MercurialChangeset`` object representing repository's
441 Returns ``MercurialChangeset`` object representing repository's
442 changeset at the given ``revision``.
442 changeset at the given ``revision``.
443 """
443 """
444 revision = self._get_revision(revision)
444 revision = self._get_revision(revision)
445 changeset = MercurialChangeset(repository=self, revision=revision)
445 changeset = MercurialChangeset(repository=self, revision=revision)
446 return changeset
446 return changeset
447
447
448 def get_changesets(self, start=None, end=None, start_date=None,
448 def get_changesets(self, start=None, end=None, start_date=None,
449 end_date=None, branch_name=None, reverse=False):
449 end_date=None, branch_name=None, reverse=False):
450 """
450 """
451 Returns iterator of ``MercurialChangeset`` objects from start to end
451 Returns iterator of ``MercurialChangeset`` objects from start to end
452 (both are inclusive)
452 (both are inclusive)
453
453
454 :param start: None, str, int or mercurial lookup format
454 :param start: None, str, int or mercurial lookup format
455 :param end: None, str, int or mercurial lookup format
455 :param end: None, str, int or mercurial lookup format
456 :param start_date:
456 :param start_date:
457 :param end_date:
457 :param end_date:
458 :param branch_name:
458 :param branch_name:
459 :param reversed: return changesets in reversed order
459 :param reversed: return changesets in reversed order
460 """
460 """
461
461
462 start_raw_id = self._get_revision(start)
462 start_raw_id = self._get_revision(start)
463 start_pos = self.revisions.index(start_raw_id) if start else None
463 start_pos = self.revisions.index(start_raw_id) if start else None
464 end_raw_id = self._get_revision(end)
464 end_raw_id = self._get_revision(end)
465 end_pos = self.revisions.index(end_raw_id) if end else None
465 end_pos = self.revisions.index(end_raw_id) if end else None
466
466
467 if None not in [start, end] and start_pos > end_pos:
467 if None not in [start, end] and start_pos > end_pos:
468 raise RepositoryError("Start revision '%s' cannot be "
468 raise RepositoryError("Start revision '%s' cannot be "
469 "after end revision '%s'" % (start, end))
469 "after end revision '%s'" % (start, end))
470
470
471 if branch_name and branch_name not in self.allbranches.keys():
471 if branch_name and branch_name not in self.allbranches.keys():
472 raise BranchDoesNotExistError('Branch %s not found in'
472 raise BranchDoesNotExistError('Branch %s not found in'
473 ' this repository' % branch_name)
473 ' this repository' % branch_name)
474 if end_pos is not None:
474 if end_pos is not None:
475 end_pos += 1
475 end_pos += 1
476 #filter branches
476 #filter branches
477
477 filter_ = []
478 if branch_name:
478 if branch_name:
479 revisions = scmutil.revrange(self._repo,
479 filter_.append('branch("%s")' % (branch_name))
480 ['branch("%s")' % (branch_name)])
480
481 if start_date:
482 filter_.append('date(">%s")' % start_date)
483 if end_date:
484 filter_.append('date("<%s")' % end_date)
485 if filter_:
486 revisions = scmutil.revrange(self._repo, filter_)
481 else:
487 else:
482 revisions = self.revisions
488 revisions = self.revisions
483
489 revs = reversed(revisions[start_pos:end_pos]) if reverse else \
484 slice_ = reversed(revisions[start_pos:end_pos]) if reverse else \
485 revisions[start_pos:end_pos]
490 revisions[start_pos:end_pos]
486
491
487 for id_ in slice_:
492 return CollectionGenerator(self, revs)
488 cs = self.get_changeset(id_)
489 if start_date and cs.date < start_date:
490 continue
491 if end_date and cs.date > end_date:
492 continue
493
494 yield cs
495
493
496 def pull(self, url):
494 def pull(self, url):
497 """
495 """
498 Tries to pull changes from external location.
496 Tries to pull changes from external location.
499 """
497 """
500 url = self._get_url(url)
498 url = self._get_url(url)
501 try:
499 try:
502 pull(self.baseui, self._repo, url)
500 pull(self.baseui, self._repo, url)
503 except Abort, err:
501 except Abort, err:
504 # Propagate error but with vcs's type
502 # Propagate error but with vcs's type
505 raise RepositoryError(str(err))
503 raise RepositoryError(str(err))
506
504
507 @LazyProperty
505 @LazyProperty
508 def workdir(self):
506 def workdir(self):
509 """
507 """
510 Returns ``Workdir`` instance for this repository.
508 Returns ``Workdir`` instance for this repository.
511 """
509 """
512 return MercurialWorkdir(self)
510 return MercurialWorkdir(self)
513
511
514 def get_config_value(self, section, name=None, config_file=None):
512 def get_config_value(self, section, name=None, config_file=None):
515 """
513 """
516 Returns configuration value for a given [``section``] and ``name``.
514 Returns configuration value for a given [``section``] and ``name``.
517
515
518 :param section: Section we want to retrieve value from
516 :param section: Section we want to retrieve value from
519 :param name: Name of configuration we want to retrieve
517 :param name: Name of configuration we want to retrieve
520 :param config_file: A path to file which should be used to retrieve
518 :param config_file: A path to file which should be used to retrieve
521 configuration from (might also be a list of file paths)
519 configuration from (might also be a list of file paths)
522 """
520 """
523 if config_file is None:
521 if config_file is None:
524 config_file = []
522 config_file = []
525 elif isinstance(config_file, basestring):
523 elif isinstance(config_file, basestring):
526 config_file = [config_file]
524 config_file = [config_file]
527
525
528 config = self._repo.ui
526 config = self._repo.ui
529 for path in config_file:
527 for path in config_file:
530 config.readconfig(path)
528 config.readconfig(path)
531 return config.config(section, name)
529 return config.config(section, name)
532
530
533 def get_user_name(self, config_file=None):
531 def get_user_name(self, config_file=None):
534 """
532 """
535 Returns user's name from global configuration file.
533 Returns user's name from global configuration file.
536
534
537 :param config_file: A path to file which should be used to retrieve
535 :param config_file: A path to file which should be used to retrieve
538 configuration from (might also be a list of file paths)
536 configuration from (might also be a list of file paths)
539 """
537 """
540 username = self.get_config_value('ui', 'username')
538 username = self.get_config_value('ui', 'username')
541 if username:
539 if username:
542 return author_name(username)
540 return author_name(username)
543 return None
541 return None
544
542
545 def get_user_email(self, config_file=None):
543 def get_user_email(self, config_file=None):
546 """
544 """
547 Returns user's email from global configuration file.
545 Returns user's email from global configuration file.
548
546
549 :param config_file: A path to file which should be used to retrieve
547 :param config_file: A path to file which should be used to retrieve
550 configuration from (might also be a list of file paths)
548 configuration from (might also be a list of file paths)
551 """
549 """
552 username = self.get_config_value('ui', 'username')
550 username = self.get_config_value('ui', 'username')
553 if username:
551 if username:
554 return author_email(username)
552 return author_email(username)
555 return None
553 return None
General Comments 0
You need to be logged in to leave comments. Login now