##// END OF EJS Templates
commits: hide evolve commits. Fixes #5392
marcink -
r2144:9e9e365e default
parent child Browse files
Show More
@@ -1,342 +1,349 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
24 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 import rhodecode.lib.helpers as h
30 import rhodecode.lib.helpers as h
31 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator)
32 LoginRequired, HasRepoPermissionAnyDecorator)
33
33
34 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.graphmod import _colored, _dagwalker
35 from rhodecode.lib.graphmod import _colored, _dagwalker
36 from rhodecode.lib.helpers import RepoPage
36 from rhodecode.lib.helpers import RepoPage
37 from rhodecode.lib.utils2 import safe_int, safe_str
37 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool
38 from rhodecode.lib.vcs.exceptions import (
38 from rhodecode.lib.vcs.exceptions import (
39 RepositoryError, CommitDoesNotExistError,
39 RepositoryError, CommitDoesNotExistError,
40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44 DEFAULT_CHANGELOG_SIZE = 20
44 DEFAULT_CHANGELOG_SIZE = 20
45
45
46
46
47 class RepoChangelogView(RepoAppView):
47 class RepoChangelogView(RepoAppView):
48
48
49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
50 """
50 """
51 This is a safe way to get commit. If an error occurs it redirects to
51 This is a safe way to get commit. If an error occurs it redirects to
52 tip with proper message
52 tip with proper message
53
53
54 :param commit_id: id of commit to fetch
54 :param commit_id: id of commit to fetch
55 :param redirect_after: toggle redirection
55 :param redirect_after: toggle redirection
56 """
56 """
57 _ = self.request.translate
57 _ = self.request.translate
58
58
59 try:
59 try:
60 return self.rhodecode_vcs_repo.get_commit(commit_id)
60 return self.rhodecode_vcs_repo.get_commit(commit_id)
61 except EmptyRepositoryError:
61 except EmptyRepositoryError:
62 if not redirect_after:
62 if not redirect_after:
63 return None
63 return None
64
64
65 h.flash(h.literal(
65 h.flash(h.literal(
66 _('There are no commits yet')), category='warning')
66 _('There are no commits yet')), category='warning')
67 raise HTTPFound(
67 raise HTTPFound(
68 h.route_path('repo_summary', repo_name=self.db_repo_name))
68 h.route_path('repo_summary', repo_name=self.db_repo_name))
69
69
70 except (CommitDoesNotExistError, LookupError):
70 except (CommitDoesNotExistError, LookupError):
71 msg = _('No such commit exists for this repository')
71 msg = _('No such commit exists for this repository')
72 h.flash(msg, category='error')
72 h.flash(msg, category='error')
73 raise HTTPNotFound()
73 raise HTTPNotFound()
74 except RepositoryError as e:
74 except RepositoryError as e:
75 h.flash(safe_str(h.escape(e)), category='error')
75 h.flash(safe_str(h.escape(e)), category='error')
76 raise HTTPNotFound()
76 raise HTTPNotFound()
77
77
78 def _graph(self, repo, commits, prev_data=None, next_data=None):
78 def _graph(self, repo, commits, prev_data=None, next_data=None):
79 """
79 """
80 Generates a DAG graph for repo
80 Generates a DAG graph for repo
81
81
82 :param repo: repo instance
82 :param repo: repo instance
83 :param commits: list of commits
83 :param commits: list of commits
84 """
84 """
85 if not commits:
85 if not commits:
86 return json.dumps([]), json.dumps([])
86 return json.dumps([]), json.dumps([])
87
87
88 def serialize(commit, parents=True):
88 def serialize(commit, parents=True):
89 data = dict(
89 data = dict(
90 raw_id=commit.raw_id,
90 raw_id=commit.raw_id,
91 idx=commit.idx,
91 idx=commit.idx,
92 branch=commit.branch,
92 branch=commit.branch,
93 )
93 )
94 if parents:
94 if parents:
95 data['parents'] = [
95 data['parents'] = [
96 serialize(x, parents=False) for x in commit.parents]
96 serialize(x, parents=False) for x in commit.parents]
97 return data
97 return data
98
98
99 prev_data = prev_data or []
99 prev_data = prev_data or []
100 next_data = next_data or []
100 next_data = next_data or []
101
101
102 current = [serialize(x) for x in commits]
102 current = [serialize(x) for x in commits]
103 commits = prev_data + current + next_data
103 commits = prev_data + current + next_data
104
104
105 dag = _dagwalker(repo, commits)
105 dag = _dagwalker(repo, commits)
106
106
107 data = [[commit_id, vtx, edges, branch]
107 data = [[commit_id, vtx, edges, branch]
108 for commit_id, vtx, edges, branch in _colored(dag)]
108 for commit_id, vtx, edges, branch in _colored(dag)]
109 return json.dumps(data), json.dumps(current)
109 return json.dumps(data), json.dumps(current)
110
110
111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
112 if branch_name not in self.rhodecode_vcs_repo.branches_all:
112 if branch_name not in self.rhodecode_vcs_repo.branches_all:
113 h.flash('Branch {} is not found.'.format(h.escape(branch_name)),
113 h.flash('Branch {} is not found.'.format(h.escape(branch_name)),
114 category='warning')
114 category='warning')
115 redirect_url = h.route_path(
115 redirect_url = h.route_path(
116 'repo_changelog_file', repo_name=repo_name,
116 'repo_changelog_file', repo_name=repo_name,
117 commit_id=branch_name, f_path=f_path or '')
117 commit_id=branch_name, f_path=f_path or '')
118 raise HTTPFound(redirect_url)
118 raise HTTPFound(redirect_url)
119
119
120 def _load_changelog_data(
120 def _load_changelog_data(
121 self, c, collection, page, chunk_size, branch_name=None,
121 self, c, collection, page, chunk_size, branch_name=None,
122 dynamic=False, f_path=None, commit_id=None):
122 dynamic=False, f_path=None, commit_id=None):
123
123
124 def url_generator(**kw):
124 def url_generator(**kw):
125 query_params = {}
125 query_params = {}
126 query_params.update(kw)
126 query_params.update(kw)
127 if f_path:
127 if f_path:
128 # changelog for file
128 # changelog for file
129 return h.route_path(
129 return h.route_path(
130 'repo_changelog_file',
130 'repo_changelog_file',
131 repo_name=c.rhodecode_db_repo.repo_name,
131 repo_name=c.rhodecode_db_repo.repo_name,
132 commit_id=commit_id, f_path=f_path,
132 commit_id=commit_id, f_path=f_path,
133 _query=query_params)
133 _query=query_params)
134 else:
134 else:
135 return h.route_path(
135 return h.route_path(
136 'repo_changelog',
136 'repo_changelog',
137 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
137 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
138
138
139 c.total_cs = len(collection)
139 c.total_cs = len(collection)
140 c.showing_commits = min(chunk_size, c.total_cs)
140 c.showing_commits = min(chunk_size, c.total_cs)
141 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
141 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
142 items_per_page=chunk_size, branch=branch_name,
142 items_per_page=chunk_size, branch=branch_name,
143 url=url_generator)
143 url=url_generator)
144
144
145 c.next_page = c.pagination.next_page
145 c.next_page = c.pagination.next_page
146 c.prev_page = c.pagination.previous_page
146 c.prev_page = c.pagination.previous_page
147
147
148 if dynamic:
148 if dynamic:
149 if self.request.GET.get('chunk') != 'next':
149 if self.request.GET.get('chunk') != 'next':
150 c.next_page = None
150 c.next_page = None
151 if self.request.GET.get('chunk') != 'prev':
151 if self.request.GET.get('chunk') != 'prev':
152 c.prev_page = None
152 c.prev_page = None
153
153
154 page_commit_ids = [x.raw_id for x in c.pagination]
154 page_commit_ids = [x.raw_id for x in c.pagination]
155 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
155 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
156 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
156 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
157
157
158 def load_default_context(self):
158 def load_default_context(self):
159 c = self._get_local_tmpl_context(include_app_defaults=True)
159 c = self._get_local_tmpl_context(include_app_defaults=True)
160
160
161 c.rhodecode_repo = self.rhodecode_vcs_repo
161 c.rhodecode_repo = self.rhodecode_vcs_repo
162 self._register_global_c(c)
162 self._register_global_c(c)
163 return c
163 return c
164
164
165 def _get_preload_attrs(self):
165 def _get_preload_attrs(self):
166 pre_load = ['author', 'branch', 'date', 'message', 'parents',
166 pre_load = ['author', 'branch', 'date', 'message', 'parents',
167 'obsolete', 'phase', 'hidden']
167 'obsolete', 'phase', 'hidden']
168 return pre_load
168 return pre_load
169
169
170 @LoginRequired()
170 @LoginRequired()
171 @HasRepoPermissionAnyDecorator(
171 @HasRepoPermissionAnyDecorator(
172 'repository.read', 'repository.write', 'repository.admin')
172 'repository.read', 'repository.write', 'repository.admin')
173 @view_config(
173 @view_config(
174 route_name='repo_changelog', request_method='GET',
174 route_name='repo_changelog', request_method='GET',
175 renderer='rhodecode:templates/changelog/changelog.mako')
175 renderer='rhodecode:templates/changelog/changelog.mako')
176 @view_config(
176 @view_config(
177 route_name='repo_changelog_file', request_method='GET',
177 route_name='repo_changelog_file', request_method='GET',
178 renderer='rhodecode:templates/changelog/changelog.mako')
178 renderer='rhodecode:templates/changelog/changelog.mako')
179 def repo_changelog(self):
179 def repo_changelog(self):
180 c = self.load_default_context()
180 c = self.load_default_context()
181
181
182 commit_id = self.request.matchdict.get('commit_id')
182 commit_id = self.request.matchdict.get('commit_id')
183 f_path = self._get_f_path(self.request.matchdict)
183 f_path = self._get_f_path(self.request.matchdict)
184 show_hidden = str2bool(self.request.GET.get('evolve'))
184
185
185 chunk_size = 20
186 chunk_size = 20
186
187
187 c.branch_name = branch_name = self.request.GET.get('branch') or ''
188 c.branch_name = branch_name = self.request.GET.get('branch') or ''
188 c.book_name = book_name = self.request.GET.get('bookmark') or ''
189 c.book_name = book_name = self.request.GET.get('bookmark') or ''
189 c.f_path = f_path
190 c.f_path = f_path
190 c.commit_id = commit_id
191 c.commit_id = commit_id
192 c.show_hidden = show_hidden
193
191 hist_limit = safe_int(self.request.GET.get('limit')) or None
194 hist_limit = safe_int(self.request.GET.get('limit')) or None
192
195
193 p = safe_int(self.request.GET.get('page', 1), 1)
196 p = safe_int(self.request.GET.get('page', 1), 1)
194
197
195 c.selected_name = branch_name or book_name
198 c.selected_name = branch_name or book_name
196 if not commit_id and branch_name:
199 if not commit_id and branch_name:
197 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
200 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
198
201
199 c.changelog_for_path = f_path
202 c.changelog_for_path = f_path
200 pre_load = self._get_preload_attrs()
203 pre_load = self._get_preload_attrs()
201
204
202 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
205 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
203
206
204 try:
207 try:
205 if f_path:
208 if f_path:
206 log.debug('generating changelog for path %s', f_path)
209 log.debug('generating changelog for path %s', f_path)
207 # get the history for the file !
210 # get the history for the file !
208 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
211 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
209
212
210 try:
213 try:
211 collection = base_commit.get_file_history(
214 collection = base_commit.get_file_history(
212 f_path, limit=hist_limit, pre_load=pre_load)
215 f_path, limit=hist_limit, pre_load=pre_load)
213 if collection and partial_xhr:
216 if collection and partial_xhr:
214 # for ajax call we remove first one since we're looking
217 # for ajax call we remove first one since we're looking
215 # at it right now in the context of a file commit
218 # at it right now in the context of a file commit
216 collection.pop(0)
219 collection.pop(0)
217 except (NodeDoesNotExistError, CommitError):
220 except (NodeDoesNotExistError, CommitError):
218 # this node is not present at tip!
221 # this node is not present at tip!
219 try:
222 try:
220 commit = self._get_commit_or_redirect(commit_id)
223 commit = self._get_commit_or_redirect(commit_id)
221 collection = commit.get_file_history(f_path)
224 collection = commit.get_file_history(f_path)
222 except RepositoryError as e:
225 except RepositoryError as e:
223 h.flash(safe_str(e), category='warning')
226 h.flash(safe_str(e), category='warning')
224 redirect_url = h.route_path(
227 redirect_url = h.route_path(
225 'repo_changelog', repo_name=self.db_repo_name)
228 'repo_changelog', repo_name=self.db_repo_name)
226 raise HTTPFound(redirect_url)
229 raise HTTPFound(redirect_url)
227 collection = list(reversed(collection))
230 collection = list(reversed(collection))
228 else:
231 else:
229 collection = self.rhodecode_vcs_repo.get_commits(
232 collection = self.rhodecode_vcs_repo.get_commits(
230 branch_name=branch_name, pre_load=pre_load)
233 branch_name=branch_name, show_hidden=show_hidden,
234 pre_load=pre_load)
231
235
232 self._load_changelog_data(
236 self._load_changelog_data(
233 c, collection, p, chunk_size, c.branch_name,
237 c, collection, p, chunk_size, c.branch_name,
234 f_path=f_path, commit_id=commit_id)
238 f_path=f_path, commit_id=commit_id)
235
239
236 except EmptyRepositoryError as e:
240 except EmptyRepositoryError as e:
237 h.flash(safe_str(h.escape(e)), category='warning')
241 h.flash(safe_str(h.escape(e)), category='warning')
238 raise HTTPFound(
242 raise HTTPFound(
239 h.route_path('repo_summary', repo_name=self.db_repo_name))
243 h.route_path('repo_summary', repo_name=self.db_repo_name))
240 except HTTPFound:
244 except HTTPFound:
241 raise
245 raise
242 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
246 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
243 log.exception(safe_str(e))
247 log.exception(safe_str(e))
244 h.flash(safe_str(h.escape(e)), category='error')
248 h.flash(safe_str(h.escape(e)), category='error')
245 raise HTTPFound(
249 raise HTTPFound(
246 h.route_path('repo_changelog', repo_name=self.db_repo_name))
250 h.route_path('repo_changelog', repo_name=self.db_repo_name))
247
251
248 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
252 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
249 # case when loading dynamic file history in file view
253 # case when loading dynamic file history in file view
250 # loading from ajax, we don't want the first result, it's popped
254 # loading from ajax, we don't want the first result, it's popped
251 # in the code above
255 # in the code above
252 html = render(
256 html = render(
253 'rhodecode:templates/changelog/changelog_file_history.mako',
257 'rhodecode:templates/changelog/changelog_file_history.mako',
254 self._get_template_context(c), self.request)
258 self._get_template_context(c), self.request)
255 return Response(html)
259 return Response(html)
256
260
257 commit_ids = []
261 commit_ids = []
258 if not f_path:
262 if not f_path:
259 # only load graph data when not in file history mode
263 # only load graph data when not in file history mode
260 commit_ids = c.pagination
264 commit_ids = c.pagination
261
265
262 c.graph_data, c.graph_commits = self._graph(
266 c.graph_data, c.graph_commits = self._graph(
263 self.rhodecode_vcs_repo, commit_ids)
267 self.rhodecode_vcs_repo, commit_ids)
264
268
265 return self._get_template_context(c)
269 return self._get_template_context(c)
266
270
267 @LoginRequired()
271 @LoginRequired()
268 @HasRepoPermissionAnyDecorator(
272 @HasRepoPermissionAnyDecorator(
269 'repository.read', 'repository.write', 'repository.admin')
273 'repository.read', 'repository.write', 'repository.admin')
270 @view_config(
274 @view_config(
271 route_name='repo_changelog_elements', request_method=('GET', 'POST'),
275 route_name='repo_changelog_elements', request_method=('GET', 'POST'),
272 renderer='rhodecode:templates/changelog/changelog_elements.mako',
276 renderer='rhodecode:templates/changelog/changelog_elements.mako',
273 xhr=True)
277 xhr=True)
274 @view_config(
278 @view_config(
275 route_name='repo_changelog_elements_file', request_method=('GET', 'POST'),
279 route_name='repo_changelog_elements_file', request_method=('GET', 'POST'),
276 renderer='rhodecode:templates/changelog/changelog_elements.mako',
280 renderer='rhodecode:templates/changelog/changelog_elements.mako',
277 xhr=True)
281 xhr=True)
278 def repo_changelog_elements(self):
282 def repo_changelog_elements(self):
279 c = self.load_default_context()
283 c = self.load_default_context()
280 commit_id = self.request.matchdict.get('commit_id')
284 commit_id = self.request.matchdict.get('commit_id')
281 f_path = self._get_f_path(self.request.matchdict)
285 f_path = self._get_f_path(self.request.matchdict)
286 show_hidden = str2bool(self.request.GET.get('evolve'))
287
282 chunk_size = 20
288 chunk_size = 20
283 hist_limit = safe_int(self.request.GET.get('limit')) or None
289 hist_limit = safe_int(self.request.GET.get('limit')) or None
284
290
285 def wrap_for_error(err):
291 def wrap_for_error(err):
286 html = '<tr>' \
292 html = '<tr>' \
287 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
293 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
288 '</tr>'.format(err)
294 '</tr>'.format(err)
289 return Response(html)
295 return Response(html)
290
296
291 c.branch_name = branch_name = self.request.GET.get('branch') or ''
297 c.branch_name = branch_name = self.request.GET.get('branch') or ''
292 c.book_name = book_name = self.request.GET.get('bookmark') or ''
298 c.book_name = book_name = self.request.GET.get('bookmark') or ''
293 c.f_path = f_path
299 c.f_path = f_path
294 c.commit_id = commit_id
300 c.commit_id = commit_id
301 c.show_hidden = show_hidden
295
302
296 c.selected_name = branch_name or book_name
303 c.selected_name = branch_name or book_name
297 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
304 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
298 return wrap_for_error(
305 return wrap_for_error(
299 safe_str('Branch: {} is not valid'.format(branch_name)))
306 safe_str('Branch: {} is not valid'.format(branch_name)))
300
307
301 pre_load = self._get_preload_attrs()
308 pre_load = self._get_preload_attrs()
302
309
303 if f_path:
310 if f_path:
304 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
311 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
305 collection = base_commit.get_file_history(
312 collection = base_commit.get_file_history(
306 f_path, limit=hist_limit, pre_load=pre_load)
313 f_path, limit=hist_limit, pre_load=pre_load)
307 collection = list(reversed(collection))
314 collection = list(reversed(collection))
308 else:
315 else:
309 collection = self.rhodecode_vcs_repo.get_commits(
316 collection = self.rhodecode_vcs_repo.get_commits(
310 branch_name=branch_name, pre_load=pre_load)
317 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load)
311
318
312 p = safe_int(self.request.GET.get('page', 1), 1)
319 p = safe_int(self.request.GET.get('page', 1), 1)
313 try:
320 try:
314 self._load_changelog_data(
321 self._load_changelog_data(
315 c, collection, p, chunk_size, dynamic=True,
322 c, collection, p, chunk_size, dynamic=True,
316 f_path=f_path, commit_id=commit_id)
323 f_path=f_path, commit_id=commit_id)
317 except EmptyRepositoryError as e:
324 except EmptyRepositoryError as e:
318 return wrap_for_error(safe_str(e))
325 return wrap_for_error(safe_str(e))
319 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
326 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
320 log.exception('Failed to fetch commits')
327 log.exception('Failed to fetch commits')
321 return wrap_for_error(safe_str(e))
328 return wrap_for_error(safe_str(e))
322
329
323 prev_data = None
330 prev_data = None
324 next_data = None
331 next_data = None
325
332
326 prev_graph = json.loads(self.request.POST.get('graph') or '{}')
333 prev_graph = json.loads(self.request.POST.get('graph') or '{}')
327
334
328 if self.request.GET.get('chunk') == 'prev':
335 if self.request.GET.get('chunk') == 'prev':
329 next_data = prev_graph
336 next_data = prev_graph
330 elif self.request.GET.get('chunk') == 'next':
337 elif self.request.GET.get('chunk') == 'next':
331 prev_data = prev_graph
338 prev_data = prev_graph
332
339
333 commit_ids = []
340 commit_ids = []
334 if not f_path:
341 if not f_path:
335 # only load graph data when not in file history mode
342 # only load graph data when not in file history mode
336 commit_ids = c.pagination
343 commit_ids = c.pagination
337
344
338 c.graph_data, c.graph_commits = self._graph(
345 c.graph_data, c.graph_commits = self._graph(
339 self.rhodecode_vcs_repo, commit_ids,
346 self.rhodecode_vcs_repo, commit_ids,
340 prev_data=prev_data, next_data=next_data)
347 prev_data=prev_data, next_data=next_data)
341
348
342 return self._get_template_context(c)
349 return self._get_template_context(c)
@@ -1,1590 +1,1591 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24
24
25 import collections
25 import collections
26 import datetime
26 import datetime
27 import itertools
27 import itertools
28 import logging
28 import logging
29 import os
29 import os
30 import time
30 import time
31 import warnings
31 import warnings
32
32
33 from zope.cachedescriptors.property import Lazy as LazyProperty
33 from zope.cachedescriptors.property import Lazy as LazyProperty
34
34
35 from rhodecode.lib.utils2 import safe_str, safe_unicode
35 from rhodecode.lib.utils2 import safe_str, safe_unicode
36 from rhodecode.lib.vcs import connection
36 from rhodecode.lib.vcs import connection
37 from rhodecode.lib.vcs.utils import author_name, author_email
37 from rhodecode.lib.vcs.utils import author_name, author_email
38 from rhodecode.lib.vcs.conf import settings
38 from rhodecode.lib.vcs.conf import settings
39 from rhodecode.lib.vcs.exceptions import (
39 from rhodecode.lib.vcs.exceptions import (
40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
44 RepositoryError)
44 RepositoryError)
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 FILEMODE_DEFAULT = 0100644
50 FILEMODE_DEFAULT = 0100644
51 FILEMODE_EXECUTABLE = 0100755
51 FILEMODE_EXECUTABLE = 0100755
52
52
53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
54 MergeResponse = collections.namedtuple(
54 MergeResponse = collections.namedtuple(
55 'MergeResponse',
55 'MergeResponse',
56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
57
57
58
58
59 class MergeFailureReason(object):
59 class MergeFailureReason(object):
60 """
60 """
61 Enumeration with all the reasons why the server side merge could fail.
61 Enumeration with all the reasons why the server side merge could fail.
62
62
63 DO NOT change the number of the reasons, as they may be stored in the
63 DO NOT change the number of the reasons, as they may be stored in the
64 database.
64 database.
65
65
66 Changing the name of a reason is acceptable and encouraged to deprecate old
66 Changing the name of a reason is acceptable and encouraged to deprecate old
67 reasons.
67 reasons.
68 """
68 """
69
69
70 # Everything went well.
70 # Everything went well.
71 NONE = 0
71 NONE = 0
72
72
73 # An unexpected exception was raised. Check the logs for more details.
73 # An unexpected exception was raised. Check the logs for more details.
74 UNKNOWN = 1
74 UNKNOWN = 1
75
75
76 # The merge was not successful, there are conflicts.
76 # The merge was not successful, there are conflicts.
77 MERGE_FAILED = 2
77 MERGE_FAILED = 2
78
78
79 # The merge succeeded but we could not push it to the target repository.
79 # The merge succeeded but we could not push it to the target repository.
80 PUSH_FAILED = 3
80 PUSH_FAILED = 3
81
81
82 # The specified target is not a head in the target repository.
82 # The specified target is not a head in the target repository.
83 TARGET_IS_NOT_HEAD = 4
83 TARGET_IS_NOT_HEAD = 4
84
84
85 # The source repository contains more branches than the target. Pushing
85 # The source repository contains more branches than the target. Pushing
86 # the merge will create additional branches in the target.
86 # the merge will create additional branches in the target.
87 HG_SOURCE_HAS_MORE_BRANCHES = 5
87 HG_SOURCE_HAS_MORE_BRANCHES = 5
88
88
89 # The target reference has multiple heads. That does not allow to correctly
89 # The target reference has multiple heads. That does not allow to correctly
90 # identify the target location. This could only happen for mercurial
90 # identify the target location. This could only happen for mercurial
91 # branches.
91 # branches.
92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93
93
94 # The target repository is locked
94 # The target repository is locked
95 TARGET_IS_LOCKED = 7
95 TARGET_IS_LOCKED = 7
96
96
97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
98 # A involved commit could not be found.
98 # A involved commit could not be found.
99 _DEPRECATED_MISSING_COMMIT = 8
99 _DEPRECATED_MISSING_COMMIT = 8
100
100
101 # The target repo reference is missing.
101 # The target repo reference is missing.
102 MISSING_TARGET_REF = 9
102 MISSING_TARGET_REF = 9
103
103
104 # The source repo reference is missing.
104 # The source repo reference is missing.
105 MISSING_SOURCE_REF = 10
105 MISSING_SOURCE_REF = 10
106
106
107 # The merge was not successful, there are conflicts related to sub
107 # The merge was not successful, there are conflicts related to sub
108 # repositories.
108 # repositories.
109 SUBREPO_MERGE_FAILED = 11
109 SUBREPO_MERGE_FAILED = 11
110
110
111
111
112 class UpdateFailureReason(object):
112 class UpdateFailureReason(object):
113 """
113 """
114 Enumeration with all the reasons why the pull request update could fail.
114 Enumeration with all the reasons why the pull request update could fail.
115
115
116 DO NOT change the number of the reasons, as they may be stored in the
116 DO NOT change the number of the reasons, as they may be stored in the
117 database.
117 database.
118
118
119 Changing the name of a reason is acceptable and encouraged to deprecate old
119 Changing the name of a reason is acceptable and encouraged to deprecate old
120 reasons.
120 reasons.
121 """
121 """
122
122
123 # Everything went well.
123 # Everything went well.
124 NONE = 0
124 NONE = 0
125
125
126 # An unexpected exception was raised. Check the logs for more details.
126 # An unexpected exception was raised. Check the logs for more details.
127 UNKNOWN = 1
127 UNKNOWN = 1
128
128
129 # The pull request is up to date.
129 # The pull request is up to date.
130 NO_CHANGE = 2
130 NO_CHANGE = 2
131
131
132 # The pull request has a reference type that is not supported for update.
132 # The pull request has a reference type that is not supported for update.
133 WRONG_REF_TYPE = 3
133 WRONG_REF_TYPE = 3
134
134
135 # Update failed because the target reference is missing.
135 # Update failed because the target reference is missing.
136 MISSING_TARGET_REF = 4
136 MISSING_TARGET_REF = 4
137
137
138 # Update failed because the source reference is missing.
138 # Update failed because the source reference is missing.
139 MISSING_SOURCE_REF = 5
139 MISSING_SOURCE_REF = 5
140
140
141
141
142 class BaseRepository(object):
142 class BaseRepository(object):
143 """
143 """
144 Base Repository for final backends
144 Base Repository for final backends
145
145
146 .. attribute:: DEFAULT_BRANCH_NAME
146 .. attribute:: DEFAULT_BRANCH_NAME
147
147
148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
149
149
150 .. attribute:: commit_ids
150 .. attribute:: commit_ids
151
151
152 list of all available commit ids, in ascending order
152 list of all available commit ids, in ascending order
153
153
154 .. attribute:: path
154 .. attribute:: path
155
155
156 absolute path to the repository
156 absolute path to the repository
157
157
158 .. attribute:: bookmarks
158 .. attribute:: bookmarks
159
159
160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
161 there are no bookmarks or the backend implementation does not support
161 there are no bookmarks or the backend implementation does not support
162 bookmarks.
162 bookmarks.
163
163
164 .. attribute:: tags
164 .. attribute:: tags
165
165
166 Mapping from name to :term:`Commit ID` of the tag.
166 Mapping from name to :term:`Commit ID` of the tag.
167
167
168 """
168 """
169
169
170 DEFAULT_BRANCH_NAME = None
170 DEFAULT_BRANCH_NAME = None
171 DEFAULT_CONTACT = u"Unknown"
171 DEFAULT_CONTACT = u"Unknown"
172 DEFAULT_DESCRIPTION = u"unknown"
172 DEFAULT_DESCRIPTION = u"unknown"
173 EMPTY_COMMIT_ID = '0' * 40
173 EMPTY_COMMIT_ID = '0' * 40
174
174
175 path = None
175 path = None
176
176
177 def __init__(self, repo_path, config=None, create=False, **kwargs):
177 def __init__(self, repo_path, config=None, create=False, **kwargs):
178 """
178 """
179 Initializes repository. Raises RepositoryError if repository could
179 Initializes repository. Raises RepositoryError if repository could
180 not be find at the given ``repo_path`` or directory at ``repo_path``
180 not be find at the given ``repo_path`` or directory at ``repo_path``
181 exists and ``create`` is set to True.
181 exists and ``create`` is set to True.
182
182
183 :param repo_path: local path of the repository
183 :param repo_path: local path of the repository
184 :param config: repository configuration
184 :param config: repository configuration
185 :param create=False: if set to True, would try to create repository.
185 :param create=False: if set to True, would try to create repository.
186 :param src_url=None: if set, should be proper url from which repository
186 :param src_url=None: if set, should be proper url from which repository
187 would be cloned; requires ``create`` parameter to be set to True -
187 would be cloned; requires ``create`` parameter to be set to True -
188 raises RepositoryError if src_url is set and create evaluates to
188 raises RepositoryError if src_url is set and create evaluates to
189 False
189 False
190 """
190 """
191 raise NotImplementedError
191 raise NotImplementedError
192
192
193 def __repr__(self):
193 def __repr__(self):
194 return '<%s at %s>' % (self.__class__.__name__, self.path)
194 return '<%s at %s>' % (self.__class__.__name__, self.path)
195
195
196 def __len__(self):
196 def __len__(self):
197 return self.count()
197 return self.count()
198
198
199 def __eq__(self, other):
199 def __eq__(self, other):
200 same_instance = isinstance(other, self.__class__)
200 same_instance = isinstance(other, self.__class__)
201 return same_instance and other.path == self.path
201 return same_instance and other.path == self.path
202
202
203 def __ne__(self, other):
203 def __ne__(self, other):
204 return not self.__eq__(other)
204 return not self.__eq__(other)
205
205
206 @LazyProperty
206 @LazyProperty
207 def EMPTY_COMMIT(self):
207 def EMPTY_COMMIT(self):
208 return EmptyCommit(self.EMPTY_COMMIT_ID)
208 return EmptyCommit(self.EMPTY_COMMIT_ID)
209
209
210 @LazyProperty
210 @LazyProperty
211 def alias(self):
211 def alias(self):
212 for k, v in settings.BACKENDS.items():
212 for k, v in settings.BACKENDS.items():
213 if v.split('.')[-1] == str(self.__class__.__name__):
213 if v.split('.')[-1] == str(self.__class__.__name__):
214 return k
214 return k
215
215
216 @LazyProperty
216 @LazyProperty
217 def name(self):
217 def name(self):
218 return safe_unicode(os.path.basename(self.path))
218 return safe_unicode(os.path.basename(self.path))
219
219
220 @LazyProperty
220 @LazyProperty
221 def description(self):
221 def description(self):
222 raise NotImplementedError
222 raise NotImplementedError
223
223
224 def refs(self):
224 def refs(self):
225 """
225 """
226 returns a `dict` with branches, bookmarks, tags, and closed_branches
226 returns a `dict` with branches, bookmarks, tags, and closed_branches
227 for this repository
227 for this repository
228 """
228 """
229 return dict(
229 return dict(
230 branches=self.branches,
230 branches=self.branches,
231 branches_closed=self.branches_closed,
231 branches_closed=self.branches_closed,
232 tags=self.tags,
232 tags=self.tags,
233 bookmarks=self.bookmarks
233 bookmarks=self.bookmarks
234 )
234 )
235
235
236 @LazyProperty
236 @LazyProperty
237 def branches(self):
237 def branches(self):
238 """
238 """
239 A `dict` which maps branch names to commit ids.
239 A `dict` which maps branch names to commit ids.
240 """
240 """
241 raise NotImplementedError
241 raise NotImplementedError
242
242
243 @LazyProperty
243 @LazyProperty
244 def tags(self):
244 def tags(self):
245 """
245 """
246 A `dict` which maps tags names to commit ids.
246 A `dict` which maps tags names to commit ids.
247 """
247 """
248 raise NotImplementedError
248 raise NotImplementedError
249
249
250 @LazyProperty
250 @LazyProperty
251 def size(self):
251 def size(self):
252 """
252 """
253 Returns combined size in bytes for all repository files
253 Returns combined size in bytes for all repository files
254 """
254 """
255 tip = self.get_commit()
255 tip = self.get_commit()
256 return tip.size
256 return tip.size
257
257
258 def size_at_commit(self, commit_id):
258 def size_at_commit(self, commit_id):
259 commit = self.get_commit(commit_id)
259 commit = self.get_commit(commit_id)
260 return commit.size
260 return commit.size
261
261
262 def is_empty(self):
262 def is_empty(self):
263 return not bool(self.commit_ids)
263 return not bool(self.commit_ids)
264
264
265 @staticmethod
265 @staticmethod
266 def check_url(url, config):
266 def check_url(url, config):
267 """
267 """
268 Function will check given url and try to verify if it's a valid
268 Function will check given url and try to verify if it's a valid
269 link.
269 link.
270 """
270 """
271 raise NotImplementedError
271 raise NotImplementedError
272
272
273 @staticmethod
273 @staticmethod
274 def is_valid_repository(path):
274 def is_valid_repository(path):
275 """
275 """
276 Check if given `path` contains a valid repository of this backend
276 Check if given `path` contains a valid repository of this backend
277 """
277 """
278 raise NotImplementedError
278 raise NotImplementedError
279
279
280 # ==========================================================================
280 # ==========================================================================
281 # COMMITS
281 # COMMITS
282 # ==========================================================================
282 # ==========================================================================
283
283
284 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
284 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
285 """
285 """
286 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
286 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
287 are both None, most recent commit is returned.
287 are both None, most recent commit is returned.
288
288
289 :param pre_load: Optional. List of commit attributes to load.
289 :param pre_load: Optional. List of commit attributes to load.
290
290
291 :raises ``EmptyRepositoryError``: if there are no commits
291 :raises ``EmptyRepositoryError``: if there are no commits
292 """
292 """
293 raise NotImplementedError
293 raise NotImplementedError
294
294
295 def __iter__(self):
295 def __iter__(self):
296 for commit_id in self.commit_ids:
296 for commit_id in self.commit_ids:
297 yield self.get_commit(commit_id=commit_id)
297 yield self.get_commit(commit_id=commit_id)
298
298
299 def get_commits(
299 def get_commits(
300 self, start_id=None, end_id=None, start_date=None, end_date=None,
300 self, start_id=None, end_id=None, start_date=None, end_date=None,
301 branch_name=None, pre_load=None):
301 branch_name=None, show_hidden=False, pre_load=None):
302 """
302 """
303 Returns iterator of `BaseCommit` objects from start to end
303 Returns iterator of `BaseCommit` objects from start to end
304 not inclusive. This should behave just like a list, ie. end is not
304 not inclusive. This should behave just like a list, ie. end is not
305 inclusive.
305 inclusive.
306
306
307 :param start_id: None or str, must be a valid commit id
307 :param start_id: None or str, must be a valid commit id
308 :param end_id: None or str, must be a valid commit id
308 :param end_id: None or str, must be a valid commit id
309 :param start_date:
309 :param start_date:
310 :param end_date:
310 :param end_date:
311 :param branch_name:
311 :param branch_name:
312 :param show_hidden:
312 :param pre_load:
313 :param pre_load:
313 """
314 """
314 raise NotImplementedError
315 raise NotImplementedError
315
316
316 def __getitem__(self, key):
317 def __getitem__(self, key):
317 """
318 """
318 Allows index based access to the commit objects of this repository.
319 Allows index based access to the commit objects of this repository.
319 """
320 """
320 pre_load = ["author", "branch", "date", "message", "parents"]
321 pre_load = ["author", "branch", "date", "message", "parents"]
321 if isinstance(key, slice):
322 if isinstance(key, slice):
322 return self._get_range(key, pre_load)
323 return self._get_range(key, pre_load)
323 return self.get_commit(commit_idx=key, pre_load=pre_load)
324 return self.get_commit(commit_idx=key, pre_load=pre_load)
324
325
325 def _get_range(self, slice_obj, pre_load):
326 def _get_range(self, slice_obj, pre_load):
326 for commit_id in self.commit_ids.__getitem__(slice_obj):
327 for commit_id in self.commit_ids.__getitem__(slice_obj):
327 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
328 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
328
329
329 def count(self):
330 def count(self):
330 return len(self.commit_ids)
331 return len(self.commit_ids)
331
332
332 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
333 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
333 """
334 """
334 Creates and returns a tag for the given ``commit_id``.
335 Creates and returns a tag for the given ``commit_id``.
335
336
336 :param name: name for new tag
337 :param name: name for new tag
337 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
338 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
338 :param commit_id: commit id for which new tag would be created
339 :param commit_id: commit id for which new tag would be created
339 :param message: message of the tag's commit
340 :param message: message of the tag's commit
340 :param date: date of tag's commit
341 :param date: date of tag's commit
341
342
342 :raises TagAlreadyExistError: if tag with same name already exists
343 :raises TagAlreadyExistError: if tag with same name already exists
343 """
344 """
344 raise NotImplementedError
345 raise NotImplementedError
345
346
346 def remove_tag(self, name, user, message=None, date=None):
347 def remove_tag(self, name, user, message=None, date=None):
347 """
348 """
348 Removes tag with the given ``name``.
349 Removes tag with the given ``name``.
349
350
350 :param name: name of the tag to be removed
351 :param name: name of the tag to be removed
351 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
352 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
352 :param message: message of the tag's removal commit
353 :param message: message of the tag's removal commit
353 :param date: date of tag's removal commit
354 :param date: date of tag's removal commit
354
355
355 :raises TagDoesNotExistError: if tag with given name does not exists
356 :raises TagDoesNotExistError: if tag with given name does not exists
356 """
357 """
357 raise NotImplementedError
358 raise NotImplementedError
358
359
359 def get_diff(
360 def get_diff(
360 self, commit1, commit2, path=None, ignore_whitespace=False,
361 self, commit1, commit2, path=None, ignore_whitespace=False,
361 context=3, path1=None):
362 context=3, path1=None):
362 """
363 """
363 Returns (git like) *diff*, as plain text. Shows changes introduced by
364 Returns (git like) *diff*, as plain text. Shows changes introduced by
364 `commit2` since `commit1`.
365 `commit2` since `commit1`.
365
366
366 :param commit1: Entry point from which diff is shown. Can be
367 :param commit1: Entry point from which diff is shown. Can be
367 ``self.EMPTY_COMMIT`` - in this case, patch showing all
368 ``self.EMPTY_COMMIT`` - in this case, patch showing all
368 the changes since empty state of the repository until `commit2`
369 the changes since empty state of the repository until `commit2`
369 :param commit2: Until which commit changes should be shown.
370 :param commit2: Until which commit changes should be shown.
370 :param path: Can be set to a path of a file to create a diff of that
371 :param path: Can be set to a path of a file to create a diff of that
371 file. If `path1` is also set, this value is only associated to
372 file. If `path1` is also set, this value is only associated to
372 `commit2`.
373 `commit2`.
373 :param ignore_whitespace: If set to ``True``, would not show whitespace
374 :param ignore_whitespace: If set to ``True``, would not show whitespace
374 changes. Defaults to ``False``.
375 changes. Defaults to ``False``.
375 :param context: How many lines before/after changed lines should be
376 :param context: How many lines before/after changed lines should be
376 shown. Defaults to ``3``.
377 shown. Defaults to ``3``.
377 :param path1: Can be set to a path to associate with `commit1`. This
378 :param path1: Can be set to a path to associate with `commit1`. This
378 parameter works only for backends which support diff generation for
379 parameter works only for backends which support diff generation for
379 different paths. Other backends will raise a `ValueError` if `path1`
380 different paths. Other backends will raise a `ValueError` if `path1`
380 is set and has a different value than `path`.
381 is set and has a different value than `path`.
381 :param file_path: filter this diff by given path pattern
382 :param file_path: filter this diff by given path pattern
382 """
383 """
383 raise NotImplementedError
384 raise NotImplementedError
384
385
385 def strip(self, commit_id, branch=None):
386 def strip(self, commit_id, branch=None):
386 """
387 """
387 Strip given commit_id from the repository
388 Strip given commit_id from the repository
388 """
389 """
389 raise NotImplementedError
390 raise NotImplementedError
390
391
391 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
392 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
392 """
393 """
393 Return a latest common ancestor commit if one exists for this repo
394 Return a latest common ancestor commit if one exists for this repo
394 `commit_id1` vs `commit_id2` from `repo2`.
395 `commit_id1` vs `commit_id2` from `repo2`.
395
396
396 :param commit_id1: Commit it from this repository to use as a
397 :param commit_id1: Commit it from this repository to use as a
397 target for the comparison.
398 target for the comparison.
398 :param commit_id2: Source commit id to use for comparison.
399 :param commit_id2: Source commit id to use for comparison.
399 :param repo2: Source repository to use for comparison.
400 :param repo2: Source repository to use for comparison.
400 """
401 """
401 raise NotImplementedError
402 raise NotImplementedError
402
403
403 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
404 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
404 """
405 """
405 Compare this repository's revision `commit_id1` with `commit_id2`.
406 Compare this repository's revision `commit_id1` with `commit_id2`.
406
407
407 Returns a tuple(commits, ancestor) that would be merged from
408 Returns a tuple(commits, ancestor) that would be merged from
408 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
409 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
409 will be returned as ancestor.
410 will be returned as ancestor.
410
411
411 :param commit_id1: Commit it from this repository to use as a
412 :param commit_id1: Commit it from this repository to use as a
412 target for the comparison.
413 target for the comparison.
413 :param commit_id2: Source commit id to use for comparison.
414 :param commit_id2: Source commit id to use for comparison.
414 :param repo2: Source repository to use for comparison.
415 :param repo2: Source repository to use for comparison.
415 :param merge: If set to ``True`` will do a merge compare which also
416 :param merge: If set to ``True`` will do a merge compare which also
416 returns the common ancestor.
417 returns the common ancestor.
417 :param pre_load: Optional. List of commit attributes to load.
418 :param pre_load: Optional. List of commit attributes to load.
418 """
419 """
419 raise NotImplementedError
420 raise NotImplementedError
420
421
421 def merge(self, target_ref, source_repo, source_ref, workspace_id,
422 def merge(self, target_ref, source_repo, source_ref, workspace_id,
422 user_name='', user_email='', message='', dry_run=False,
423 user_name='', user_email='', message='', dry_run=False,
423 use_rebase=False, close_branch=False):
424 use_rebase=False, close_branch=False):
424 """
425 """
425 Merge the revisions specified in `source_ref` from `source_repo`
426 Merge the revisions specified in `source_ref` from `source_repo`
426 onto the `target_ref` of this repository.
427 onto the `target_ref` of this repository.
427
428
428 `source_ref` and `target_ref` are named tupls with the following
429 `source_ref` and `target_ref` are named tupls with the following
429 fields `type`, `name` and `commit_id`.
430 fields `type`, `name` and `commit_id`.
430
431
431 Returns a MergeResponse named tuple with the following fields
432 Returns a MergeResponse named tuple with the following fields
432 'possible', 'executed', 'source_commit', 'target_commit',
433 'possible', 'executed', 'source_commit', 'target_commit',
433 'merge_commit'.
434 'merge_commit'.
434
435
435 :param target_ref: `target_ref` points to the commit on top of which
436 :param target_ref: `target_ref` points to the commit on top of which
436 the `source_ref` should be merged.
437 the `source_ref` should be merged.
437 :param source_repo: The repository that contains the commits to be
438 :param source_repo: The repository that contains the commits to be
438 merged.
439 merged.
439 :param source_ref: `source_ref` points to the topmost commit from
440 :param source_ref: `source_ref` points to the topmost commit from
440 the `source_repo` which should be merged.
441 the `source_repo` which should be merged.
441 :param workspace_id: `workspace_id` unique identifier.
442 :param workspace_id: `workspace_id` unique identifier.
442 :param user_name: Merge commit `user_name`.
443 :param user_name: Merge commit `user_name`.
443 :param user_email: Merge commit `user_email`.
444 :param user_email: Merge commit `user_email`.
444 :param message: Merge commit `message`.
445 :param message: Merge commit `message`.
445 :param dry_run: If `True` the merge will not take place.
446 :param dry_run: If `True` the merge will not take place.
446 :param use_rebase: If `True` commits from the source will be rebased
447 :param use_rebase: If `True` commits from the source will be rebased
447 on top of the target instead of being merged.
448 on top of the target instead of being merged.
448 :param close_branch: If `True` branch will be close before merging it
449 :param close_branch: If `True` branch will be close before merging it
449 """
450 """
450 if dry_run:
451 if dry_run:
451 message = message or 'dry_run_merge_message'
452 message = message or 'dry_run_merge_message'
452 user_email = user_email or 'dry-run-merge@rhodecode.com'
453 user_email = user_email or 'dry-run-merge@rhodecode.com'
453 user_name = user_name or 'Dry-Run User'
454 user_name = user_name or 'Dry-Run User'
454 else:
455 else:
455 if not user_name:
456 if not user_name:
456 raise ValueError('user_name cannot be empty')
457 raise ValueError('user_name cannot be empty')
457 if not user_email:
458 if not user_email:
458 raise ValueError('user_email cannot be empty')
459 raise ValueError('user_email cannot be empty')
459 if not message:
460 if not message:
460 raise ValueError('message cannot be empty')
461 raise ValueError('message cannot be empty')
461
462
462 shadow_repository_path = self._maybe_prepare_merge_workspace(
463 shadow_repository_path = self._maybe_prepare_merge_workspace(
463 workspace_id, target_ref)
464 workspace_id, target_ref)
464
465
465 try:
466 try:
466 return self._merge_repo(
467 return self._merge_repo(
467 shadow_repository_path, target_ref, source_repo,
468 shadow_repository_path, target_ref, source_repo,
468 source_ref, message, user_name, user_email, dry_run=dry_run,
469 source_ref, message, user_name, user_email, dry_run=dry_run,
469 use_rebase=use_rebase, close_branch=close_branch)
470 use_rebase=use_rebase, close_branch=close_branch)
470 except RepositoryError:
471 except RepositoryError:
471 log.exception(
472 log.exception(
472 'Unexpected failure when running merge, dry-run=%s',
473 'Unexpected failure when running merge, dry-run=%s',
473 dry_run)
474 dry_run)
474 return MergeResponse(
475 return MergeResponse(
475 False, False, None, MergeFailureReason.UNKNOWN)
476 False, False, None, MergeFailureReason.UNKNOWN)
476
477
477 def _merge_repo(self, shadow_repository_path, target_ref,
478 def _merge_repo(self, shadow_repository_path, target_ref,
478 source_repo, source_ref, merge_message,
479 source_repo, source_ref, merge_message,
479 merger_name, merger_email, dry_run=False,
480 merger_name, merger_email, dry_run=False,
480 use_rebase=False, close_branch=False):
481 use_rebase=False, close_branch=False):
481 """Internal implementation of merge."""
482 """Internal implementation of merge."""
482 raise NotImplementedError
483 raise NotImplementedError
483
484
484 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
485 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
485 """
486 """
486 Create the merge workspace.
487 Create the merge workspace.
487
488
488 :param workspace_id: `workspace_id` unique identifier.
489 :param workspace_id: `workspace_id` unique identifier.
489 """
490 """
490 raise NotImplementedError
491 raise NotImplementedError
491
492
492 def cleanup_merge_workspace(self, workspace_id):
493 def cleanup_merge_workspace(self, workspace_id):
493 """
494 """
494 Remove merge workspace.
495 Remove merge workspace.
495
496
496 This function MUST not fail in case there is no workspace associated to
497 This function MUST not fail in case there is no workspace associated to
497 the given `workspace_id`.
498 the given `workspace_id`.
498
499
499 :param workspace_id: `workspace_id` unique identifier.
500 :param workspace_id: `workspace_id` unique identifier.
500 """
501 """
501 raise NotImplementedError
502 raise NotImplementedError
502
503
503 # ========== #
504 # ========== #
504 # COMMIT API #
505 # COMMIT API #
505 # ========== #
506 # ========== #
506
507
507 @LazyProperty
508 @LazyProperty
508 def in_memory_commit(self):
509 def in_memory_commit(self):
509 """
510 """
510 Returns :class:`InMemoryCommit` object for this repository.
511 Returns :class:`InMemoryCommit` object for this repository.
511 """
512 """
512 raise NotImplementedError
513 raise NotImplementedError
513
514
514 # ======================== #
515 # ======================== #
515 # UTILITIES FOR SUBCLASSES #
516 # UTILITIES FOR SUBCLASSES #
516 # ======================== #
517 # ======================== #
517
518
518 def _validate_diff_commits(self, commit1, commit2):
519 def _validate_diff_commits(self, commit1, commit2):
519 """
520 """
520 Validates that the given commits are related to this repository.
521 Validates that the given commits are related to this repository.
521
522
522 Intended as a utility for sub classes to have a consistent validation
523 Intended as a utility for sub classes to have a consistent validation
523 of input parameters in methods like :meth:`get_diff`.
524 of input parameters in methods like :meth:`get_diff`.
524 """
525 """
525 self._validate_commit(commit1)
526 self._validate_commit(commit1)
526 self._validate_commit(commit2)
527 self._validate_commit(commit2)
527 if (isinstance(commit1, EmptyCommit) and
528 if (isinstance(commit1, EmptyCommit) and
528 isinstance(commit2, EmptyCommit)):
529 isinstance(commit2, EmptyCommit)):
529 raise ValueError("Cannot compare two empty commits")
530 raise ValueError("Cannot compare two empty commits")
530
531
531 def _validate_commit(self, commit):
532 def _validate_commit(self, commit):
532 if not isinstance(commit, BaseCommit):
533 if not isinstance(commit, BaseCommit):
533 raise TypeError(
534 raise TypeError(
534 "%s is not of type BaseCommit" % repr(commit))
535 "%s is not of type BaseCommit" % repr(commit))
535 if commit.repository != self and not isinstance(commit, EmptyCommit):
536 if commit.repository != self and not isinstance(commit, EmptyCommit):
536 raise ValueError(
537 raise ValueError(
537 "Commit %s must be a valid commit from this repository %s, "
538 "Commit %s must be a valid commit from this repository %s, "
538 "related to this repository instead %s." %
539 "related to this repository instead %s." %
539 (commit, self, commit.repository))
540 (commit, self, commit.repository))
540
541
541 def _validate_commit_id(self, commit_id):
542 def _validate_commit_id(self, commit_id):
542 if not isinstance(commit_id, basestring):
543 if not isinstance(commit_id, basestring):
543 raise TypeError("commit_id must be a string value")
544 raise TypeError("commit_id must be a string value")
544
545
545 def _validate_commit_idx(self, commit_idx):
546 def _validate_commit_idx(self, commit_idx):
546 if not isinstance(commit_idx, (int, long)):
547 if not isinstance(commit_idx, (int, long)):
547 raise TypeError("commit_idx must be a numeric value")
548 raise TypeError("commit_idx must be a numeric value")
548
549
549 def _validate_branch_name(self, branch_name):
550 def _validate_branch_name(self, branch_name):
550 if branch_name and branch_name not in self.branches_all:
551 if branch_name and branch_name not in self.branches_all:
551 msg = ("Branch %s not found in %s" % (branch_name, self))
552 msg = ("Branch %s not found in %s" % (branch_name, self))
552 raise BranchDoesNotExistError(msg)
553 raise BranchDoesNotExistError(msg)
553
554
554 #
555 #
555 # Supporting deprecated API parts
556 # Supporting deprecated API parts
556 # TODO: johbo: consider to move this into a mixin
557 # TODO: johbo: consider to move this into a mixin
557 #
558 #
558
559
559 @property
560 @property
560 def EMPTY_CHANGESET(self):
561 def EMPTY_CHANGESET(self):
561 warnings.warn(
562 warnings.warn(
562 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
563 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
563 return self.EMPTY_COMMIT_ID
564 return self.EMPTY_COMMIT_ID
564
565
565 @property
566 @property
566 def revisions(self):
567 def revisions(self):
567 warnings.warn("Use commits attribute instead", DeprecationWarning)
568 warnings.warn("Use commits attribute instead", DeprecationWarning)
568 return self.commit_ids
569 return self.commit_ids
569
570
570 @revisions.setter
571 @revisions.setter
571 def revisions(self, value):
572 def revisions(self, value):
572 warnings.warn("Use commits attribute instead", DeprecationWarning)
573 warnings.warn("Use commits attribute instead", DeprecationWarning)
573 self.commit_ids = value
574 self.commit_ids = value
574
575
575 def get_changeset(self, revision=None, pre_load=None):
576 def get_changeset(self, revision=None, pre_load=None):
576 warnings.warn("Use get_commit instead", DeprecationWarning)
577 warnings.warn("Use get_commit instead", DeprecationWarning)
577 commit_id = None
578 commit_id = None
578 commit_idx = None
579 commit_idx = None
579 if isinstance(revision, basestring):
580 if isinstance(revision, basestring):
580 commit_id = revision
581 commit_id = revision
581 else:
582 else:
582 commit_idx = revision
583 commit_idx = revision
583 return self.get_commit(
584 return self.get_commit(
584 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
585 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
585
586
586 def get_changesets(
587 def get_changesets(
587 self, start=None, end=None, start_date=None, end_date=None,
588 self, start=None, end=None, start_date=None, end_date=None,
588 branch_name=None, pre_load=None):
589 branch_name=None, pre_load=None):
589 warnings.warn("Use get_commits instead", DeprecationWarning)
590 warnings.warn("Use get_commits instead", DeprecationWarning)
590 start_id = self._revision_to_commit(start)
591 start_id = self._revision_to_commit(start)
591 end_id = self._revision_to_commit(end)
592 end_id = self._revision_to_commit(end)
592 return self.get_commits(
593 return self.get_commits(
593 start_id=start_id, end_id=end_id, start_date=start_date,
594 start_id=start_id, end_id=end_id, start_date=start_date,
594 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
595 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
595
596
596 def _revision_to_commit(self, revision):
597 def _revision_to_commit(self, revision):
597 """
598 """
598 Translates a revision to a commit_id
599 Translates a revision to a commit_id
599
600
600 Helps to support the old changeset based API which allows to use
601 Helps to support the old changeset based API which allows to use
601 commit ids and commit indices interchangeable.
602 commit ids and commit indices interchangeable.
602 """
603 """
603 if revision is None:
604 if revision is None:
604 return revision
605 return revision
605
606
606 if isinstance(revision, basestring):
607 if isinstance(revision, basestring):
607 commit_id = revision
608 commit_id = revision
608 else:
609 else:
609 commit_id = self.commit_ids[revision]
610 commit_id = self.commit_ids[revision]
610 return commit_id
611 return commit_id
611
612
612 @property
613 @property
613 def in_memory_changeset(self):
614 def in_memory_changeset(self):
614 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
615 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
615 return self.in_memory_commit
616 return self.in_memory_commit
616
617
617
618
618 class BaseCommit(object):
619 class BaseCommit(object):
619 """
620 """
620 Each backend should implement it's commit representation.
621 Each backend should implement it's commit representation.
621
622
622 **Attributes**
623 **Attributes**
623
624
624 ``repository``
625 ``repository``
625 repository object within which commit exists
626 repository object within which commit exists
626
627
627 ``id``
628 ``id``
628 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
629 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
629 just ``tip``.
630 just ``tip``.
630
631
631 ``raw_id``
632 ``raw_id``
632 raw commit representation (i.e. full 40 length sha for git
633 raw commit representation (i.e. full 40 length sha for git
633 backend)
634 backend)
634
635
635 ``short_id``
636 ``short_id``
636 shortened (if apply) version of ``raw_id``; it would be simple
637 shortened (if apply) version of ``raw_id``; it would be simple
637 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
638 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
638 as ``raw_id`` for subversion
639 as ``raw_id`` for subversion
639
640
640 ``idx``
641 ``idx``
641 commit index
642 commit index
642
643
643 ``files``
644 ``files``
644 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
645 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
645
646
646 ``dirs``
647 ``dirs``
647 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
648 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
648
649
649 ``nodes``
650 ``nodes``
650 combined list of ``Node`` objects
651 combined list of ``Node`` objects
651
652
652 ``author``
653 ``author``
653 author of the commit, as unicode
654 author of the commit, as unicode
654
655
655 ``message``
656 ``message``
656 message of the commit, as unicode
657 message of the commit, as unicode
657
658
658 ``parents``
659 ``parents``
659 list of parent commits
660 list of parent commits
660
661
661 """
662 """
662
663
663 branch = None
664 branch = None
664 """
665 """
665 Depending on the backend this should be set to the branch name of the
666 Depending on the backend this should be set to the branch name of the
666 commit. Backends not supporting branches on commits should leave this
667 commit. Backends not supporting branches on commits should leave this
667 value as ``None``.
668 value as ``None``.
668 """
669 """
669
670
670 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
671 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
671 """
672 """
672 This template is used to generate a default prefix for repository archives
673 This template is used to generate a default prefix for repository archives
673 if no prefix has been specified.
674 if no prefix has been specified.
674 """
675 """
675
676
676 def __str__(self):
677 def __str__(self):
677 return '<%s at %s:%s>' % (
678 return '<%s at %s:%s>' % (
678 self.__class__.__name__, self.idx, self.short_id)
679 self.__class__.__name__, self.idx, self.short_id)
679
680
680 def __repr__(self):
681 def __repr__(self):
681 return self.__str__()
682 return self.__str__()
682
683
683 def __unicode__(self):
684 def __unicode__(self):
684 return u'%s:%s' % (self.idx, self.short_id)
685 return u'%s:%s' % (self.idx, self.short_id)
685
686
686 def __eq__(self, other):
687 def __eq__(self, other):
687 same_instance = isinstance(other, self.__class__)
688 same_instance = isinstance(other, self.__class__)
688 return same_instance and self.raw_id == other.raw_id
689 return same_instance and self.raw_id == other.raw_id
689
690
690 def __json__(self):
691 def __json__(self):
691 parents = []
692 parents = []
692 try:
693 try:
693 for parent in self.parents:
694 for parent in self.parents:
694 parents.append({'raw_id': parent.raw_id})
695 parents.append({'raw_id': parent.raw_id})
695 except NotImplementedError:
696 except NotImplementedError:
696 # empty commit doesn't have parents implemented
697 # empty commit doesn't have parents implemented
697 pass
698 pass
698
699
699 return {
700 return {
700 'short_id': self.short_id,
701 'short_id': self.short_id,
701 'raw_id': self.raw_id,
702 'raw_id': self.raw_id,
702 'revision': self.idx,
703 'revision': self.idx,
703 'message': self.message,
704 'message': self.message,
704 'date': self.date,
705 'date': self.date,
705 'author': self.author,
706 'author': self.author,
706 'parents': parents,
707 'parents': parents,
707 'branch': self.branch
708 'branch': self.branch
708 }
709 }
709
710
710 @LazyProperty
711 @LazyProperty
711 def last(self):
712 def last(self):
712 """
713 """
713 ``True`` if this is last commit in repository, ``False``
714 ``True`` if this is last commit in repository, ``False``
714 otherwise; trying to access this attribute while there is no
715 otherwise; trying to access this attribute while there is no
715 commits would raise `EmptyRepositoryError`
716 commits would raise `EmptyRepositoryError`
716 """
717 """
717 if self.repository is None:
718 if self.repository is None:
718 raise CommitError("Cannot check if it's most recent commit")
719 raise CommitError("Cannot check if it's most recent commit")
719 return self.raw_id == self.repository.commit_ids[-1]
720 return self.raw_id == self.repository.commit_ids[-1]
720
721
721 @LazyProperty
722 @LazyProperty
722 def parents(self):
723 def parents(self):
723 """
724 """
724 Returns list of parent commits.
725 Returns list of parent commits.
725 """
726 """
726 raise NotImplementedError
727 raise NotImplementedError
727
728
728 @property
729 @property
729 def merge(self):
730 def merge(self):
730 """
731 """
731 Returns boolean if commit is a merge.
732 Returns boolean if commit is a merge.
732 """
733 """
733 return len(self.parents) > 1
734 return len(self.parents) > 1
734
735
735 @LazyProperty
736 @LazyProperty
736 def children(self):
737 def children(self):
737 """
738 """
738 Returns list of child commits.
739 Returns list of child commits.
739 """
740 """
740 raise NotImplementedError
741 raise NotImplementedError
741
742
742 @LazyProperty
743 @LazyProperty
743 def id(self):
744 def id(self):
744 """
745 """
745 Returns string identifying this commit.
746 Returns string identifying this commit.
746 """
747 """
747 raise NotImplementedError
748 raise NotImplementedError
748
749
749 @LazyProperty
750 @LazyProperty
750 def raw_id(self):
751 def raw_id(self):
751 """
752 """
752 Returns raw string identifying this commit.
753 Returns raw string identifying this commit.
753 """
754 """
754 raise NotImplementedError
755 raise NotImplementedError
755
756
756 @LazyProperty
757 @LazyProperty
757 def short_id(self):
758 def short_id(self):
758 """
759 """
759 Returns shortened version of ``raw_id`` attribute, as string,
760 Returns shortened version of ``raw_id`` attribute, as string,
760 identifying this commit, useful for presentation to users.
761 identifying this commit, useful for presentation to users.
761 """
762 """
762 raise NotImplementedError
763 raise NotImplementedError
763
764
764 @LazyProperty
765 @LazyProperty
765 def idx(self):
766 def idx(self):
766 """
767 """
767 Returns integer identifying this commit.
768 Returns integer identifying this commit.
768 """
769 """
769 raise NotImplementedError
770 raise NotImplementedError
770
771
771 @LazyProperty
772 @LazyProperty
772 def committer(self):
773 def committer(self):
773 """
774 """
774 Returns committer for this commit
775 Returns committer for this commit
775 """
776 """
776 raise NotImplementedError
777 raise NotImplementedError
777
778
778 @LazyProperty
779 @LazyProperty
779 def committer_name(self):
780 def committer_name(self):
780 """
781 """
781 Returns committer name for this commit
782 Returns committer name for this commit
782 """
783 """
783
784
784 return author_name(self.committer)
785 return author_name(self.committer)
785
786
786 @LazyProperty
787 @LazyProperty
787 def committer_email(self):
788 def committer_email(self):
788 """
789 """
789 Returns committer email address for this commit
790 Returns committer email address for this commit
790 """
791 """
791
792
792 return author_email(self.committer)
793 return author_email(self.committer)
793
794
794 @LazyProperty
795 @LazyProperty
795 def author(self):
796 def author(self):
796 """
797 """
797 Returns author for this commit
798 Returns author for this commit
798 """
799 """
799
800
800 raise NotImplementedError
801 raise NotImplementedError
801
802
802 @LazyProperty
803 @LazyProperty
803 def author_name(self):
804 def author_name(self):
804 """
805 """
805 Returns author name for this commit
806 Returns author name for this commit
806 """
807 """
807
808
808 return author_name(self.author)
809 return author_name(self.author)
809
810
810 @LazyProperty
811 @LazyProperty
811 def author_email(self):
812 def author_email(self):
812 """
813 """
813 Returns author email address for this commit
814 Returns author email address for this commit
814 """
815 """
815
816
816 return author_email(self.author)
817 return author_email(self.author)
817
818
818 def get_file_mode(self, path):
819 def get_file_mode(self, path):
819 """
820 """
820 Returns stat mode of the file at `path`.
821 Returns stat mode of the file at `path`.
821 """
822 """
822 raise NotImplementedError
823 raise NotImplementedError
823
824
824 def is_link(self, path):
825 def is_link(self, path):
825 """
826 """
826 Returns ``True`` if given `path` is a symlink
827 Returns ``True`` if given `path` is a symlink
827 """
828 """
828 raise NotImplementedError
829 raise NotImplementedError
829
830
830 def get_file_content(self, path):
831 def get_file_content(self, path):
831 """
832 """
832 Returns content of the file at the given `path`.
833 Returns content of the file at the given `path`.
833 """
834 """
834 raise NotImplementedError
835 raise NotImplementedError
835
836
836 def get_file_size(self, path):
837 def get_file_size(self, path):
837 """
838 """
838 Returns size of the file at the given `path`.
839 Returns size of the file at the given `path`.
839 """
840 """
840 raise NotImplementedError
841 raise NotImplementedError
841
842
842 def get_file_commit(self, path, pre_load=None):
843 def get_file_commit(self, path, pre_load=None):
843 """
844 """
844 Returns last commit of the file at the given `path`.
845 Returns last commit of the file at the given `path`.
845
846
846 :param pre_load: Optional. List of commit attributes to load.
847 :param pre_load: Optional. List of commit attributes to load.
847 """
848 """
848 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
849 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
849 if not commits:
850 if not commits:
850 raise RepositoryError(
851 raise RepositoryError(
851 'Failed to fetch history for path {}. '
852 'Failed to fetch history for path {}. '
852 'Please check if such path exists in your repository'.format(
853 'Please check if such path exists in your repository'.format(
853 path))
854 path))
854 return commits[0]
855 return commits[0]
855
856
856 def get_file_history(self, path, limit=None, pre_load=None):
857 def get_file_history(self, path, limit=None, pre_load=None):
857 """
858 """
858 Returns history of file as reversed list of :class:`BaseCommit`
859 Returns history of file as reversed list of :class:`BaseCommit`
859 objects for which file at given `path` has been modified.
860 objects for which file at given `path` has been modified.
860
861
861 :param limit: Optional. Allows to limit the size of the returned
862 :param limit: Optional. Allows to limit the size of the returned
862 history. This is intended as a hint to the underlying backend, so
863 history. This is intended as a hint to the underlying backend, so
863 that it can apply optimizations depending on the limit.
864 that it can apply optimizations depending on the limit.
864 :param pre_load: Optional. List of commit attributes to load.
865 :param pre_load: Optional. List of commit attributes to load.
865 """
866 """
866 raise NotImplementedError
867 raise NotImplementedError
867
868
868 def get_file_annotate(self, path, pre_load=None):
869 def get_file_annotate(self, path, pre_load=None):
869 """
870 """
870 Returns a generator of four element tuples with
871 Returns a generator of four element tuples with
871 lineno, sha, commit lazy loader and line
872 lineno, sha, commit lazy loader and line
872
873
873 :param pre_load: Optional. List of commit attributes to load.
874 :param pre_load: Optional. List of commit attributes to load.
874 """
875 """
875 raise NotImplementedError
876 raise NotImplementedError
876
877
877 def get_nodes(self, path):
878 def get_nodes(self, path):
878 """
879 """
879 Returns combined ``DirNode`` and ``FileNode`` objects list representing
880 Returns combined ``DirNode`` and ``FileNode`` objects list representing
880 state of commit at the given ``path``.
881 state of commit at the given ``path``.
881
882
882 :raises ``CommitError``: if node at the given ``path`` is not
883 :raises ``CommitError``: if node at the given ``path`` is not
883 instance of ``DirNode``
884 instance of ``DirNode``
884 """
885 """
885 raise NotImplementedError
886 raise NotImplementedError
886
887
887 def get_node(self, path):
888 def get_node(self, path):
888 """
889 """
889 Returns ``Node`` object from the given ``path``.
890 Returns ``Node`` object from the given ``path``.
890
891
891 :raises ``NodeDoesNotExistError``: if there is no node at the given
892 :raises ``NodeDoesNotExistError``: if there is no node at the given
892 ``path``
893 ``path``
893 """
894 """
894 raise NotImplementedError
895 raise NotImplementedError
895
896
896 def get_largefile_node(self, path):
897 def get_largefile_node(self, path):
897 """
898 """
898 Returns the path to largefile from Mercurial/Git-lfs storage.
899 Returns the path to largefile from Mercurial/Git-lfs storage.
899 or None if it's not a largefile node
900 or None if it's not a largefile node
900 """
901 """
901 return None
902 return None
902
903
903 def archive_repo(self, file_path, kind='tgz', subrepos=None,
904 def archive_repo(self, file_path, kind='tgz', subrepos=None,
904 prefix=None, write_metadata=False, mtime=None):
905 prefix=None, write_metadata=False, mtime=None):
905 """
906 """
906 Creates an archive containing the contents of the repository.
907 Creates an archive containing the contents of the repository.
907
908
908 :param file_path: path to the file which to create the archive.
909 :param file_path: path to the file which to create the archive.
909 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
910 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
910 :param prefix: name of root directory in archive.
911 :param prefix: name of root directory in archive.
911 Default is repository name and commit's short_id joined with dash:
912 Default is repository name and commit's short_id joined with dash:
912 ``"{repo_name}-{short_id}"``.
913 ``"{repo_name}-{short_id}"``.
913 :param write_metadata: write a metadata file into archive.
914 :param write_metadata: write a metadata file into archive.
914 :param mtime: custom modification time for archive creation, defaults
915 :param mtime: custom modification time for archive creation, defaults
915 to time.time() if not given.
916 to time.time() if not given.
916
917
917 :raise VCSError: If prefix has a problem.
918 :raise VCSError: If prefix has a problem.
918 """
919 """
919 allowed_kinds = settings.ARCHIVE_SPECS.keys()
920 allowed_kinds = settings.ARCHIVE_SPECS.keys()
920 if kind not in allowed_kinds:
921 if kind not in allowed_kinds:
921 raise ImproperArchiveTypeError(
922 raise ImproperArchiveTypeError(
922 'Archive kind (%s) not supported use one of %s' %
923 'Archive kind (%s) not supported use one of %s' %
923 (kind, allowed_kinds))
924 (kind, allowed_kinds))
924
925
925 prefix = self._validate_archive_prefix(prefix)
926 prefix = self._validate_archive_prefix(prefix)
926
927
927 mtime = mtime or time.mktime(self.date.timetuple())
928 mtime = mtime or time.mktime(self.date.timetuple())
928
929
929 file_info = []
930 file_info = []
930 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
931 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
931 for _r, _d, files in cur_rev.walk('/'):
932 for _r, _d, files in cur_rev.walk('/'):
932 for f in files:
933 for f in files:
933 f_path = os.path.join(prefix, f.path)
934 f_path = os.path.join(prefix, f.path)
934 file_info.append(
935 file_info.append(
935 (f_path, f.mode, f.is_link(), f.raw_bytes))
936 (f_path, f.mode, f.is_link(), f.raw_bytes))
936
937
937 if write_metadata:
938 if write_metadata:
938 metadata = [
939 metadata = [
939 ('repo_name', self.repository.name),
940 ('repo_name', self.repository.name),
940 ('rev', self.raw_id),
941 ('rev', self.raw_id),
941 ('create_time', mtime),
942 ('create_time', mtime),
942 ('branch', self.branch),
943 ('branch', self.branch),
943 ('tags', ','.join(self.tags)),
944 ('tags', ','.join(self.tags)),
944 ]
945 ]
945 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
946 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
946 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
947 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
947
948
948 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
949 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
949
950
950 def _validate_archive_prefix(self, prefix):
951 def _validate_archive_prefix(self, prefix):
951 if prefix is None:
952 if prefix is None:
952 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
953 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
953 repo_name=safe_str(self.repository.name),
954 repo_name=safe_str(self.repository.name),
954 short_id=self.short_id)
955 short_id=self.short_id)
955 elif not isinstance(prefix, str):
956 elif not isinstance(prefix, str):
956 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
957 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
957 elif prefix.startswith('/'):
958 elif prefix.startswith('/'):
958 raise VCSError("Prefix cannot start with leading slash")
959 raise VCSError("Prefix cannot start with leading slash")
959 elif prefix.strip() == '':
960 elif prefix.strip() == '':
960 raise VCSError("Prefix cannot be empty")
961 raise VCSError("Prefix cannot be empty")
961 return prefix
962 return prefix
962
963
963 @LazyProperty
964 @LazyProperty
964 def root(self):
965 def root(self):
965 """
966 """
966 Returns ``RootNode`` object for this commit.
967 Returns ``RootNode`` object for this commit.
967 """
968 """
968 return self.get_node('')
969 return self.get_node('')
969
970
970 def next(self, branch=None):
971 def next(self, branch=None):
971 """
972 """
972 Returns next commit from current, if branch is gives it will return
973 Returns next commit from current, if branch is gives it will return
973 next commit belonging to this branch
974 next commit belonging to this branch
974
975
975 :param branch: show commits within the given named branch
976 :param branch: show commits within the given named branch
976 """
977 """
977 indexes = xrange(self.idx + 1, self.repository.count())
978 indexes = xrange(self.idx + 1, self.repository.count())
978 return self._find_next(indexes, branch)
979 return self._find_next(indexes, branch)
979
980
980 def prev(self, branch=None):
981 def prev(self, branch=None):
981 """
982 """
982 Returns previous commit from current, if branch is gives it will
983 Returns previous commit from current, if branch is gives it will
983 return previous commit belonging to this branch
984 return previous commit belonging to this branch
984
985
985 :param branch: show commit within the given named branch
986 :param branch: show commit within the given named branch
986 """
987 """
987 indexes = xrange(self.idx - 1, -1, -1)
988 indexes = xrange(self.idx - 1, -1, -1)
988 return self._find_next(indexes, branch)
989 return self._find_next(indexes, branch)
989
990
990 def _find_next(self, indexes, branch=None):
991 def _find_next(self, indexes, branch=None):
991 if branch and self.branch != branch:
992 if branch and self.branch != branch:
992 raise VCSError('Branch option used on commit not belonging '
993 raise VCSError('Branch option used on commit not belonging '
993 'to that branch')
994 'to that branch')
994
995
995 for next_idx in indexes:
996 for next_idx in indexes:
996 commit = self.repository.get_commit(commit_idx=next_idx)
997 commit = self.repository.get_commit(commit_idx=next_idx)
997 if branch and branch != commit.branch:
998 if branch and branch != commit.branch:
998 continue
999 continue
999 return commit
1000 return commit
1000 raise CommitDoesNotExistError
1001 raise CommitDoesNotExistError
1001
1002
1002 def diff(self, ignore_whitespace=True, context=3):
1003 def diff(self, ignore_whitespace=True, context=3):
1003 """
1004 """
1004 Returns a `Diff` object representing the change made by this commit.
1005 Returns a `Diff` object representing the change made by this commit.
1005 """
1006 """
1006 parent = (
1007 parent = (
1007 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
1008 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
1008 diff = self.repository.get_diff(
1009 diff = self.repository.get_diff(
1009 parent, self,
1010 parent, self,
1010 ignore_whitespace=ignore_whitespace,
1011 ignore_whitespace=ignore_whitespace,
1011 context=context)
1012 context=context)
1012 return diff
1013 return diff
1013
1014
1014 @LazyProperty
1015 @LazyProperty
1015 def added(self):
1016 def added(self):
1016 """
1017 """
1017 Returns list of added ``FileNode`` objects.
1018 Returns list of added ``FileNode`` objects.
1018 """
1019 """
1019 raise NotImplementedError
1020 raise NotImplementedError
1020
1021
1021 @LazyProperty
1022 @LazyProperty
1022 def changed(self):
1023 def changed(self):
1023 """
1024 """
1024 Returns list of modified ``FileNode`` objects.
1025 Returns list of modified ``FileNode`` objects.
1025 """
1026 """
1026 raise NotImplementedError
1027 raise NotImplementedError
1027
1028
1028 @LazyProperty
1029 @LazyProperty
1029 def removed(self):
1030 def removed(self):
1030 """
1031 """
1031 Returns list of removed ``FileNode`` objects.
1032 Returns list of removed ``FileNode`` objects.
1032 """
1033 """
1033 raise NotImplementedError
1034 raise NotImplementedError
1034
1035
1035 @LazyProperty
1036 @LazyProperty
1036 def size(self):
1037 def size(self):
1037 """
1038 """
1038 Returns total number of bytes from contents of all filenodes.
1039 Returns total number of bytes from contents of all filenodes.
1039 """
1040 """
1040 return sum((node.size for node in self.get_filenodes_generator()))
1041 return sum((node.size for node in self.get_filenodes_generator()))
1041
1042
1042 def walk(self, topurl=''):
1043 def walk(self, topurl=''):
1043 """
1044 """
1044 Similar to os.walk method. Insted of filesystem it walks through
1045 Similar to os.walk method. Insted of filesystem it walks through
1045 commit starting at given ``topurl``. Returns generator of tuples
1046 commit starting at given ``topurl``. Returns generator of tuples
1046 (topnode, dirnodes, filenodes).
1047 (topnode, dirnodes, filenodes).
1047 """
1048 """
1048 topnode = self.get_node(topurl)
1049 topnode = self.get_node(topurl)
1049 if not topnode.is_dir():
1050 if not topnode.is_dir():
1050 return
1051 return
1051 yield (topnode, topnode.dirs, topnode.files)
1052 yield (topnode, topnode.dirs, topnode.files)
1052 for dirnode in topnode.dirs:
1053 for dirnode in topnode.dirs:
1053 for tup in self.walk(dirnode.path):
1054 for tup in self.walk(dirnode.path):
1054 yield tup
1055 yield tup
1055
1056
1056 def get_filenodes_generator(self):
1057 def get_filenodes_generator(self):
1057 """
1058 """
1058 Returns generator that yields *all* file nodes.
1059 Returns generator that yields *all* file nodes.
1059 """
1060 """
1060 for topnode, dirs, files in self.walk():
1061 for topnode, dirs, files in self.walk():
1061 for node in files:
1062 for node in files:
1062 yield node
1063 yield node
1063
1064
1064 #
1065 #
1065 # Utilities for sub classes to support consistent behavior
1066 # Utilities for sub classes to support consistent behavior
1066 #
1067 #
1067
1068
1068 def no_node_at_path(self, path):
1069 def no_node_at_path(self, path):
1069 return NodeDoesNotExistError(
1070 return NodeDoesNotExistError(
1070 u"There is no file nor directory at the given path: "
1071 u"There is no file nor directory at the given path: "
1071 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1072 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1072
1073
1073 def _fix_path(self, path):
1074 def _fix_path(self, path):
1074 """
1075 """
1075 Paths are stored without trailing slash so we need to get rid off it if
1076 Paths are stored without trailing slash so we need to get rid off it if
1076 needed.
1077 needed.
1077 """
1078 """
1078 return path.rstrip('/')
1079 return path.rstrip('/')
1079
1080
1080 #
1081 #
1081 # Deprecated API based on changesets
1082 # Deprecated API based on changesets
1082 #
1083 #
1083
1084
1084 @property
1085 @property
1085 def revision(self):
1086 def revision(self):
1086 warnings.warn("Use idx instead", DeprecationWarning)
1087 warnings.warn("Use idx instead", DeprecationWarning)
1087 return self.idx
1088 return self.idx
1088
1089
1089 @revision.setter
1090 @revision.setter
1090 def revision(self, value):
1091 def revision(self, value):
1091 warnings.warn("Use idx instead", DeprecationWarning)
1092 warnings.warn("Use idx instead", DeprecationWarning)
1092 self.idx = value
1093 self.idx = value
1093
1094
1094 def get_file_changeset(self, path):
1095 def get_file_changeset(self, path):
1095 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1096 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1096 return self.get_file_commit(path)
1097 return self.get_file_commit(path)
1097
1098
1098
1099
1099 class BaseChangesetClass(type):
1100 class BaseChangesetClass(type):
1100
1101
1101 def __instancecheck__(self, instance):
1102 def __instancecheck__(self, instance):
1102 return isinstance(instance, BaseCommit)
1103 return isinstance(instance, BaseCommit)
1103
1104
1104
1105
1105 class BaseChangeset(BaseCommit):
1106 class BaseChangeset(BaseCommit):
1106
1107
1107 __metaclass__ = BaseChangesetClass
1108 __metaclass__ = BaseChangesetClass
1108
1109
1109 def __new__(cls, *args, **kwargs):
1110 def __new__(cls, *args, **kwargs):
1110 warnings.warn(
1111 warnings.warn(
1111 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1112 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1112 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1113 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1113
1114
1114
1115
1115 class BaseInMemoryCommit(object):
1116 class BaseInMemoryCommit(object):
1116 """
1117 """
1117 Represents differences between repository's state (most recent head) and
1118 Represents differences between repository's state (most recent head) and
1118 changes made *in place*.
1119 changes made *in place*.
1119
1120
1120 **Attributes**
1121 **Attributes**
1121
1122
1122 ``repository``
1123 ``repository``
1123 repository object for this in-memory-commit
1124 repository object for this in-memory-commit
1124
1125
1125 ``added``
1126 ``added``
1126 list of ``FileNode`` objects marked as *added*
1127 list of ``FileNode`` objects marked as *added*
1127
1128
1128 ``changed``
1129 ``changed``
1129 list of ``FileNode`` objects marked as *changed*
1130 list of ``FileNode`` objects marked as *changed*
1130
1131
1131 ``removed``
1132 ``removed``
1132 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1133 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1133 *removed*
1134 *removed*
1134
1135
1135 ``parents``
1136 ``parents``
1136 list of :class:`BaseCommit` instances representing parents of
1137 list of :class:`BaseCommit` instances representing parents of
1137 in-memory commit. Should always be 2-element sequence.
1138 in-memory commit. Should always be 2-element sequence.
1138
1139
1139 """
1140 """
1140
1141
1141 def __init__(self, repository):
1142 def __init__(self, repository):
1142 self.repository = repository
1143 self.repository = repository
1143 self.added = []
1144 self.added = []
1144 self.changed = []
1145 self.changed = []
1145 self.removed = []
1146 self.removed = []
1146 self.parents = []
1147 self.parents = []
1147
1148
1148 def add(self, *filenodes):
1149 def add(self, *filenodes):
1149 """
1150 """
1150 Marks given ``FileNode`` objects as *to be committed*.
1151 Marks given ``FileNode`` objects as *to be committed*.
1151
1152
1152 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1153 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1153 latest commit
1154 latest commit
1154 :raises ``NodeAlreadyAddedError``: if node with same path is already
1155 :raises ``NodeAlreadyAddedError``: if node with same path is already
1155 marked as *added*
1156 marked as *added*
1156 """
1157 """
1157 # Check if not already marked as *added* first
1158 # Check if not already marked as *added* first
1158 for node in filenodes:
1159 for node in filenodes:
1159 if node.path in (n.path for n in self.added):
1160 if node.path in (n.path for n in self.added):
1160 raise NodeAlreadyAddedError(
1161 raise NodeAlreadyAddedError(
1161 "Such FileNode %s is already marked for addition"
1162 "Such FileNode %s is already marked for addition"
1162 % node.path)
1163 % node.path)
1163 for node in filenodes:
1164 for node in filenodes:
1164 self.added.append(node)
1165 self.added.append(node)
1165
1166
1166 def change(self, *filenodes):
1167 def change(self, *filenodes):
1167 """
1168 """
1168 Marks given ``FileNode`` objects to be *changed* in next commit.
1169 Marks given ``FileNode`` objects to be *changed* in next commit.
1169
1170
1170 :raises ``EmptyRepositoryError``: if there are no commits yet
1171 :raises ``EmptyRepositoryError``: if there are no commits yet
1171 :raises ``NodeAlreadyExistsError``: if node with same path is already
1172 :raises ``NodeAlreadyExistsError``: if node with same path is already
1172 marked to be *changed*
1173 marked to be *changed*
1173 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1174 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1174 marked to be *removed*
1175 marked to be *removed*
1175 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1176 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1176 commit
1177 commit
1177 :raises ``NodeNotChangedError``: if node hasn't really be changed
1178 :raises ``NodeNotChangedError``: if node hasn't really be changed
1178 """
1179 """
1179 for node in filenodes:
1180 for node in filenodes:
1180 if node.path in (n.path for n in self.removed):
1181 if node.path in (n.path for n in self.removed):
1181 raise NodeAlreadyRemovedError(
1182 raise NodeAlreadyRemovedError(
1182 "Node at %s is already marked as removed" % node.path)
1183 "Node at %s is already marked as removed" % node.path)
1183 try:
1184 try:
1184 self.repository.get_commit()
1185 self.repository.get_commit()
1185 except EmptyRepositoryError:
1186 except EmptyRepositoryError:
1186 raise EmptyRepositoryError(
1187 raise EmptyRepositoryError(
1187 "Nothing to change - try to *add* new nodes rather than "
1188 "Nothing to change - try to *add* new nodes rather than "
1188 "changing them")
1189 "changing them")
1189 for node in filenodes:
1190 for node in filenodes:
1190 if node.path in (n.path for n in self.changed):
1191 if node.path in (n.path for n in self.changed):
1191 raise NodeAlreadyChangedError(
1192 raise NodeAlreadyChangedError(
1192 "Node at '%s' is already marked as changed" % node.path)
1193 "Node at '%s' is already marked as changed" % node.path)
1193 self.changed.append(node)
1194 self.changed.append(node)
1194
1195
1195 def remove(self, *filenodes):
1196 def remove(self, *filenodes):
1196 """
1197 """
1197 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1198 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1198 *removed* in next commit.
1199 *removed* in next commit.
1199
1200
1200 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1201 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1201 be *removed*
1202 be *removed*
1202 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1203 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1203 be *changed*
1204 be *changed*
1204 """
1205 """
1205 for node in filenodes:
1206 for node in filenodes:
1206 if node.path in (n.path for n in self.removed):
1207 if node.path in (n.path for n in self.removed):
1207 raise NodeAlreadyRemovedError(
1208 raise NodeAlreadyRemovedError(
1208 "Node is already marked to for removal at %s" % node.path)
1209 "Node is already marked to for removal at %s" % node.path)
1209 if node.path in (n.path for n in self.changed):
1210 if node.path in (n.path for n in self.changed):
1210 raise NodeAlreadyChangedError(
1211 raise NodeAlreadyChangedError(
1211 "Node is already marked to be changed at %s" % node.path)
1212 "Node is already marked to be changed at %s" % node.path)
1212 # We only mark node as *removed* - real removal is done by
1213 # We only mark node as *removed* - real removal is done by
1213 # commit method
1214 # commit method
1214 self.removed.append(node)
1215 self.removed.append(node)
1215
1216
1216 def reset(self):
1217 def reset(self):
1217 """
1218 """
1218 Resets this instance to initial state (cleans ``added``, ``changed``
1219 Resets this instance to initial state (cleans ``added``, ``changed``
1219 and ``removed`` lists).
1220 and ``removed`` lists).
1220 """
1221 """
1221 self.added = []
1222 self.added = []
1222 self.changed = []
1223 self.changed = []
1223 self.removed = []
1224 self.removed = []
1224 self.parents = []
1225 self.parents = []
1225
1226
1226 def get_ipaths(self):
1227 def get_ipaths(self):
1227 """
1228 """
1228 Returns generator of paths from nodes marked as added, changed or
1229 Returns generator of paths from nodes marked as added, changed or
1229 removed.
1230 removed.
1230 """
1231 """
1231 for node in itertools.chain(self.added, self.changed, self.removed):
1232 for node in itertools.chain(self.added, self.changed, self.removed):
1232 yield node.path
1233 yield node.path
1233
1234
1234 def get_paths(self):
1235 def get_paths(self):
1235 """
1236 """
1236 Returns list of paths from nodes marked as added, changed or removed.
1237 Returns list of paths from nodes marked as added, changed or removed.
1237 """
1238 """
1238 return list(self.get_ipaths())
1239 return list(self.get_ipaths())
1239
1240
1240 def check_integrity(self, parents=None):
1241 def check_integrity(self, parents=None):
1241 """
1242 """
1242 Checks in-memory commit's integrity. Also, sets parents if not
1243 Checks in-memory commit's integrity. Also, sets parents if not
1243 already set.
1244 already set.
1244
1245
1245 :raises CommitError: if any error occurs (i.e.
1246 :raises CommitError: if any error occurs (i.e.
1246 ``NodeDoesNotExistError``).
1247 ``NodeDoesNotExistError``).
1247 """
1248 """
1248 if not self.parents:
1249 if not self.parents:
1249 parents = parents or []
1250 parents = parents or []
1250 if len(parents) == 0:
1251 if len(parents) == 0:
1251 try:
1252 try:
1252 parents = [self.repository.get_commit(), None]
1253 parents = [self.repository.get_commit(), None]
1253 except EmptyRepositoryError:
1254 except EmptyRepositoryError:
1254 parents = [None, None]
1255 parents = [None, None]
1255 elif len(parents) == 1:
1256 elif len(parents) == 1:
1256 parents += [None]
1257 parents += [None]
1257 self.parents = parents
1258 self.parents = parents
1258
1259
1259 # Local parents, only if not None
1260 # Local parents, only if not None
1260 parents = [p for p in self.parents if p]
1261 parents = [p for p in self.parents if p]
1261
1262
1262 # Check nodes marked as added
1263 # Check nodes marked as added
1263 for p in parents:
1264 for p in parents:
1264 for node in self.added:
1265 for node in self.added:
1265 try:
1266 try:
1266 p.get_node(node.path)
1267 p.get_node(node.path)
1267 except NodeDoesNotExistError:
1268 except NodeDoesNotExistError:
1268 pass
1269 pass
1269 else:
1270 else:
1270 raise NodeAlreadyExistsError(
1271 raise NodeAlreadyExistsError(
1271 "Node `%s` already exists at %s" % (node.path, p))
1272 "Node `%s` already exists at %s" % (node.path, p))
1272
1273
1273 # Check nodes marked as changed
1274 # Check nodes marked as changed
1274 missing = set(self.changed)
1275 missing = set(self.changed)
1275 not_changed = set(self.changed)
1276 not_changed = set(self.changed)
1276 if self.changed and not parents:
1277 if self.changed and not parents:
1277 raise NodeDoesNotExistError(str(self.changed[0].path))
1278 raise NodeDoesNotExistError(str(self.changed[0].path))
1278 for p in parents:
1279 for p in parents:
1279 for node in self.changed:
1280 for node in self.changed:
1280 try:
1281 try:
1281 old = p.get_node(node.path)
1282 old = p.get_node(node.path)
1282 missing.remove(node)
1283 missing.remove(node)
1283 # if content actually changed, remove node from not_changed
1284 # if content actually changed, remove node from not_changed
1284 if old.content != node.content:
1285 if old.content != node.content:
1285 not_changed.remove(node)
1286 not_changed.remove(node)
1286 except NodeDoesNotExistError:
1287 except NodeDoesNotExistError:
1287 pass
1288 pass
1288 if self.changed and missing:
1289 if self.changed and missing:
1289 raise NodeDoesNotExistError(
1290 raise NodeDoesNotExistError(
1290 "Node `%s` marked as modified but missing in parents: %s"
1291 "Node `%s` marked as modified but missing in parents: %s"
1291 % (node.path, parents))
1292 % (node.path, parents))
1292
1293
1293 if self.changed and not_changed:
1294 if self.changed and not_changed:
1294 raise NodeNotChangedError(
1295 raise NodeNotChangedError(
1295 "Node `%s` wasn't actually changed (parents: %s)"
1296 "Node `%s` wasn't actually changed (parents: %s)"
1296 % (not_changed.pop().path, parents))
1297 % (not_changed.pop().path, parents))
1297
1298
1298 # Check nodes marked as removed
1299 # Check nodes marked as removed
1299 if self.removed and not parents:
1300 if self.removed and not parents:
1300 raise NodeDoesNotExistError(
1301 raise NodeDoesNotExistError(
1301 "Cannot remove node at %s as there "
1302 "Cannot remove node at %s as there "
1302 "were no parents specified" % self.removed[0].path)
1303 "were no parents specified" % self.removed[0].path)
1303 really_removed = set()
1304 really_removed = set()
1304 for p in parents:
1305 for p in parents:
1305 for node in self.removed:
1306 for node in self.removed:
1306 try:
1307 try:
1307 p.get_node(node.path)
1308 p.get_node(node.path)
1308 really_removed.add(node)
1309 really_removed.add(node)
1309 except CommitError:
1310 except CommitError:
1310 pass
1311 pass
1311 not_removed = set(self.removed) - really_removed
1312 not_removed = set(self.removed) - really_removed
1312 if not_removed:
1313 if not_removed:
1313 # TODO: johbo: This code branch does not seem to be covered
1314 # TODO: johbo: This code branch does not seem to be covered
1314 raise NodeDoesNotExistError(
1315 raise NodeDoesNotExistError(
1315 "Cannot remove node at %s from "
1316 "Cannot remove node at %s from "
1316 "following parents: %s" % (not_removed, parents))
1317 "following parents: %s" % (not_removed, parents))
1317
1318
1318 def commit(
1319 def commit(
1319 self, message, author, parents=None, branch=None, date=None,
1320 self, message, author, parents=None, branch=None, date=None,
1320 **kwargs):
1321 **kwargs):
1321 """
1322 """
1322 Performs in-memory commit (doesn't check workdir in any way) and
1323 Performs in-memory commit (doesn't check workdir in any way) and
1323 returns newly created :class:`BaseCommit`. Updates repository's
1324 returns newly created :class:`BaseCommit`. Updates repository's
1324 attribute `commits`.
1325 attribute `commits`.
1325
1326
1326 .. note::
1327 .. note::
1327
1328
1328 While overriding this method each backend's should call
1329 While overriding this method each backend's should call
1329 ``self.check_integrity(parents)`` in the first place.
1330 ``self.check_integrity(parents)`` in the first place.
1330
1331
1331 :param message: message of the commit
1332 :param message: message of the commit
1332 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1333 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1333 :param parents: single parent or sequence of parents from which commit
1334 :param parents: single parent or sequence of parents from which commit
1334 would be derived
1335 would be derived
1335 :param date: ``datetime.datetime`` instance. Defaults to
1336 :param date: ``datetime.datetime`` instance. Defaults to
1336 ``datetime.datetime.now()``.
1337 ``datetime.datetime.now()``.
1337 :param branch: branch name, as string. If none given, default backend's
1338 :param branch: branch name, as string. If none given, default backend's
1338 branch would be used.
1339 branch would be used.
1339
1340
1340 :raises ``CommitError``: if any error occurs while committing
1341 :raises ``CommitError``: if any error occurs while committing
1341 """
1342 """
1342 raise NotImplementedError
1343 raise NotImplementedError
1343
1344
1344
1345
1345 class BaseInMemoryChangesetClass(type):
1346 class BaseInMemoryChangesetClass(type):
1346
1347
1347 def __instancecheck__(self, instance):
1348 def __instancecheck__(self, instance):
1348 return isinstance(instance, BaseInMemoryCommit)
1349 return isinstance(instance, BaseInMemoryCommit)
1349
1350
1350
1351
1351 class BaseInMemoryChangeset(BaseInMemoryCommit):
1352 class BaseInMemoryChangeset(BaseInMemoryCommit):
1352
1353
1353 __metaclass__ = BaseInMemoryChangesetClass
1354 __metaclass__ = BaseInMemoryChangesetClass
1354
1355
1355 def __new__(cls, *args, **kwargs):
1356 def __new__(cls, *args, **kwargs):
1356 warnings.warn(
1357 warnings.warn(
1357 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1358 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1358 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1359 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1359
1360
1360
1361
1361 class EmptyCommit(BaseCommit):
1362 class EmptyCommit(BaseCommit):
1362 """
1363 """
1363 An dummy empty commit. It's possible to pass hash when creating
1364 An dummy empty commit. It's possible to pass hash when creating
1364 an EmptyCommit
1365 an EmptyCommit
1365 """
1366 """
1366
1367
1367 def __init__(
1368 def __init__(
1368 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1369 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1369 message='', author='', date=None):
1370 message='', author='', date=None):
1370 self._empty_commit_id = commit_id
1371 self._empty_commit_id = commit_id
1371 # TODO: johbo: Solve idx parameter, default value does not make
1372 # TODO: johbo: Solve idx parameter, default value does not make
1372 # too much sense
1373 # too much sense
1373 self.idx = idx
1374 self.idx = idx
1374 self.message = message
1375 self.message = message
1375 self.author = author
1376 self.author = author
1376 self.date = date or datetime.datetime.fromtimestamp(0)
1377 self.date = date or datetime.datetime.fromtimestamp(0)
1377 self.repository = repo
1378 self.repository = repo
1378 self.alias = alias
1379 self.alias = alias
1379
1380
1380 @LazyProperty
1381 @LazyProperty
1381 def raw_id(self):
1382 def raw_id(self):
1382 """
1383 """
1383 Returns raw string identifying this commit, useful for web
1384 Returns raw string identifying this commit, useful for web
1384 representation.
1385 representation.
1385 """
1386 """
1386
1387
1387 return self._empty_commit_id
1388 return self._empty_commit_id
1388
1389
1389 @LazyProperty
1390 @LazyProperty
1390 def branch(self):
1391 def branch(self):
1391 if self.alias:
1392 if self.alias:
1392 from rhodecode.lib.vcs.backends import get_backend
1393 from rhodecode.lib.vcs.backends import get_backend
1393 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1394 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1394
1395
1395 @LazyProperty
1396 @LazyProperty
1396 def short_id(self):
1397 def short_id(self):
1397 return self.raw_id[:12]
1398 return self.raw_id[:12]
1398
1399
1399 @LazyProperty
1400 @LazyProperty
1400 def id(self):
1401 def id(self):
1401 return self.raw_id
1402 return self.raw_id
1402
1403
1403 def get_file_commit(self, path):
1404 def get_file_commit(self, path):
1404 return self
1405 return self
1405
1406
1406 def get_file_content(self, path):
1407 def get_file_content(self, path):
1407 return u''
1408 return u''
1408
1409
1409 def get_file_size(self, path):
1410 def get_file_size(self, path):
1410 return 0
1411 return 0
1411
1412
1412
1413
1413 class EmptyChangesetClass(type):
1414 class EmptyChangesetClass(type):
1414
1415
1415 def __instancecheck__(self, instance):
1416 def __instancecheck__(self, instance):
1416 return isinstance(instance, EmptyCommit)
1417 return isinstance(instance, EmptyCommit)
1417
1418
1418
1419
1419 class EmptyChangeset(EmptyCommit):
1420 class EmptyChangeset(EmptyCommit):
1420
1421
1421 __metaclass__ = EmptyChangesetClass
1422 __metaclass__ = EmptyChangesetClass
1422
1423
1423 def __new__(cls, *args, **kwargs):
1424 def __new__(cls, *args, **kwargs):
1424 warnings.warn(
1425 warnings.warn(
1425 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1426 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1426 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1427 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1427
1428
1428 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1429 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1429 alias=None, revision=-1, message='', author='', date=None):
1430 alias=None, revision=-1, message='', author='', date=None):
1430 if requested_revision is not None:
1431 if requested_revision is not None:
1431 warnings.warn(
1432 warnings.warn(
1432 "Parameter requested_revision not supported anymore",
1433 "Parameter requested_revision not supported anymore",
1433 DeprecationWarning)
1434 DeprecationWarning)
1434 super(EmptyChangeset, self).__init__(
1435 super(EmptyChangeset, self).__init__(
1435 commit_id=cs, repo=repo, alias=alias, idx=revision,
1436 commit_id=cs, repo=repo, alias=alias, idx=revision,
1436 message=message, author=author, date=date)
1437 message=message, author=author, date=date)
1437
1438
1438 @property
1439 @property
1439 def revision(self):
1440 def revision(self):
1440 warnings.warn("Use idx instead", DeprecationWarning)
1441 warnings.warn("Use idx instead", DeprecationWarning)
1441 return self.idx
1442 return self.idx
1442
1443
1443 @revision.setter
1444 @revision.setter
1444 def revision(self, value):
1445 def revision(self, value):
1445 warnings.warn("Use idx instead", DeprecationWarning)
1446 warnings.warn("Use idx instead", DeprecationWarning)
1446 self.idx = value
1447 self.idx = value
1447
1448
1448
1449
1449 class EmptyRepository(BaseRepository):
1450 class EmptyRepository(BaseRepository):
1450 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1451 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1451 pass
1452 pass
1452
1453
1453 def get_diff(self, *args, **kwargs):
1454 def get_diff(self, *args, **kwargs):
1454 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1455 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1455 return GitDiff('')
1456 return GitDiff('')
1456
1457
1457
1458
1458 class CollectionGenerator(object):
1459 class CollectionGenerator(object):
1459
1460
1460 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1461 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1461 self.repo = repo
1462 self.repo = repo
1462 self.commit_ids = commit_ids
1463 self.commit_ids = commit_ids
1463 # TODO: (oliver) this isn't currently hooked up
1464 # TODO: (oliver) this isn't currently hooked up
1464 self.collection_size = None
1465 self.collection_size = None
1465 self.pre_load = pre_load
1466 self.pre_load = pre_load
1466
1467
1467 def __len__(self):
1468 def __len__(self):
1468 if self.collection_size is not None:
1469 if self.collection_size is not None:
1469 return self.collection_size
1470 return self.collection_size
1470 return self.commit_ids.__len__()
1471 return self.commit_ids.__len__()
1471
1472
1472 def __iter__(self):
1473 def __iter__(self):
1473 for commit_id in self.commit_ids:
1474 for commit_id in self.commit_ids:
1474 # TODO: johbo: Mercurial passes in commit indices or commit ids
1475 # TODO: johbo: Mercurial passes in commit indices or commit ids
1475 yield self._commit_factory(commit_id)
1476 yield self._commit_factory(commit_id)
1476
1477
1477 def _commit_factory(self, commit_id):
1478 def _commit_factory(self, commit_id):
1478 """
1479 """
1479 Allows backends to override the way commits are generated.
1480 Allows backends to override the way commits are generated.
1480 """
1481 """
1481 return self.repo.get_commit(commit_id=commit_id,
1482 return self.repo.get_commit(commit_id=commit_id,
1482 pre_load=self.pre_load)
1483 pre_load=self.pre_load)
1483
1484
1484 def __getslice__(self, i, j):
1485 def __getslice__(self, i, j):
1485 """
1486 """
1486 Returns an iterator of sliced repository
1487 Returns an iterator of sliced repository
1487 """
1488 """
1488 commit_ids = self.commit_ids[i:j]
1489 commit_ids = self.commit_ids[i:j]
1489 return self.__class__(
1490 return self.__class__(
1490 self.repo, commit_ids, pre_load=self.pre_load)
1491 self.repo, commit_ids, pre_load=self.pre_load)
1491
1492
1492 def __repr__(self):
1493 def __repr__(self):
1493 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1494 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1494
1495
1495
1496
1496 class Config(object):
1497 class Config(object):
1497 """
1498 """
1498 Represents the configuration for a repository.
1499 Represents the configuration for a repository.
1499
1500
1500 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1501 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1501 standard library. It implements only the needed subset.
1502 standard library. It implements only the needed subset.
1502 """
1503 """
1503
1504
1504 def __init__(self):
1505 def __init__(self):
1505 self._values = {}
1506 self._values = {}
1506
1507
1507 def copy(self):
1508 def copy(self):
1508 clone = Config()
1509 clone = Config()
1509 for section, values in self._values.items():
1510 for section, values in self._values.items():
1510 clone._values[section] = values.copy()
1511 clone._values[section] = values.copy()
1511 return clone
1512 return clone
1512
1513
1513 def __repr__(self):
1514 def __repr__(self):
1514 return '<Config(%s sections) at %s>' % (
1515 return '<Config(%s sections) at %s>' % (
1515 len(self._values), hex(id(self)))
1516 len(self._values), hex(id(self)))
1516
1517
1517 def items(self, section):
1518 def items(self, section):
1518 return self._values.get(section, {}).iteritems()
1519 return self._values.get(section, {}).iteritems()
1519
1520
1520 def get(self, section, option):
1521 def get(self, section, option):
1521 return self._values.get(section, {}).get(option)
1522 return self._values.get(section, {}).get(option)
1522
1523
1523 def set(self, section, option, value):
1524 def set(self, section, option, value):
1524 section_values = self._values.setdefault(section, {})
1525 section_values = self._values.setdefault(section, {})
1525 section_values[option] = value
1526 section_values[option] = value
1526
1527
1527 def clear_section(self, section):
1528 def clear_section(self, section):
1528 self._values[section] = {}
1529 self._values[section] = {}
1529
1530
1530 def serialize(self):
1531 def serialize(self):
1531 """
1532 """
1532 Creates a list of three tuples (section, key, value) representing
1533 Creates a list of three tuples (section, key, value) representing
1533 this config object.
1534 this config object.
1534 """
1535 """
1535 items = []
1536 items = []
1536 for section in self._values:
1537 for section in self._values:
1537 for option, value in self._values[section].items():
1538 for option, value in self._values[section].items():
1538 items.append(
1539 items.append(
1539 (safe_str(section), safe_str(option), safe_str(value)))
1540 (safe_str(section), safe_str(option), safe_str(value)))
1540 return items
1541 return items
1541
1542
1542
1543
1543 class Diff(object):
1544 class Diff(object):
1544 """
1545 """
1545 Represents a diff result from a repository backend.
1546 Represents a diff result from a repository backend.
1546
1547
1547 Subclasses have to provide a backend specific value for
1548 Subclasses have to provide a backend specific value for
1548 :attr:`_header_re` and :attr:`_meta_re`.
1549 :attr:`_header_re` and :attr:`_meta_re`.
1549 """
1550 """
1550 _meta_re = None
1551 _meta_re = None
1551 _header_re = None
1552 _header_re = None
1552
1553
1553 def __init__(self, raw_diff):
1554 def __init__(self, raw_diff):
1554 self.raw = raw_diff
1555 self.raw = raw_diff
1555
1556
1556 def chunks(self):
1557 def chunks(self):
1557 """
1558 """
1558 split the diff in chunks of separate --git a/file b/file chunks
1559 split the diff in chunks of separate --git a/file b/file chunks
1559 to make diffs consistent we must prepend with \n, and make sure
1560 to make diffs consistent we must prepend with \n, and make sure
1560 we can detect last chunk as this was also has special rule
1561 we can detect last chunk as this was also has special rule
1561 """
1562 """
1562
1563
1563 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1564 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1564 header = diff_parts[0]
1565 header = diff_parts[0]
1565
1566
1566 if self._meta_re:
1567 if self._meta_re:
1567 match = self._meta_re.match(header)
1568 match = self._meta_re.match(header)
1568
1569
1569 chunks = diff_parts[1:]
1570 chunks = diff_parts[1:]
1570 total_chunks = len(chunks)
1571 total_chunks = len(chunks)
1571
1572
1572 return (
1573 return (
1573 DiffChunk(chunk, self, cur_chunk == total_chunks)
1574 DiffChunk(chunk, self, cur_chunk == total_chunks)
1574 for cur_chunk, chunk in enumerate(chunks, start=1))
1575 for cur_chunk, chunk in enumerate(chunks, start=1))
1575
1576
1576
1577
1577 class DiffChunk(object):
1578 class DiffChunk(object):
1578
1579
1579 def __init__(self, chunk, diff, last_chunk):
1580 def __init__(self, chunk, diff, last_chunk):
1580 self._diff = diff
1581 self._diff = diff
1581
1582
1582 # since we split by \ndiff --git that part is lost from original diff
1583 # since we split by \ndiff --git that part is lost from original diff
1583 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1584 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1584 if not last_chunk:
1585 if not last_chunk:
1585 chunk += '\n'
1586 chunk += '\n'
1586
1587
1587 match = self._diff._header_re.match(chunk)
1588 match = self._diff._header_re.match(chunk)
1588 self.header = match.groupdict()
1589 self.header = match.groupdict()
1589 self.diff = chunk[match.end():]
1590 self.diff = chunk[match.end():]
1590 self.raw = chunk
1591 self.raw = chunk
@@ -1,946 +1,947 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 GIT repository module
22 GIT repository module
23 """
23 """
24
24
25 import logging
25 import logging
26 import os
26 import os
27 import re
27 import re
28 import shutil
28 import shutil
29
29
30 from zope.cachedescriptors.property import Lazy as LazyProperty
30 from zope.cachedescriptors.property import Lazy as LazyProperty
31
31
32 from rhodecode.lib.compat import OrderedDict
32 from rhodecode.lib.compat import OrderedDict
33 from rhodecode.lib.datelib import (
33 from rhodecode.lib.datelib import (
34 utcdate_fromtimestamp, makedate, date_astimestamp)
34 utcdate_fromtimestamp, makedate, date_astimestamp)
35 from rhodecode.lib.utils import safe_unicode, safe_str
35 from rhodecode.lib.utils import safe_unicode, safe_str
36 from rhodecode.lib.vcs import connection, path as vcspath
36 from rhodecode.lib.vcs import connection, path as vcspath
37 from rhodecode.lib.vcs.backends.base import (
37 from rhodecode.lib.vcs.backends.base import (
38 BaseRepository, CollectionGenerator, Config, MergeResponse,
38 BaseRepository, CollectionGenerator, Config, MergeResponse,
39 MergeFailureReason, Reference)
39 MergeFailureReason, Reference)
40 from rhodecode.lib.vcs.backends.git.commit import GitCommit
40 from rhodecode.lib.vcs.backends.git.commit import GitCommit
41 from rhodecode.lib.vcs.backends.git.diff import GitDiff
41 from rhodecode.lib.vcs.backends.git.diff import GitDiff
42 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
42 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
43 from rhodecode.lib.vcs.exceptions import (
43 from rhodecode.lib.vcs.exceptions import (
44 CommitDoesNotExistError, EmptyRepositoryError,
44 CommitDoesNotExistError, EmptyRepositoryError,
45 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
45 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
46
46
47
47
48 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
48 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class GitRepository(BaseRepository):
53 class GitRepository(BaseRepository):
54 """
54 """
55 Git repository backend.
55 Git repository backend.
56 """
56 """
57 DEFAULT_BRANCH_NAME = 'master'
57 DEFAULT_BRANCH_NAME = 'master'
58
58
59 contact = BaseRepository.DEFAULT_CONTACT
59 contact = BaseRepository.DEFAULT_CONTACT
60
60
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 update_after_clone=False, with_wire=None, bare=False):
62 update_after_clone=False, with_wire=None, bare=False):
63
63
64 self.path = safe_str(os.path.abspath(repo_path))
64 self.path = safe_str(os.path.abspath(repo_path))
65 self.config = config if config else Config()
65 self.config = config if config else Config()
66 self._remote = connection.Git(
66 self._remote = connection.Git(
67 self.path, self.config, with_wire=with_wire)
67 self.path, self.config, with_wire=with_wire)
68
68
69 self._init_repo(create, src_url, update_after_clone, bare)
69 self._init_repo(create, src_url, update_after_clone, bare)
70
70
71 # caches
71 # caches
72 self._commit_ids = {}
72 self._commit_ids = {}
73
73
74 self.bookmarks = {}
74 self.bookmarks = {}
75
75
76 @LazyProperty
76 @LazyProperty
77 def bare(self):
77 def bare(self):
78 return self._remote.bare()
78 return self._remote.bare()
79
79
80 @LazyProperty
80 @LazyProperty
81 def head(self):
81 def head(self):
82 return self._remote.head()
82 return self._remote.head()
83
83
84 @LazyProperty
84 @LazyProperty
85 def commit_ids(self):
85 def commit_ids(self):
86 """
86 """
87 Returns list of commit ids, in ascending order. Being lazy
87 Returns list of commit ids, in ascending order. Being lazy
88 attribute allows external tools to inject commit ids from cache.
88 attribute allows external tools to inject commit ids from cache.
89 """
89 """
90 commit_ids = self._get_all_commit_ids()
90 commit_ids = self._get_all_commit_ids()
91 self._rebuild_cache(commit_ids)
91 self._rebuild_cache(commit_ids)
92 return commit_ids
92 return commit_ids
93
93
94 def _rebuild_cache(self, commit_ids):
94 def _rebuild_cache(self, commit_ids):
95 self._commit_ids = dict((commit_id, index)
95 self._commit_ids = dict((commit_id, index)
96 for index, commit_id in enumerate(commit_ids))
96 for index, commit_id in enumerate(commit_ids))
97
97
98 def run_git_command(self, cmd, **opts):
98 def run_git_command(self, cmd, **opts):
99 """
99 """
100 Runs given ``cmd`` as git command and returns tuple
100 Runs given ``cmd`` as git command and returns tuple
101 (stdout, stderr).
101 (stdout, stderr).
102
102
103 :param cmd: git command to be executed
103 :param cmd: git command to be executed
104 :param opts: env options to pass into Subprocess command
104 :param opts: env options to pass into Subprocess command
105 """
105 """
106 if not isinstance(cmd, list):
106 if not isinstance(cmd, list):
107 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
107 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
108
108
109 out, err = self._remote.run_git_command(cmd, **opts)
109 out, err = self._remote.run_git_command(cmd, **opts)
110 if err:
110 if err:
111 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
111 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
112 return out, err
112 return out, err
113
113
114 @staticmethod
114 @staticmethod
115 def check_url(url, config):
115 def check_url(url, config):
116 """
116 """
117 Function will check given url and try to verify if it's a valid
117 Function will check given url and try to verify if it's a valid
118 link. Sometimes it may happened that git will issue basic
118 link. Sometimes it may happened that git will issue basic
119 auth request that can cause whole API to hang when used from python
119 auth request that can cause whole API to hang when used from python
120 or other external calls.
120 or other external calls.
121
121
122 On failures it'll raise urllib2.HTTPError, exception is also thrown
122 On failures it'll raise urllib2.HTTPError, exception is also thrown
123 when the return code is non 200
123 when the return code is non 200
124 """
124 """
125 # check first if it's not an url
125 # check first if it's not an url
126 if os.path.isdir(url) or url.startswith('file:'):
126 if os.path.isdir(url) or url.startswith('file:'):
127 return True
127 return True
128
128
129 if '+' in url.split('://', 1)[0]:
129 if '+' in url.split('://', 1)[0]:
130 url = url.split('+', 1)[1]
130 url = url.split('+', 1)[1]
131
131
132 # Request the _remote to verify the url
132 # Request the _remote to verify the url
133 return connection.Git.check_url(url, config.serialize())
133 return connection.Git.check_url(url, config.serialize())
134
134
135 @staticmethod
135 @staticmethod
136 def is_valid_repository(path):
136 def is_valid_repository(path):
137 if os.path.isdir(os.path.join(path, '.git')):
137 if os.path.isdir(os.path.join(path, '.git')):
138 return True
138 return True
139 # check case of bare repository
139 # check case of bare repository
140 try:
140 try:
141 GitRepository(path)
141 GitRepository(path)
142 return True
142 return True
143 except VCSError:
143 except VCSError:
144 pass
144 pass
145 return False
145 return False
146
146
147 def _init_repo(self, create, src_url=None, update_after_clone=False,
147 def _init_repo(self, create, src_url=None, update_after_clone=False,
148 bare=False):
148 bare=False):
149 if create and os.path.exists(self.path):
149 if create and os.path.exists(self.path):
150 raise RepositoryError(
150 raise RepositoryError(
151 "Cannot create repository at %s, location already exist"
151 "Cannot create repository at %s, location already exist"
152 % self.path)
152 % self.path)
153
153
154 try:
154 try:
155 if create and src_url:
155 if create and src_url:
156 GitRepository.check_url(src_url, self.config)
156 GitRepository.check_url(src_url, self.config)
157 self.clone(src_url, update_after_clone, bare)
157 self.clone(src_url, update_after_clone, bare)
158 elif create:
158 elif create:
159 os.makedirs(self.path, mode=0755)
159 os.makedirs(self.path, mode=0755)
160
160
161 if bare:
161 if bare:
162 self._remote.init_bare()
162 self._remote.init_bare()
163 else:
163 else:
164 self._remote.init()
164 self._remote.init()
165 else:
165 else:
166 self._remote.assert_correct_path()
166 self._remote.assert_correct_path()
167 # TODO: johbo: check if we have to translate the OSError here
167 # TODO: johbo: check if we have to translate the OSError here
168 except OSError as err:
168 except OSError as err:
169 raise RepositoryError(err)
169 raise RepositoryError(err)
170
170
171 def _get_all_commit_ids(self, filters=None):
171 def _get_all_commit_ids(self, filters=None):
172 # we must check if this repo is not empty, since later command
172 # we must check if this repo is not empty, since later command
173 # fails if it is. And it's cheaper to ask than throw the subprocess
173 # fails if it is. And it's cheaper to ask than throw the subprocess
174 # errors
174 # errors
175 try:
175 try:
176 self._remote.head()
176 self._remote.head()
177 except KeyError:
177 except KeyError:
178 return []
178 return []
179
179
180 rev_filter = ['--branches', '--tags']
180 rev_filter = ['--branches', '--tags']
181 extra_filter = []
181 extra_filter = []
182
182
183 if filters:
183 if filters:
184 if filters.get('since'):
184 if filters.get('since'):
185 extra_filter.append('--since=%s' % (filters['since']))
185 extra_filter.append('--since=%s' % (filters['since']))
186 if filters.get('until'):
186 if filters.get('until'):
187 extra_filter.append('--until=%s' % (filters['until']))
187 extra_filter.append('--until=%s' % (filters['until']))
188 if filters.get('branch_name'):
188 if filters.get('branch_name'):
189 rev_filter = ['--tags']
189 rev_filter = ['--tags']
190 extra_filter.append(filters['branch_name'])
190 extra_filter.append(filters['branch_name'])
191 rev_filter.extend(extra_filter)
191 rev_filter.extend(extra_filter)
192
192
193 # if filters.get('start') or filters.get('end'):
193 # if filters.get('start') or filters.get('end'):
194 # # skip is offset, max-count is limit
194 # # skip is offset, max-count is limit
195 # if filters.get('start'):
195 # if filters.get('start'):
196 # extra_filter += ' --skip=%s' % filters['start']
196 # extra_filter += ' --skip=%s' % filters['start']
197 # if filters.get('end'):
197 # if filters.get('end'):
198 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
198 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
199
199
200 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
200 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
201 try:
201 try:
202 output, __ = self.run_git_command(cmd)
202 output, __ = self.run_git_command(cmd)
203 except RepositoryError:
203 except RepositoryError:
204 # Can be raised for empty repositories
204 # Can be raised for empty repositories
205 return []
205 return []
206 return output.splitlines()
206 return output.splitlines()
207
207
208 def _get_commit_id(self, commit_id_or_idx):
208 def _get_commit_id(self, commit_id_or_idx):
209 def is_null(value):
209 def is_null(value):
210 return len(value) == commit_id_or_idx.count('0')
210 return len(value) == commit_id_or_idx.count('0')
211
211
212 if self.is_empty():
212 if self.is_empty():
213 raise EmptyRepositoryError("There are no commits yet")
213 raise EmptyRepositoryError("There are no commits yet")
214
214
215 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
215 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
216 return self.commit_ids[-1]
216 return self.commit_ids[-1]
217
217
218 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
218 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
219 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
219 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
220 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
220 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
221 try:
221 try:
222 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
222 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
223 except Exception:
223 except Exception:
224 msg = "Commit %s does not exist for %s" % (
224 msg = "Commit %s does not exist for %s" % (
225 commit_id_or_idx, self)
225 commit_id_or_idx, self)
226 raise CommitDoesNotExistError(msg)
226 raise CommitDoesNotExistError(msg)
227
227
228 elif is_bstr:
228 elif is_bstr:
229 # check full path ref, eg. refs/heads/master
229 # check full path ref, eg. refs/heads/master
230 ref_id = self._refs.get(commit_id_or_idx)
230 ref_id = self._refs.get(commit_id_or_idx)
231 if ref_id:
231 if ref_id:
232 return ref_id
232 return ref_id
233
233
234 # check branch name
234 # check branch name
235 branch_ids = self.branches.values()
235 branch_ids = self.branches.values()
236 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
236 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
237 if ref_id:
237 if ref_id:
238 return ref_id
238 return ref_id
239
239
240 # check tag name
240 # check tag name
241 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
241 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
242 if ref_id:
242 if ref_id:
243 return ref_id
243 return ref_id
244
244
245 if (not SHA_PATTERN.match(commit_id_or_idx) or
245 if (not SHA_PATTERN.match(commit_id_or_idx) or
246 commit_id_or_idx not in self.commit_ids):
246 commit_id_or_idx not in self.commit_ids):
247 msg = "Commit %s does not exist for %s" % (
247 msg = "Commit %s does not exist for %s" % (
248 commit_id_or_idx, self)
248 commit_id_or_idx, self)
249 raise CommitDoesNotExistError(msg)
249 raise CommitDoesNotExistError(msg)
250
250
251 # Ensure we return full id
251 # Ensure we return full id
252 if not SHA_PATTERN.match(str(commit_id_or_idx)):
252 if not SHA_PATTERN.match(str(commit_id_or_idx)):
253 raise CommitDoesNotExistError(
253 raise CommitDoesNotExistError(
254 "Given commit id %s not recognized" % commit_id_or_idx)
254 "Given commit id %s not recognized" % commit_id_or_idx)
255 return commit_id_or_idx
255 return commit_id_or_idx
256
256
257 def get_hook_location(self):
257 def get_hook_location(self):
258 """
258 """
259 returns absolute path to location where hooks are stored
259 returns absolute path to location where hooks are stored
260 """
260 """
261 loc = os.path.join(self.path, 'hooks')
261 loc = os.path.join(self.path, 'hooks')
262 if not self.bare:
262 if not self.bare:
263 loc = os.path.join(self.path, '.git', 'hooks')
263 loc = os.path.join(self.path, '.git', 'hooks')
264 return loc
264 return loc
265
265
266 @LazyProperty
266 @LazyProperty
267 def last_change(self):
267 def last_change(self):
268 """
268 """
269 Returns last change made on this repository as
269 Returns last change made on this repository as
270 `datetime.datetime` object.
270 `datetime.datetime` object.
271 """
271 """
272 try:
272 try:
273 return self.get_commit().date
273 return self.get_commit().date
274 except RepositoryError:
274 except RepositoryError:
275 tzoffset = makedate()[1]
275 tzoffset = makedate()[1]
276 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
276 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
277
277
278 def _get_fs_mtime(self):
278 def _get_fs_mtime(self):
279 idx_loc = '' if self.bare else '.git'
279 idx_loc = '' if self.bare else '.git'
280 # fallback to filesystem
280 # fallback to filesystem
281 in_path = os.path.join(self.path, idx_loc, "index")
281 in_path = os.path.join(self.path, idx_loc, "index")
282 he_path = os.path.join(self.path, idx_loc, "HEAD")
282 he_path = os.path.join(self.path, idx_loc, "HEAD")
283 if os.path.exists(in_path):
283 if os.path.exists(in_path):
284 return os.stat(in_path).st_mtime
284 return os.stat(in_path).st_mtime
285 else:
285 else:
286 return os.stat(he_path).st_mtime
286 return os.stat(he_path).st_mtime
287
287
288 @LazyProperty
288 @LazyProperty
289 def description(self):
289 def description(self):
290 description = self._remote.get_description()
290 description = self._remote.get_description()
291 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
291 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
292
292
293 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
293 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
294 if self.is_empty():
294 if self.is_empty():
295 return OrderedDict()
295 return OrderedDict()
296
296
297 result = []
297 result = []
298 for ref, sha in self._refs.iteritems():
298 for ref, sha in self._refs.iteritems():
299 if ref.startswith(prefix):
299 if ref.startswith(prefix):
300 ref_name = ref
300 ref_name = ref
301 if strip_prefix:
301 if strip_prefix:
302 ref_name = ref[len(prefix):]
302 ref_name = ref[len(prefix):]
303 result.append((safe_unicode(ref_name), sha))
303 result.append((safe_unicode(ref_name), sha))
304
304
305 def get_name(entry):
305 def get_name(entry):
306 return entry[0]
306 return entry[0]
307
307
308 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
308 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
309
309
310 def _get_branches(self):
310 def _get_branches(self):
311 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
311 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
312
312
313 @LazyProperty
313 @LazyProperty
314 def branches(self):
314 def branches(self):
315 return self._get_branches()
315 return self._get_branches()
316
316
317 @LazyProperty
317 @LazyProperty
318 def branches_closed(self):
318 def branches_closed(self):
319 return {}
319 return {}
320
320
321 @LazyProperty
321 @LazyProperty
322 def branches_all(self):
322 def branches_all(self):
323 all_branches = {}
323 all_branches = {}
324 all_branches.update(self.branches)
324 all_branches.update(self.branches)
325 all_branches.update(self.branches_closed)
325 all_branches.update(self.branches_closed)
326 return all_branches
326 return all_branches
327
327
328 @LazyProperty
328 @LazyProperty
329 def tags(self):
329 def tags(self):
330 return self._get_tags()
330 return self._get_tags()
331
331
332 def _get_tags(self):
332 def _get_tags(self):
333 return self._get_refs_entries(
333 return self._get_refs_entries(
334 prefix='refs/tags/', strip_prefix=True, reverse=True)
334 prefix='refs/tags/', strip_prefix=True, reverse=True)
335
335
336 def tag(self, name, user, commit_id=None, message=None, date=None,
336 def tag(self, name, user, commit_id=None, message=None, date=None,
337 **kwargs):
337 **kwargs):
338 # TODO: fix this method to apply annotated tags correct with message
338 # TODO: fix this method to apply annotated tags correct with message
339 """
339 """
340 Creates and returns a tag for the given ``commit_id``.
340 Creates and returns a tag for the given ``commit_id``.
341
341
342 :param name: name for new tag
342 :param name: name for new tag
343 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
343 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
344 :param commit_id: commit id for which new tag would be created
344 :param commit_id: commit id for which new tag would be created
345 :param message: message of the tag's commit
345 :param message: message of the tag's commit
346 :param date: date of tag's commit
346 :param date: date of tag's commit
347
347
348 :raises TagAlreadyExistError: if tag with same name already exists
348 :raises TagAlreadyExistError: if tag with same name already exists
349 """
349 """
350 if name in self.tags:
350 if name in self.tags:
351 raise TagAlreadyExistError("Tag %s already exists" % name)
351 raise TagAlreadyExistError("Tag %s already exists" % name)
352 commit = self.get_commit(commit_id=commit_id)
352 commit = self.get_commit(commit_id=commit_id)
353 message = message or "Added tag %s for commit %s" % (
353 message = message or "Added tag %s for commit %s" % (
354 name, commit.raw_id)
354 name, commit.raw_id)
355 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
355 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
356
356
357 self._refs = self._get_refs()
357 self._refs = self._get_refs()
358 self.tags = self._get_tags()
358 self.tags = self._get_tags()
359 return commit
359 return commit
360
360
361 def remove_tag(self, name, user, message=None, date=None):
361 def remove_tag(self, name, user, message=None, date=None):
362 """
362 """
363 Removes tag with the given ``name``.
363 Removes tag with the given ``name``.
364
364
365 :param name: name of the tag to be removed
365 :param name: name of the tag to be removed
366 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
366 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
367 :param message: message of the tag's removal commit
367 :param message: message of the tag's removal commit
368 :param date: date of tag's removal commit
368 :param date: date of tag's removal commit
369
369
370 :raises TagDoesNotExistError: if tag with given name does not exists
370 :raises TagDoesNotExistError: if tag with given name does not exists
371 """
371 """
372 if name not in self.tags:
372 if name not in self.tags:
373 raise TagDoesNotExistError("Tag %s does not exist" % name)
373 raise TagDoesNotExistError("Tag %s does not exist" % name)
374 tagpath = vcspath.join(
374 tagpath = vcspath.join(
375 self._remote.get_refs_path(), 'refs', 'tags', name)
375 self._remote.get_refs_path(), 'refs', 'tags', name)
376 try:
376 try:
377 os.remove(tagpath)
377 os.remove(tagpath)
378 self._refs = self._get_refs()
378 self._refs = self._get_refs()
379 self.tags = self._get_tags()
379 self.tags = self._get_tags()
380 except OSError as e:
380 except OSError as e:
381 raise RepositoryError(e.strerror)
381 raise RepositoryError(e.strerror)
382
382
383 def _get_refs(self):
383 def _get_refs(self):
384 return self._remote.get_refs()
384 return self._remote.get_refs()
385
385
386 @LazyProperty
386 @LazyProperty
387 def _refs(self):
387 def _refs(self):
388 return self._get_refs()
388 return self._get_refs()
389
389
390 @property
390 @property
391 def _ref_tree(self):
391 def _ref_tree(self):
392 node = tree = {}
392 node = tree = {}
393 for ref, sha in self._refs.iteritems():
393 for ref, sha in self._refs.iteritems():
394 path = ref.split('/')
394 path = ref.split('/')
395 for bit in path[:-1]:
395 for bit in path[:-1]:
396 node = node.setdefault(bit, {})
396 node = node.setdefault(bit, {})
397 node[path[-1]] = sha
397 node[path[-1]] = sha
398 node = tree
398 node = tree
399 return tree
399 return tree
400
400
401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
402 """
402 """
403 Returns `GitCommit` object representing commit from git repository
403 Returns `GitCommit` object representing commit from git repository
404 at the given `commit_id` or head (most recent commit) if None given.
404 at the given `commit_id` or head (most recent commit) if None given.
405 """
405 """
406 if commit_id is not None:
406 if commit_id is not None:
407 self._validate_commit_id(commit_id)
407 self._validate_commit_id(commit_id)
408 elif commit_idx is not None:
408 elif commit_idx is not None:
409 self._validate_commit_idx(commit_idx)
409 self._validate_commit_idx(commit_idx)
410 commit_id = commit_idx
410 commit_id = commit_idx
411 commit_id = self._get_commit_id(commit_id)
411 commit_id = self._get_commit_id(commit_id)
412 try:
412 try:
413 # Need to call remote to translate id for tagging scenario
413 # Need to call remote to translate id for tagging scenario
414 commit_id = self._remote.get_object(commit_id)["commit_id"]
414 commit_id = self._remote.get_object(commit_id)["commit_id"]
415 idx = self._commit_ids[commit_id]
415 idx = self._commit_ids[commit_id]
416 except KeyError:
416 except KeyError:
417 raise RepositoryError("Cannot get object with id %s" % commit_id)
417 raise RepositoryError("Cannot get object with id %s" % commit_id)
418
418
419 return GitCommit(self, commit_id, idx, pre_load=pre_load)
419 return GitCommit(self, commit_id, idx, pre_load=pre_load)
420
420
421 def get_commits(
421 def get_commits(
422 self, start_id=None, end_id=None, start_date=None, end_date=None,
422 self, start_id=None, end_id=None, start_date=None, end_date=None,
423 branch_name=None, pre_load=None):
423 branch_name=None, show_hidden=False, pre_load=None):
424 """
424 """
425 Returns generator of `GitCommit` objects from start to end (both
425 Returns generator of `GitCommit` objects from start to end (both
426 are inclusive), in ascending date order.
426 are inclusive), in ascending date order.
427
427
428 :param start_id: None, str(commit_id)
428 :param start_id: None, str(commit_id)
429 :param end_id: None, str(commit_id)
429 :param end_id: None, str(commit_id)
430 :param start_date: if specified, commits with commit date less than
430 :param start_date: if specified, commits with commit date less than
431 ``start_date`` would be filtered out from returned set
431 ``start_date`` would be filtered out from returned set
432 :param end_date: if specified, commits with commit date greater than
432 :param end_date: if specified, commits with commit date greater than
433 ``end_date`` would be filtered out from returned set
433 ``end_date`` would be filtered out from returned set
434 :param branch_name: if specified, commits not reachable from given
434 :param branch_name: if specified, commits not reachable from given
435 branch would be filtered out from returned set
435 branch would be filtered out from returned set
436
436 :param show_hidden: Show hidden commits such as obsolete or hidden from
437 Mercurial evolve
437 :raise BranchDoesNotExistError: If given `branch_name` does not
438 :raise BranchDoesNotExistError: If given `branch_name` does not
438 exist.
439 exist.
439 :raise CommitDoesNotExistError: If commits for given `start` or
440 :raise CommitDoesNotExistError: If commits for given `start` or
440 `end` could not be found.
441 `end` could not be found.
441
442
442 """
443 """
443 if self.is_empty():
444 if self.is_empty():
444 raise EmptyRepositoryError("There are no commits yet")
445 raise EmptyRepositoryError("There are no commits yet")
445 self._validate_branch_name(branch_name)
446 self._validate_branch_name(branch_name)
446
447
447 if start_id is not None:
448 if start_id is not None:
448 self._validate_commit_id(start_id)
449 self._validate_commit_id(start_id)
449 if end_id is not None:
450 if end_id is not None:
450 self._validate_commit_id(end_id)
451 self._validate_commit_id(end_id)
451
452
452 start_raw_id = self._get_commit_id(start_id)
453 start_raw_id = self._get_commit_id(start_id)
453 start_pos = self._commit_ids[start_raw_id] if start_id else None
454 start_pos = self._commit_ids[start_raw_id] if start_id else None
454 end_raw_id = self._get_commit_id(end_id)
455 end_raw_id = self._get_commit_id(end_id)
455 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
456 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
456
457
457 if None not in [start_id, end_id] and start_pos > end_pos:
458 if None not in [start_id, end_id] and start_pos > end_pos:
458 raise RepositoryError(
459 raise RepositoryError(
459 "Start commit '%s' cannot be after end commit '%s'" %
460 "Start commit '%s' cannot be after end commit '%s'" %
460 (start_id, end_id))
461 (start_id, end_id))
461
462
462 if end_pos is not None:
463 if end_pos is not None:
463 end_pos += 1
464 end_pos += 1
464
465
465 filter_ = []
466 filter_ = []
466 if branch_name:
467 if branch_name:
467 filter_.append({'branch_name': branch_name})
468 filter_.append({'branch_name': branch_name})
468 if start_date and not end_date:
469 if start_date and not end_date:
469 filter_.append({'since': start_date})
470 filter_.append({'since': start_date})
470 if end_date and not start_date:
471 if end_date and not start_date:
471 filter_.append({'until': end_date})
472 filter_.append({'until': end_date})
472 if start_date and end_date:
473 if start_date and end_date:
473 filter_.append({'since': start_date})
474 filter_.append({'since': start_date})
474 filter_.append({'until': end_date})
475 filter_.append({'until': end_date})
475
476
476 # if start_pos or end_pos:
477 # if start_pos or end_pos:
477 # filter_.append({'start': start_pos})
478 # filter_.append({'start': start_pos})
478 # filter_.append({'end': end_pos})
479 # filter_.append({'end': end_pos})
479
480
480 if filter_:
481 if filter_:
481 revfilters = {
482 revfilters = {
482 'branch_name': branch_name,
483 'branch_name': branch_name,
483 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
484 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
484 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
485 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
485 'start': start_pos,
486 'start': start_pos,
486 'end': end_pos,
487 'end': end_pos,
487 }
488 }
488 commit_ids = self._get_all_commit_ids(filters=revfilters)
489 commit_ids = self._get_all_commit_ids(filters=revfilters)
489
490
490 # pure python stuff, it's slow due to walker walking whole repo
491 # pure python stuff, it's slow due to walker walking whole repo
491 # def get_revs(walker):
492 # def get_revs(walker):
492 # for walker_entry in walker:
493 # for walker_entry in walker:
493 # yield walker_entry.commit.id
494 # yield walker_entry.commit.id
494 # revfilters = {}
495 # revfilters = {}
495 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
496 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
496 else:
497 else:
497 commit_ids = self.commit_ids
498 commit_ids = self.commit_ids
498
499
499 if start_pos or end_pos:
500 if start_pos or end_pos:
500 commit_ids = commit_ids[start_pos: end_pos]
501 commit_ids = commit_ids[start_pos: end_pos]
501
502
502 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
503 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
503
504
504 def get_diff(
505 def get_diff(
505 self, commit1, commit2, path='', ignore_whitespace=False,
506 self, commit1, commit2, path='', ignore_whitespace=False,
506 context=3, path1=None):
507 context=3, path1=None):
507 """
508 """
508 Returns (git like) *diff*, as plain text. Shows changes introduced by
509 Returns (git like) *diff*, as plain text. Shows changes introduced by
509 ``commit2`` since ``commit1``.
510 ``commit2`` since ``commit1``.
510
511
511 :param commit1: Entry point from which diff is shown. Can be
512 :param commit1: Entry point from which diff is shown. Can be
512 ``self.EMPTY_COMMIT`` - in this case, patch showing all
513 ``self.EMPTY_COMMIT`` - in this case, patch showing all
513 the changes since empty state of the repository until ``commit2``
514 the changes since empty state of the repository until ``commit2``
514 :param commit2: Until which commits changes should be shown.
515 :param commit2: Until which commits changes should be shown.
515 :param ignore_whitespace: If set to ``True``, would not show whitespace
516 :param ignore_whitespace: If set to ``True``, would not show whitespace
516 changes. Defaults to ``False``.
517 changes. Defaults to ``False``.
517 :param context: How many lines before/after changed lines should be
518 :param context: How many lines before/after changed lines should be
518 shown. Defaults to ``3``.
519 shown. Defaults to ``3``.
519 """
520 """
520 self._validate_diff_commits(commit1, commit2)
521 self._validate_diff_commits(commit1, commit2)
521 if path1 is not None and path1 != path:
522 if path1 is not None and path1 != path:
522 raise ValueError("Diff of two different paths not supported.")
523 raise ValueError("Diff of two different paths not supported.")
523
524
524 flags = [
525 flags = [
525 '-U%s' % context, '--full-index', '--binary', '-p',
526 '-U%s' % context, '--full-index', '--binary', '-p',
526 '-M', '--abbrev=40']
527 '-M', '--abbrev=40']
527 if ignore_whitespace:
528 if ignore_whitespace:
528 flags.append('-w')
529 flags.append('-w')
529
530
530 if commit1 == self.EMPTY_COMMIT:
531 if commit1 == self.EMPTY_COMMIT:
531 cmd = ['show'] + flags + [commit2.raw_id]
532 cmd = ['show'] + flags + [commit2.raw_id]
532 else:
533 else:
533 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
534 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
534
535
535 if path:
536 if path:
536 cmd.extend(['--', path])
537 cmd.extend(['--', path])
537
538
538 stdout, __ = self.run_git_command(cmd)
539 stdout, __ = self.run_git_command(cmd)
539 # If we used 'show' command, strip first few lines (until actual diff
540 # If we used 'show' command, strip first few lines (until actual diff
540 # starts)
541 # starts)
541 if commit1 == self.EMPTY_COMMIT:
542 if commit1 == self.EMPTY_COMMIT:
542 lines = stdout.splitlines()
543 lines = stdout.splitlines()
543 x = 0
544 x = 0
544 for line in lines:
545 for line in lines:
545 if line.startswith('diff'):
546 if line.startswith('diff'):
546 break
547 break
547 x += 1
548 x += 1
548 # Append new line just like 'diff' command do
549 # Append new line just like 'diff' command do
549 stdout = '\n'.join(lines[x:]) + '\n'
550 stdout = '\n'.join(lines[x:]) + '\n'
550 return GitDiff(stdout)
551 return GitDiff(stdout)
551
552
552 def strip(self, commit_id, branch_name):
553 def strip(self, commit_id, branch_name):
553 commit = self.get_commit(commit_id=commit_id)
554 commit = self.get_commit(commit_id=commit_id)
554 if commit.merge:
555 if commit.merge:
555 raise Exception('Cannot reset to merge commit')
556 raise Exception('Cannot reset to merge commit')
556
557
557 # parent is going to be the new head now
558 # parent is going to be the new head now
558 commit = commit.parents[0]
559 commit = commit.parents[0]
559 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
560 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
560
561
561 self.commit_ids = self._get_all_commit_ids()
562 self.commit_ids = self._get_all_commit_ids()
562 self._rebuild_cache(self.commit_ids)
563 self._rebuild_cache(self.commit_ids)
563
564
564 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
565 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
565 if commit_id1 == commit_id2:
566 if commit_id1 == commit_id2:
566 return commit_id1
567 return commit_id1
567
568
568 if self != repo2:
569 if self != repo2:
569 commits = self._remote.get_missing_revs(
570 commits = self._remote.get_missing_revs(
570 commit_id1, commit_id2, repo2.path)
571 commit_id1, commit_id2, repo2.path)
571 if commits:
572 if commits:
572 commit = repo2.get_commit(commits[-1])
573 commit = repo2.get_commit(commits[-1])
573 if commit.parents:
574 if commit.parents:
574 ancestor_id = commit.parents[0].raw_id
575 ancestor_id = commit.parents[0].raw_id
575 else:
576 else:
576 ancestor_id = None
577 ancestor_id = None
577 else:
578 else:
578 # no commits from other repo, ancestor_id is the commit_id2
579 # no commits from other repo, ancestor_id is the commit_id2
579 ancestor_id = commit_id2
580 ancestor_id = commit_id2
580 else:
581 else:
581 output, __ = self.run_git_command(
582 output, __ = self.run_git_command(
582 ['merge-base', commit_id1, commit_id2])
583 ['merge-base', commit_id1, commit_id2])
583 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
584 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
584
585
585 return ancestor_id
586 return ancestor_id
586
587
587 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
588 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
588 repo1 = self
589 repo1 = self
589 ancestor_id = None
590 ancestor_id = None
590
591
591 if commit_id1 == commit_id2:
592 if commit_id1 == commit_id2:
592 commits = []
593 commits = []
593 elif repo1 != repo2:
594 elif repo1 != repo2:
594 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
595 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
595 repo2.path)
596 repo2.path)
596 commits = [
597 commits = [
597 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
598 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
598 for commit_id in reversed(missing_ids)]
599 for commit_id in reversed(missing_ids)]
599 else:
600 else:
600 output, __ = repo1.run_git_command(
601 output, __ = repo1.run_git_command(
601 ['log', '--reverse', '--pretty=format: %H', '-s',
602 ['log', '--reverse', '--pretty=format: %H', '-s',
602 '%s..%s' % (commit_id1, commit_id2)])
603 '%s..%s' % (commit_id1, commit_id2)])
603 commits = [
604 commits = [
604 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
605 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
605 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
606 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
606
607
607 return commits
608 return commits
608
609
609 @LazyProperty
610 @LazyProperty
610 def in_memory_commit(self):
611 def in_memory_commit(self):
611 """
612 """
612 Returns ``GitInMemoryCommit`` object for this repository.
613 Returns ``GitInMemoryCommit`` object for this repository.
613 """
614 """
614 return GitInMemoryCommit(self)
615 return GitInMemoryCommit(self)
615
616
616 def clone(self, url, update_after_clone=True, bare=False):
617 def clone(self, url, update_after_clone=True, bare=False):
617 """
618 """
618 Tries to clone commits from external location.
619 Tries to clone commits from external location.
619
620
620 :param update_after_clone: If set to ``False``, git won't checkout
621 :param update_after_clone: If set to ``False``, git won't checkout
621 working directory
622 working directory
622 :param bare: If set to ``True``, repository would be cloned into
623 :param bare: If set to ``True``, repository would be cloned into
623 *bare* git repository (no working directory at all).
624 *bare* git repository (no working directory at all).
624 """
625 """
625 # init_bare and init expect empty dir created to proceed
626 # init_bare and init expect empty dir created to proceed
626 if not os.path.exists(self.path):
627 if not os.path.exists(self.path):
627 os.mkdir(self.path)
628 os.mkdir(self.path)
628
629
629 if bare:
630 if bare:
630 self._remote.init_bare()
631 self._remote.init_bare()
631 else:
632 else:
632 self._remote.init()
633 self._remote.init()
633
634
634 deferred = '^{}'
635 deferred = '^{}'
635 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
636 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
636
637
637 return self._remote.clone(
638 return self._remote.clone(
638 url, deferred, valid_refs, update_after_clone)
639 url, deferred, valid_refs, update_after_clone)
639
640
640 def pull(self, url, commit_ids=None):
641 def pull(self, url, commit_ids=None):
641 """
642 """
642 Tries to pull changes from external location. We use fetch here since
643 Tries to pull changes from external location. We use fetch here since
643 pull in get does merges and we want to be compatible with hg backend so
644 pull in get does merges and we want to be compatible with hg backend so
644 pull == fetch in this case
645 pull == fetch in this case
645 """
646 """
646 self.fetch(url, commit_ids=commit_ids)
647 self.fetch(url, commit_ids=commit_ids)
647
648
648 def fetch(self, url, commit_ids=None):
649 def fetch(self, url, commit_ids=None):
649 """
650 """
650 Tries to fetch changes from external location.
651 Tries to fetch changes from external location.
651 """
652 """
652 refs = None
653 refs = None
653
654
654 if commit_ids is not None:
655 if commit_ids is not None:
655 remote_refs = self._remote.get_remote_refs(url)
656 remote_refs = self._remote.get_remote_refs(url)
656 refs = [
657 refs = [
657 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
658 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
658 self._remote.fetch(url, refs=refs)
659 self._remote.fetch(url, refs=refs)
659
660
660 def set_refs(self, ref_name, commit_id):
661 def set_refs(self, ref_name, commit_id):
661 self._remote.set_refs(ref_name, commit_id)
662 self._remote.set_refs(ref_name, commit_id)
662
663
663 def remove_ref(self, ref_name):
664 def remove_ref(self, ref_name):
664 self._remote.remove_ref(ref_name)
665 self._remote.remove_ref(ref_name)
665
666
666 def _update_server_info(self):
667 def _update_server_info(self):
667 """
668 """
668 runs gits update-server-info command in this repo instance
669 runs gits update-server-info command in this repo instance
669 """
670 """
670 self._remote.update_server_info()
671 self._remote.update_server_info()
671
672
672 def _current_branch(self):
673 def _current_branch(self):
673 """
674 """
674 Return the name of the current branch.
675 Return the name of the current branch.
675
676
676 It only works for non bare repositories (i.e. repositories with a
677 It only works for non bare repositories (i.e. repositories with a
677 working copy)
678 working copy)
678 """
679 """
679 if self.bare:
680 if self.bare:
680 raise RepositoryError('Bare git repos do not have active branches')
681 raise RepositoryError('Bare git repos do not have active branches')
681
682
682 if self.is_empty():
683 if self.is_empty():
683 return None
684 return None
684
685
685 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
686 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
686 return stdout.strip()
687 return stdout.strip()
687
688
688 def _checkout(self, branch_name, create=False):
689 def _checkout(self, branch_name, create=False):
689 """
690 """
690 Checkout a branch in the working directory.
691 Checkout a branch in the working directory.
691
692
692 It tries to create the branch if create is True, failing if the branch
693 It tries to create the branch if create is True, failing if the branch
693 already exists.
694 already exists.
694
695
695 It only works for non bare repositories (i.e. repositories with a
696 It only works for non bare repositories (i.e. repositories with a
696 working copy)
697 working copy)
697 """
698 """
698 if self.bare:
699 if self.bare:
699 raise RepositoryError('Cannot checkout branches in a bare git repo')
700 raise RepositoryError('Cannot checkout branches in a bare git repo')
700
701
701 cmd = ['checkout']
702 cmd = ['checkout']
702 if create:
703 if create:
703 cmd.append('-b')
704 cmd.append('-b')
704 cmd.append(branch_name)
705 cmd.append(branch_name)
705 self.run_git_command(cmd, fail_on_stderr=False)
706 self.run_git_command(cmd, fail_on_stderr=False)
706
707
707 def _identify(self):
708 def _identify(self):
708 """
709 """
709 Return the current state of the working directory.
710 Return the current state of the working directory.
710 """
711 """
711 if self.bare:
712 if self.bare:
712 raise RepositoryError('Bare git repos do not have active branches')
713 raise RepositoryError('Bare git repos do not have active branches')
713
714
714 if self.is_empty():
715 if self.is_empty():
715 return None
716 return None
716
717
717 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
718 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
718 return stdout.strip()
719 return stdout.strip()
719
720
720 def _local_clone(self, clone_path, branch_name):
721 def _local_clone(self, clone_path, branch_name):
721 """
722 """
722 Create a local clone of the current repo.
723 Create a local clone of the current repo.
723 """
724 """
724 # N.B.(skreft): the --branch option is required as otherwise the shallow
725 # N.B.(skreft): the --branch option is required as otherwise the shallow
725 # clone will only fetch the active branch.
726 # clone will only fetch the active branch.
726 cmd = ['clone', '--branch', branch_name, '--single-branch',
727 cmd = ['clone', '--branch', branch_name, '--single-branch',
727 self.path, os.path.abspath(clone_path)]
728 self.path, os.path.abspath(clone_path)]
728 self.run_git_command(cmd, fail_on_stderr=False)
729 self.run_git_command(cmd, fail_on_stderr=False)
729
730
730 def _local_fetch(self, repository_path, branch_name):
731 def _local_fetch(self, repository_path, branch_name):
731 """
732 """
732 Fetch a branch from a local repository.
733 Fetch a branch from a local repository.
733 """
734 """
734 repository_path = os.path.abspath(repository_path)
735 repository_path = os.path.abspath(repository_path)
735 if repository_path == self.path:
736 if repository_path == self.path:
736 raise ValueError('Cannot fetch from the same repository')
737 raise ValueError('Cannot fetch from the same repository')
737
738
738 cmd = ['fetch', '--no-tags', repository_path, branch_name]
739 cmd = ['fetch', '--no-tags', repository_path, branch_name]
739 self.run_git_command(cmd, fail_on_stderr=False)
740 self.run_git_command(cmd, fail_on_stderr=False)
740
741
741 def _last_fetch_heads(self):
742 def _last_fetch_heads(self):
742 """
743 """
743 Return the last fetched heads that need merging.
744 Return the last fetched heads that need merging.
744
745
745 The algorithm is defined at
746 The algorithm is defined at
746 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
747 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
747 """
748 """
748 if not self.bare:
749 if not self.bare:
749 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
750 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
750 else:
751 else:
751 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
752 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
752
753
753 heads = []
754 heads = []
754 with open(fetch_heads_path) as f:
755 with open(fetch_heads_path) as f:
755 for line in f:
756 for line in f:
756 if ' not-for-merge ' in line:
757 if ' not-for-merge ' in line:
757 continue
758 continue
758 line = re.sub('\t.*', '', line, flags=re.DOTALL)
759 line = re.sub('\t.*', '', line, flags=re.DOTALL)
759 heads.append(line)
760 heads.append(line)
760
761
761 return heads
762 return heads
762
763
763 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
764 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
764 return GitRepository(shadow_repository_path)
765 return GitRepository(shadow_repository_path)
765
766
766 def _local_pull(self, repository_path, branch_name):
767 def _local_pull(self, repository_path, branch_name):
767 """
768 """
768 Pull a branch from a local repository.
769 Pull a branch from a local repository.
769 """
770 """
770 if self.bare:
771 if self.bare:
771 raise RepositoryError('Cannot pull into a bare git repository')
772 raise RepositoryError('Cannot pull into a bare git repository')
772 # N.B.(skreft): The --ff-only option is to make sure this is a
773 # N.B.(skreft): The --ff-only option is to make sure this is a
773 # fast-forward (i.e., we are only pulling new changes and there are no
774 # fast-forward (i.e., we are only pulling new changes and there are no
774 # conflicts with our current branch)
775 # conflicts with our current branch)
775 # Additionally, that option needs to go before --no-tags, otherwise git
776 # Additionally, that option needs to go before --no-tags, otherwise git
776 # pull complains about it being an unknown flag.
777 # pull complains about it being an unknown flag.
777 cmd = ['pull', '--ff-only', '--no-tags', repository_path, branch_name]
778 cmd = ['pull', '--ff-only', '--no-tags', repository_path, branch_name]
778 self.run_git_command(cmd, fail_on_stderr=False)
779 self.run_git_command(cmd, fail_on_stderr=False)
779
780
780 def _local_merge(self, merge_message, user_name, user_email, heads):
781 def _local_merge(self, merge_message, user_name, user_email, heads):
781 """
782 """
782 Merge the given head into the checked out branch.
783 Merge the given head into the checked out branch.
783
784
784 It will force a merge commit.
785 It will force a merge commit.
785
786
786 Currently it raises an error if the repo is empty, as it is not possible
787 Currently it raises an error if the repo is empty, as it is not possible
787 to create a merge commit in an empty repo.
788 to create a merge commit in an empty repo.
788
789
789 :param merge_message: The message to use for the merge commit.
790 :param merge_message: The message to use for the merge commit.
790 :param heads: the heads to merge.
791 :param heads: the heads to merge.
791 """
792 """
792 if self.bare:
793 if self.bare:
793 raise RepositoryError('Cannot merge into a bare git repository')
794 raise RepositoryError('Cannot merge into a bare git repository')
794
795
795 if not heads:
796 if not heads:
796 return
797 return
797
798
798 if self.is_empty():
799 if self.is_empty():
799 # TODO(skreft): do somehting more robust in this case.
800 # TODO(skreft): do somehting more robust in this case.
800 raise RepositoryError(
801 raise RepositoryError(
801 'Do not know how to merge into empty repositories yet')
802 'Do not know how to merge into empty repositories yet')
802
803
803 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
804 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
804 # commit message. We also specify the user who is doing the merge.
805 # commit message. We also specify the user who is doing the merge.
805 cmd = ['-c', 'user.name=%s' % safe_str(user_name),
806 cmd = ['-c', 'user.name=%s' % safe_str(user_name),
806 '-c', 'user.email=%s' % safe_str(user_email),
807 '-c', 'user.email=%s' % safe_str(user_email),
807 'merge', '--no-ff', '-m', safe_str(merge_message)]
808 'merge', '--no-ff', '-m', safe_str(merge_message)]
808 cmd.extend(heads)
809 cmd.extend(heads)
809 try:
810 try:
810 self.run_git_command(cmd, fail_on_stderr=False)
811 self.run_git_command(cmd, fail_on_stderr=False)
811 except RepositoryError:
812 except RepositoryError:
812 # Cleanup any merge leftovers
813 # Cleanup any merge leftovers
813 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
814 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
814 raise
815 raise
815
816
816 def _local_push(
817 def _local_push(
817 self, source_branch, repository_path, target_branch,
818 self, source_branch, repository_path, target_branch,
818 enable_hooks=False, rc_scm_data=None):
819 enable_hooks=False, rc_scm_data=None):
819 """
820 """
820 Push the source_branch to the given repository and target_branch.
821 Push the source_branch to the given repository and target_branch.
821
822
822 Currently it if the target_branch is not master and the target repo is
823 Currently it if the target_branch is not master and the target repo is
823 empty, the push will work, but then GitRepository won't be able to find
824 empty, the push will work, but then GitRepository won't be able to find
824 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
825 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
825 pointing to master, which does not exist).
826 pointing to master, which does not exist).
826
827
827 It does not run the hooks in the target repo.
828 It does not run the hooks in the target repo.
828 """
829 """
829 # TODO(skreft): deal with the case in which the target repo is empty,
830 # TODO(skreft): deal with the case in which the target repo is empty,
830 # and the target_branch is not master.
831 # and the target_branch is not master.
831 target_repo = GitRepository(repository_path)
832 target_repo = GitRepository(repository_path)
832 if (not target_repo.bare and
833 if (not target_repo.bare and
833 target_repo._current_branch() == target_branch):
834 target_repo._current_branch() == target_branch):
834 # Git prevents pushing to the checked out branch, so simulate it by
835 # Git prevents pushing to the checked out branch, so simulate it by
835 # pulling into the target repository.
836 # pulling into the target repository.
836 target_repo._local_pull(self.path, source_branch)
837 target_repo._local_pull(self.path, source_branch)
837 else:
838 else:
838 cmd = ['push', os.path.abspath(repository_path),
839 cmd = ['push', os.path.abspath(repository_path),
839 '%s:%s' % (source_branch, target_branch)]
840 '%s:%s' % (source_branch, target_branch)]
840 gitenv = {}
841 gitenv = {}
841 if rc_scm_data:
842 if rc_scm_data:
842 gitenv.update({'RC_SCM_DATA': rc_scm_data})
843 gitenv.update({'RC_SCM_DATA': rc_scm_data})
843
844
844 if not enable_hooks:
845 if not enable_hooks:
845 gitenv['RC_SKIP_HOOKS'] = '1'
846 gitenv['RC_SKIP_HOOKS'] = '1'
846 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
847 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
847
848
848 def _get_new_pr_branch(self, source_branch, target_branch):
849 def _get_new_pr_branch(self, source_branch, target_branch):
849 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
850 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
850 pr_branches = []
851 pr_branches = []
851 for branch in self.branches:
852 for branch in self.branches:
852 if branch.startswith(prefix):
853 if branch.startswith(prefix):
853 pr_branches.append(int(branch[len(prefix):]))
854 pr_branches.append(int(branch[len(prefix):]))
854
855
855 if not pr_branches:
856 if not pr_branches:
856 branch_id = 0
857 branch_id = 0
857 else:
858 else:
858 branch_id = max(pr_branches) + 1
859 branch_id = max(pr_branches) + 1
859
860
860 return '%s%d' % (prefix, branch_id)
861 return '%s%d' % (prefix, branch_id)
861
862
862 def _merge_repo(self, shadow_repository_path, target_ref,
863 def _merge_repo(self, shadow_repository_path, target_ref,
863 source_repo, source_ref, merge_message,
864 source_repo, source_ref, merge_message,
864 merger_name, merger_email, dry_run=False,
865 merger_name, merger_email, dry_run=False,
865 use_rebase=False, close_branch=False):
866 use_rebase=False, close_branch=False):
866 if target_ref.commit_id != self.branches[target_ref.name]:
867 if target_ref.commit_id != self.branches[target_ref.name]:
867 return MergeResponse(
868 return MergeResponse(
868 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
869 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
869
870
870 shadow_repo = GitRepository(shadow_repository_path)
871 shadow_repo = GitRepository(shadow_repository_path)
871 shadow_repo._checkout(target_ref.name)
872 shadow_repo._checkout(target_ref.name)
872 shadow_repo._local_pull(self.path, target_ref.name)
873 shadow_repo._local_pull(self.path, target_ref.name)
873 # Need to reload repo to invalidate the cache, or otherwise we cannot
874 # Need to reload repo to invalidate the cache, or otherwise we cannot
874 # retrieve the last target commit.
875 # retrieve the last target commit.
875 shadow_repo = GitRepository(shadow_repository_path)
876 shadow_repo = GitRepository(shadow_repository_path)
876 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
877 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
877 return MergeResponse(
878 return MergeResponse(
878 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
879 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
879
880
880 pr_branch = shadow_repo._get_new_pr_branch(
881 pr_branch = shadow_repo._get_new_pr_branch(
881 source_ref.name, target_ref.name)
882 source_ref.name, target_ref.name)
882 shadow_repo._checkout(pr_branch, create=True)
883 shadow_repo._checkout(pr_branch, create=True)
883 try:
884 try:
884 shadow_repo._local_fetch(source_repo.path, source_ref.name)
885 shadow_repo._local_fetch(source_repo.path, source_ref.name)
885 except RepositoryError:
886 except RepositoryError:
886 log.exception('Failure when doing local fetch on git shadow repo')
887 log.exception('Failure when doing local fetch on git shadow repo')
887 return MergeResponse(
888 return MergeResponse(
888 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
889 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
889
890
890 merge_ref = None
891 merge_ref = None
891 merge_failure_reason = MergeFailureReason.NONE
892 merge_failure_reason = MergeFailureReason.NONE
892 try:
893 try:
893 shadow_repo._local_merge(merge_message, merger_name, merger_email,
894 shadow_repo._local_merge(merge_message, merger_name, merger_email,
894 [source_ref.commit_id])
895 [source_ref.commit_id])
895 merge_possible = True
896 merge_possible = True
896
897
897 # Need to reload repo to invalidate the cache, or otherwise we
898 # Need to reload repo to invalidate the cache, or otherwise we
898 # cannot retrieve the merge commit.
899 # cannot retrieve the merge commit.
899 shadow_repo = GitRepository(shadow_repository_path)
900 shadow_repo = GitRepository(shadow_repository_path)
900 merge_commit_id = shadow_repo.branches[pr_branch]
901 merge_commit_id = shadow_repo.branches[pr_branch]
901
902
902 # Set a reference pointing to the merge commit. This reference may
903 # Set a reference pointing to the merge commit. This reference may
903 # be used to easily identify the last successful merge commit in
904 # be used to easily identify the last successful merge commit in
904 # the shadow repository.
905 # the shadow repository.
905 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
906 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
906 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
907 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
907 except RepositoryError:
908 except RepositoryError:
908 log.exception('Failure when doing local merge on git shadow repo')
909 log.exception('Failure when doing local merge on git shadow repo')
909 merge_possible = False
910 merge_possible = False
910 merge_failure_reason = MergeFailureReason.MERGE_FAILED
911 merge_failure_reason = MergeFailureReason.MERGE_FAILED
911
912
912 if merge_possible and not dry_run:
913 if merge_possible and not dry_run:
913 try:
914 try:
914 shadow_repo._local_push(
915 shadow_repo._local_push(
915 pr_branch, self.path, target_ref.name, enable_hooks=True,
916 pr_branch, self.path, target_ref.name, enable_hooks=True,
916 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
917 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
917 merge_succeeded = True
918 merge_succeeded = True
918 except RepositoryError:
919 except RepositoryError:
919 log.exception(
920 log.exception(
920 'Failure when doing local push on git shadow repo')
921 'Failure when doing local push on git shadow repo')
921 merge_succeeded = False
922 merge_succeeded = False
922 merge_failure_reason = MergeFailureReason.PUSH_FAILED
923 merge_failure_reason = MergeFailureReason.PUSH_FAILED
923 else:
924 else:
924 merge_succeeded = False
925 merge_succeeded = False
925
926
926 return MergeResponse(
927 return MergeResponse(
927 merge_possible, merge_succeeded, merge_ref,
928 merge_possible, merge_succeeded, merge_ref,
928 merge_failure_reason)
929 merge_failure_reason)
929
930
930 def _get_shadow_repository_path(self, workspace_id):
931 def _get_shadow_repository_path(self, workspace_id):
931 # The name of the shadow repository must start with '.', so it is
932 # The name of the shadow repository must start with '.', so it is
932 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
933 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
933 return os.path.join(
934 return os.path.join(
934 os.path.dirname(self.path),
935 os.path.dirname(self.path),
935 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
936 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
936
937
937 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
938 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
938 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
939 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
939 if not os.path.exists(shadow_repository_path):
940 if not os.path.exists(shadow_repository_path):
940 self._local_clone(shadow_repository_path, target_ref.name)
941 self._local_clone(shadow_repository_path, target_ref.name)
941
942
942 return shadow_repository_path
943 return shadow_repository_path
943
944
944 def cleanup_merge_workspace(self, workspace_id):
945 def cleanup_merge_workspace(self, workspace_id):
945 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
946 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
946 shutil.rmtree(shadow_repository_path, ignore_errors=True)
947 shutil.rmtree(shadow_repository_path, ignore_errors=True)
@@ -1,882 +1,889 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 HG repository module
22 HG repository module
23 """
23 """
24
24
25 import logging
25 import logging
26 import binascii
26 import binascii
27 import os
27 import os
28 import shutil
28 import shutil
29 import urllib
29 import urllib
30
30
31 from zope.cachedescriptors.property import Lazy as LazyProperty
31 from zope.cachedescriptors.property import Lazy as LazyProperty
32
32
33 from rhodecode.lib.compat import OrderedDict
33 from rhodecode.lib.compat import OrderedDict
34 from rhodecode.lib.datelib import (
34 from rhodecode.lib.datelib import (
35 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate,
35 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate,
36 date_astimestamp)
36 date_astimestamp)
37 from rhodecode.lib.utils import safe_unicode, safe_str
37 from rhodecode.lib.utils import safe_unicode, safe_str
38 from rhodecode.lib.vcs import connection
38 from rhodecode.lib.vcs import connection
39 from rhodecode.lib.vcs.backends.base import (
39 from rhodecode.lib.vcs.backends.base import (
40 BaseRepository, CollectionGenerator, Config, MergeResponse,
40 BaseRepository, CollectionGenerator, Config, MergeResponse,
41 MergeFailureReason, Reference)
41 MergeFailureReason, Reference)
42 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
42 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
43 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
43 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
44 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
44 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
46 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
47 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError)
47 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError)
48
48
49 hexlify = binascii.hexlify
49 hexlify = binascii.hexlify
50 nullid = "\0" * 20
50 nullid = "\0" * 20
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class MercurialRepository(BaseRepository):
55 class MercurialRepository(BaseRepository):
56 """
56 """
57 Mercurial repository backend
57 Mercurial repository backend
58 """
58 """
59 DEFAULT_BRANCH_NAME = 'default'
59 DEFAULT_BRANCH_NAME = 'default'
60
60
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 update_after_clone=False, with_wire=None):
62 update_after_clone=False, with_wire=None):
63 """
63 """
64 Raises RepositoryError if repository could not be find at the given
64 Raises RepositoryError if repository could not be find at the given
65 ``repo_path``.
65 ``repo_path``.
66
66
67 :param repo_path: local path of the repository
67 :param repo_path: local path of the repository
68 :param config: config object containing the repo configuration
68 :param config: config object containing the repo configuration
69 :param create=False: if set to True, would try to create repository if
69 :param create=False: if set to True, would try to create repository if
70 it does not exist rather than raising exception
70 it does not exist rather than raising exception
71 :param src_url=None: would try to clone repository from given location
71 :param src_url=None: would try to clone repository from given location
72 :param update_after_clone=False: sets update of working copy after
72 :param update_after_clone=False: sets update of working copy after
73 making a clone
73 making a clone
74 """
74 """
75 self.path = safe_str(os.path.abspath(repo_path))
75 self.path = safe_str(os.path.abspath(repo_path))
76 self.config = config if config else Config()
76 self.config = config if config else Config()
77 self._remote = connection.Hg(
77 self._remote = connection.Hg(
78 self.path, self.config, with_wire=with_wire)
78 self.path, self.config, with_wire=with_wire)
79
79
80 self._init_repo(create, src_url, update_after_clone)
80 self._init_repo(create, src_url, update_after_clone)
81
81
82 # caches
82 # caches
83 self._commit_ids = {}
83 self._commit_ids = {}
84
84
85 @LazyProperty
85 @LazyProperty
86 def commit_ids(self):
86 def commit_ids(self):
87 """
87 """
88 Returns list of commit ids, in ascending order. Being lazy
88 Returns list of commit ids, in ascending order. Being lazy
89 attribute allows external tools to inject shas from cache.
89 attribute allows external tools to inject shas from cache.
90 """
90 """
91 commit_ids = self._get_all_commit_ids()
91 commit_ids = self._get_all_commit_ids()
92 self._rebuild_cache(commit_ids)
92 self._rebuild_cache(commit_ids)
93 return commit_ids
93 return commit_ids
94
94
95 def _rebuild_cache(self, commit_ids):
95 def _rebuild_cache(self, commit_ids):
96 self._commit_ids = dict((commit_id, index)
96 self._commit_ids = dict((commit_id, index)
97 for index, commit_id in enumerate(commit_ids))
97 for index, commit_id in enumerate(commit_ids))
98
98
99 @LazyProperty
99 @LazyProperty
100 def branches(self):
100 def branches(self):
101 return self._get_branches()
101 return self._get_branches()
102
102
103 @LazyProperty
103 @LazyProperty
104 def branches_closed(self):
104 def branches_closed(self):
105 return self._get_branches(active=False, closed=True)
105 return self._get_branches(active=False, closed=True)
106
106
107 @LazyProperty
107 @LazyProperty
108 def branches_all(self):
108 def branches_all(self):
109 all_branches = {}
109 all_branches = {}
110 all_branches.update(self.branches)
110 all_branches.update(self.branches)
111 all_branches.update(self.branches_closed)
111 all_branches.update(self.branches_closed)
112 return all_branches
112 return all_branches
113
113
114 def _get_branches(self, active=True, closed=False):
114 def _get_branches(self, active=True, closed=False):
115 """
115 """
116 Gets branches for this repository
116 Gets branches for this repository
117 Returns only not closed active branches by default
117 Returns only not closed active branches by default
118
118
119 :param active: return also active branches
119 :param active: return also active branches
120 :param closed: return also closed branches
120 :param closed: return also closed branches
121
121
122 """
122 """
123 if self.is_empty():
123 if self.is_empty():
124 return {}
124 return {}
125
125
126 def get_name(ctx):
126 def get_name(ctx):
127 return ctx[0]
127 return ctx[0]
128
128
129 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
129 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
130 self._remote.branches(active, closed).items()]
130 self._remote.branches(active, closed).items()]
131
131
132 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
132 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
133
133
134 @LazyProperty
134 @LazyProperty
135 def tags(self):
135 def tags(self):
136 """
136 """
137 Gets tags for this repository
137 Gets tags for this repository
138 """
138 """
139 return self._get_tags()
139 return self._get_tags()
140
140
141 def _get_tags(self):
141 def _get_tags(self):
142 if self.is_empty():
142 if self.is_empty():
143 return {}
143 return {}
144
144
145 def get_name(ctx):
145 def get_name(ctx):
146 return ctx[0]
146 return ctx[0]
147
147
148 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
148 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
149 self._remote.tags().items()]
149 self._remote.tags().items()]
150
150
151 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
151 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
152
152
153 def tag(self, name, user, commit_id=None, message=None, date=None,
153 def tag(self, name, user, commit_id=None, message=None, date=None,
154 **kwargs):
154 **kwargs):
155 """
155 """
156 Creates and returns a tag for the given ``commit_id``.
156 Creates and returns a tag for the given ``commit_id``.
157
157
158 :param name: name for new tag
158 :param name: name for new tag
159 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
159 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
160 :param commit_id: commit id for which new tag would be created
160 :param commit_id: commit id for which new tag would be created
161 :param message: message of the tag's commit
161 :param message: message of the tag's commit
162 :param date: date of tag's commit
162 :param date: date of tag's commit
163
163
164 :raises TagAlreadyExistError: if tag with same name already exists
164 :raises TagAlreadyExistError: if tag with same name already exists
165 """
165 """
166 if name in self.tags:
166 if name in self.tags:
167 raise TagAlreadyExistError("Tag %s already exists" % name)
167 raise TagAlreadyExistError("Tag %s already exists" % name)
168 commit = self.get_commit(commit_id=commit_id)
168 commit = self.get_commit(commit_id=commit_id)
169 local = kwargs.setdefault('local', False)
169 local = kwargs.setdefault('local', False)
170
170
171 if message is None:
171 if message is None:
172 message = "Added tag %s for commit %s" % (name, commit.short_id)
172 message = "Added tag %s for commit %s" % (name, commit.short_id)
173
173
174 date, tz = date_to_timestamp_plus_offset(date)
174 date, tz = date_to_timestamp_plus_offset(date)
175
175
176 self._remote.tag(
176 self._remote.tag(
177 name, commit.raw_id, message, local, user, date, tz)
177 name, commit.raw_id, message, local, user, date, tz)
178 self._remote.invalidate_vcs_cache()
178 self._remote.invalidate_vcs_cache()
179
179
180 # Reinitialize tags
180 # Reinitialize tags
181 self.tags = self._get_tags()
181 self.tags = self._get_tags()
182 tag_id = self.tags[name]
182 tag_id = self.tags[name]
183
183
184 return self.get_commit(commit_id=tag_id)
184 return self.get_commit(commit_id=tag_id)
185
185
186 def remove_tag(self, name, user, message=None, date=None):
186 def remove_tag(self, name, user, message=None, date=None):
187 """
187 """
188 Removes tag with the given `name`.
188 Removes tag with the given `name`.
189
189
190 :param name: name of the tag to be removed
190 :param name: name of the tag to be removed
191 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
191 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
192 :param message: message of the tag's removal commit
192 :param message: message of the tag's removal commit
193 :param date: date of tag's removal commit
193 :param date: date of tag's removal commit
194
194
195 :raises TagDoesNotExistError: if tag with given name does not exists
195 :raises TagDoesNotExistError: if tag with given name does not exists
196 """
196 """
197 if name not in self.tags:
197 if name not in self.tags:
198 raise TagDoesNotExistError("Tag %s does not exist" % name)
198 raise TagDoesNotExistError("Tag %s does not exist" % name)
199 if message is None:
199 if message is None:
200 message = "Removed tag %s" % name
200 message = "Removed tag %s" % name
201 local = False
201 local = False
202
202
203 date, tz = date_to_timestamp_plus_offset(date)
203 date, tz = date_to_timestamp_plus_offset(date)
204
204
205 self._remote.tag(name, nullid, message, local, user, date, tz)
205 self._remote.tag(name, nullid, message, local, user, date, tz)
206 self._remote.invalidate_vcs_cache()
206 self._remote.invalidate_vcs_cache()
207 self.tags = self._get_tags()
207 self.tags = self._get_tags()
208
208
209 @LazyProperty
209 @LazyProperty
210 def bookmarks(self):
210 def bookmarks(self):
211 """
211 """
212 Gets bookmarks for this repository
212 Gets bookmarks for this repository
213 """
213 """
214 return self._get_bookmarks()
214 return self._get_bookmarks()
215
215
216 def _get_bookmarks(self):
216 def _get_bookmarks(self):
217 if self.is_empty():
217 if self.is_empty():
218 return {}
218 return {}
219
219
220 def get_name(ctx):
220 def get_name(ctx):
221 return ctx[0]
221 return ctx[0]
222
222
223 _bookmarks = [
223 _bookmarks = [
224 (safe_unicode(n), hexlify(h)) for n, h in
224 (safe_unicode(n), hexlify(h)) for n, h in
225 self._remote.bookmarks().items()]
225 self._remote.bookmarks().items()]
226
226
227 return OrderedDict(sorted(_bookmarks, key=get_name))
227 return OrderedDict(sorted(_bookmarks, key=get_name))
228
228
229 def _get_all_commit_ids(self):
229 def _get_all_commit_ids(self):
230 return self._remote.get_all_commit_ids('visible')
230 return self._remote.get_all_commit_ids('visible')
231
231
232 def get_diff(
232 def get_diff(
233 self, commit1, commit2, path='', ignore_whitespace=False,
233 self, commit1, commit2, path='', ignore_whitespace=False,
234 context=3, path1=None):
234 context=3, path1=None):
235 """
235 """
236 Returns (git like) *diff*, as plain text. Shows changes introduced by
236 Returns (git like) *diff*, as plain text. Shows changes introduced by
237 `commit2` since `commit1`.
237 `commit2` since `commit1`.
238
238
239 :param commit1: Entry point from which diff is shown. Can be
239 :param commit1: Entry point from which diff is shown. Can be
240 ``self.EMPTY_COMMIT`` - in this case, patch showing all
240 ``self.EMPTY_COMMIT`` - in this case, patch showing all
241 the changes since empty state of the repository until `commit2`
241 the changes since empty state of the repository until `commit2`
242 :param commit2: Until which commit changes should be shown.
242 :param commit2: Until which commit changes should be shown.
243 :param ignore_whitespace: If set to ``True``, would not show whitespace
243 :param ignore_whitespace: If set to ``True``, would not show whitespace
244 changes. Defaults to ``False``.
244 changes. Defaults to ``False``.
245 :param context: How many lines before/after changed lines should be
245 :param context: How many lines before/after changed lines should be
246 shown. Defaults to ``3``.
246 shown. Defaults to ``3``.
247 """
247 """
248 self._validate_diff_commits(commit1, commit2)
248 self._validate_diff_commits(commit1, commit2)
249 if path1 is not None and path1 != path:
249 if path1 is not None and path1 != path:
250 raise ValueError("Diff of two different paths not supported.")
250 raise ValueError("Diff of two different paths not supported.")
251
251
252 if path:
252 if path:
253 file_filter = [self.path, path]
253 file_filter = [self.path, path]
254 else:
254 else:
255 file_filter = None
255 file_filter = None
256
256
257 diff = self._remote.diff(
257 diff = self._remote.diff(
258 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
258 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
259 opt_git=True, opt_ignorews=ignore_whitespace,
259 opt_git=True, opt_ignorews=ignore_whitespace,
260 context=context)
260 context=context)
261 return MercurialDiff(diff)
261 return MercurialDiff(diff)
262
262
263 def strip(self, commit_id, branch=None):
263 def strip(self, commit_id, branch=None):
264 self._remote.strip(commit_id, update=False, backup="none")
264 self._remote.strip(commit_id, update=False, backup="none")
265
265
266 self._remote.invalidate_vcs_cache()
266 self._remote.invalidate_vcs_cache()
267 self.commit_ids = self._get_all_commit_ids()
267 self.commit_ids = self._get_all_commit_ids()
268 self._rebuild_cache(self.commit_ids)
268 self._rebuild_cache(self.commit_ids)
269
269
270 def verify(self):
270 def verify(self):
271 verify = self._remote.verify()
271 verify = self._remote.verify()
272
272
273 self._remote.invalidate_vcs_cache()
273 self._remote.invalidate_vcs_cache()
274 return verify
274 return verify
275
275
276 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
276 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
277 if commit_id1 == commit_id2:
277 if commit_id1 == commit_id2:
278 return commit_id1
278 return commit_id1
279
279
280 ancestors = self._remote.revs_from_revspec(
280 ancestors = self._remote.revs_from_revspec(
281 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
281 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
282 other_path=repo2.path)
282 other_path=repo2.path)
283 return repo2[ancestors[0]].raw_id if ancestors else None
283 return repo2[ancestors[0]].raw_id if ancestors else None
284
284
285 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
285 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
286 if commit_id1 == commit_id2:
286 if commit_id1 == commit_id2:
287 commits = []
287 commits = []
288 else:
288 else:
289 if merge:
289 if merge:
290 indexes = self._remote.revs_from_revspec(
290 indexes = self._remote.revs_from_revspec(
291 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
291 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
292 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
292 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
293 else:
293 else:
294 indexes = self._remote.revs_from_revspec(
294 indexes = self._remote.revs_from_revspec(
295 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
295 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
296 commit_id1, other_path=repo2.path)
296 commit_id1, other_path=repo2.path)
297
297
298 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
298 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
299 for idx in indexes]
299 for idx in indexes]
300
300
301 return commits
301 return commits
302
302
303 @staticmethod
303 @staticmethod
304 def check_url(url, config):
304 def check_url(url, config):
305 """
305 """
306 Function will check given url and try to verify if it's a valid
306 Function will check given url and try to verify if it's a valid
307 link. Sometimes it may happened that mercurial will issue basic
307 link. Sometimes it may happened that mercurial will issue basic
308 auth request that can cause whole API to hang when used from python
308 auth request that can cause whole API to hang when used from python
309 or other external calls.
309 or other external calls.
310
310
311 On failures it'll raise urllib2.HTTPError, exception is also thrown
311 On failures it'll raise urllib2.HTTPError, exception is also thrown
312 when the return code is non 200
312 when the return code is non 200
313 """
313 """
314 # check first if it's not an local url
314 # check first if it's not an local url
315 if os.path.isdir(url) or url.startswith('file:'):
315 if os.path.isdir(url) or url.startswith('file:'):
316 return True
316 return True
317
317
318 # Request the _remote to verify the url
318 # Request the _remote to verify the url
319 return connection.Hg.check_url(url, config.serialize())
319 return connection.Hg.check_url(url, config.serialize())
320
320
321 @staticmethod
321 @staticmethod
322 def is_valid_repository(path):
322 def is_valid_repository(path):
323 return os.path.isdir(os.path.join(path, '.hg'))
323 return os.path.isdir(os.path.join(path, '.hg'))
324
324
325 def _init_repo(self, create, src_url=None, update_after_clone=False):
325 def _init_repo(self, create, src_url=None, update_after_clone=False):
326 """
326 """
327 Function will check for mercurial repository in given path. If there
327 Function will check for mercurial repository in given path. If there
328 is no repository in that path it will raise an exception unless
328 is no repository in that path it will raise an exception unless
329 `create` parameter is set to True - in that case repository would
329 `create` parameter is set to True - in that case repository would
330 be created.
330 be created.
331
331
332 If `src_url` is given, would try to clone repository from the
332 If `src_url` is given, would try to clone repository from the
333 location at given clone_point. Additionally it'll make update to
333 location at given clone_point. Additionally it'll make update to
334 working copy accordingly to `update_after_clone` flag.
334 working copy accordingly to `update_after_clone` flag.
335 """
335 """
336 if create and os.path.exists(self.path):
336 if create and os.path.exists(self.path):
337 raise RepositoryError(
337 raise RepositoryError(
338 "Cannot create repository at %s, location already exist"
338 "Cannot create repository at %s, location already exist"
339 % self.path)
339 % self.path)
340
340
341 if src_url:
341 if src_url:
342 url = str(self._get_url(src_url))
342 url = str(self._get_url(src_url))
343 MercurialRepository.check_url(url, self.config)
343 MercurialRepository.check_url(url, self.config)
344
344
345 self._remote.clone(url, self.path, update_after_clone)
345 self._remote.clone(url, self.path, update_after_clone)
346
346
347 # Don't try to create if we've already cloned repo
347 # Don't try to create if we've already cloned repo
348 create = False
348 create = False
349
349
350 if create:
350 if create:
351 os.makedirs(self.path, mode=0755)
351 os.makedirs(self.path, mode=0755)
352
352
353 self._remote.localrepository(create)
353 self._remote.localrepository(create)
354
354
355 @LazyProperty
355 @LazyProperty
356 def in_memory_commit(self):
356 def in_memory_commit(self):
357 return MercurialInMemoryCommit(self)
357 return MercurialInMemoryCommit(self)
358
358
359 @LazyProperty
359 @LazyProperty
360 def description(self):
360 def description(self):
361 description = self._remote.get_config_value(
361 description = self._remote.get_config_value(
362 'web', 'description', untrusted=True)
362 'web', 'description', untrusted=True)
363 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
363 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
364
364
365 @LazyProperty
365 @LazyProperty
366 def contact(self):
366 def contact(self):
367 contact = (
367 contact = (
368 self._remote.get_config_value("web", "contact") or
368 self._remote.get_config_value("web", "contact") or
369 self._remote.get_config_value("ui", "username"))
369 self._remote.get_config_value("ui", "username"))
370 return safe_unicode(contact or self.DEFAULT_CONTACT)
370 return safe_unicode(contact or self.DEFAULT_CONTACT)
371
371
372 @LazyProperty
372 @LazyProperty
373 def last_change(self):
373 def last_change(self):
374 """
374 """
375 Returns last change made on this repository as
375 Returns last change made on this repository as
376 `datetime.datetime` object.
376 `datetime.datetime` object.
377 """
377 """
378 try:
378 try:
379 return self.get_commit().date
379 return self.get_commit().date
380 except RepositoryError:
380 except RepositoryError:
381 tzoffset = makedate()[1]
381 tzoffset = makedate()[1]
382 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
382 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
383
383
384 def _get_fs_mtime(self):
384 def _get_fs_mtime(self):
385 # fallback to filesystem
385 # fallback to filesystem
386 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
386 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
387 st_path = os.path.join(self.path, '.hg', "store")
387 st_path = os.path.join(self.path, '.hg', "store")
388 if os.path.exists(cl_path):
388 if os.path.exists(cl_path):
389 return os.stat(cl_path).st_mtime
389 return os.stat(cl_path).st_mtime
390 else:
390 else:
391 return os.stat(st_path).st_mtime
391 return os.stat(st_path).st_mtime
392
392
393 def _sanitize_commit_idx(self, idx):
393 def _sanitize_commit_idx(self, idx):
394 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
394 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
395 # number. A `long` is treated in the correct way though. So we convert
395 # number. A `long` is treated in the correct way though. So we convert
396 # `int` to `long` here to make sure it is handled correctly.
396 # `int` to `long` here to make sure it is handled correctly.
397 if isinstance(idx, int):
397 if isinstance(idx, int):
398 return long(idx)
398 return long(idx)
399 return idx
399 return idx
400
400
401 def _get_url(self, url):
401 def _get_url(self, url):
402 """
402 """
403 Returns normalized url. If schema is not given, would fall
403 Returns normalized url. If schema is not given, would fall
404 to filesystem
404 to filesystem
405 (``file:///``) schema.
405 (``file:///``) schema.
406 """
406 """
407 url = url.encode('utf8')
407 url = url.encode('utf8')
408 if url != 'default' and '://' not in url:
408 if url != 'default' and '://' not in url:
409 url = "file:" + urllib.pathname2url(url)
409 url = "file:" + urllib.pathname2url(url)
410 return url
410 return url
411
411
412 def get_hook_location(self):
412 def get_hook_location(self):
413 """
413 """
414 returns absolute path to location where hooks are stored
414 returns absolute path to location where hooks are stored
415 """
415 """
416 return os.path.join(self.path, '.hg', '.hgrc')
416 return os.path.join(self.path, '.hg', '.hgrc')
417
417
418 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
418 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
419 """
419 """
420 Returns ``MercurialCommit`` object representing repository's
420 Returns ``MercurialCommit`` object representing repository's
421 commit at the given `commit_id` or `commit_idx`.
421 commit at the given `commit_id` or `commit_idx`.
422 """
422 """
423 if self.is_empty():
423 if self.is_empty():
424 raise EmptyRepositoryError("There are no commits yet")
424 raise EmptyRepositoryError("There are no commits yet")
425
425
426 if commit_id is not None:
426 if commit_id is not None:
427 self._validate_commit_id(commit_id)
427 self._validate_commit_id(commit_id)
428 try:
428 try:
429 idx = self._commit_ids[commit_id]
429 idx = self._commit_ids[commit_id]
430 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
430 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
431 except KeyError:
431 except KeyError:
432 pass
432 pass
433 elif commit_idx is not None:
433 elif commit_idx is not None:
434 self._validate_commit_idx(commit_idx)
434 self._validate_commit_idx(commit_idx)
435 commit_idx = self._sanitize_commit_idx(commit_idx)
435 commit_idx = self._sanitize_commit_idx(commit_idx)
436 try:
436 try:
437 id_ = self.commit_ids[commit_idx]
437 id_ = self.commit_ids[commit_idx]
438 if commit_idx < 0:
438 if commit_idx < 0:
439 commit_idx += len(self.commit_ids)
439 commit_idx += len(self.commit_ids)
440 return MercurialCommit(
440 return MercurialCommit(
441 self, id_, commit_idx, pre_load=pre_load)
441 self, id_, commit_idx, pre_load=pre_load)
442 except IndexError:
442 except IndexError:
443 commit_id = commit_idx
443 commit_id = commit_idx
444 else:
444 else:
445 commit_id = "tip"
445 commit_id = "tip"
446
446
447 # TODO Paris: Ugly hack to "serialize" long for msgpack
447 # TODO Paris: Ugly hack to "serialize" long for msgpack
448 if isinstance(commit_id, long):
448 if isinstance(commit_id, long):
449 commit_id = float(commit_id)
449 commit_id = float(commit_id)
450
450
451 if isinstance(commit_id, unicode):
451 if isinstance(commit_id, unicode):
452 commit_id = safe_str(commit_id)
452 commit_id = safe_str(commit_id)
453
453
454 try:
454 try:
455 raw_id, idx = self._remote.lookup(commit_id, both=True)
455 raw_id, idx = self._remote.lookup(commit_id, both=True)
456 except CommitDoesNotExistError:
456 except CommitDoesNotExistError:
457 msg = "Commit %s does not exist for %s" % (
457 msg = "Commit %s does not exist for %s" % (
458 commit_id, self)
458 commit_id, self)
459 raise CommitDoesNotExistError(msg)
459 raise CommitDoesNotExistError(msg)
460
460
461 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
461 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
462
462
463 def get_commits(
463 def get_commits(
464 self, start_id=None, end_id=None, start_date=None, end_date=None,
464 self, start_id=None, end_id=None, start_date=None, end_date=None,
465 branch_name=None, pre_load=None):
465 branch_name=None, show_hidden=False, pre_load=None):
466 """
466 """
467 Returns generator of ``MercurialCommit`` objects from start to end
467 Returns generator of ``MercurialCommit`` objects from start to end
468 (both are inclusive)
468 (both are inclusive)
469
469
470 :param start_id: None, str(commit_id)
470 :param start_id: None, str(commit_id)
471 :param end_id: None, str(commit_id)
471 :param end_id: None, str(commit_id)
472 :param start_date: if specified, commits with commit date less than
472 :param start_date: if specified, commits with commit date less than
473 ``start_date`` would be filtered out from returned set
473 ``start_date`` would be filtered out from returned set
474 :param end_date: if specified, commits with commit date greater than
474 :param end_date: if specified, commits with commit date greater than
475 ``end_date`` would be filtered out from returned set
475 ``end_date`` would be filtered out from returned set
476 :param branch_name: if specified, commits not reachable from given
476 :param branch_name: if specified, commits not reachable from given
477 branch would be filtered out from returned set
477 branch would be filtered out from returned set
478
478 :param show_hidden: Show hidden commits such as obsolete or hidden from
479 Mercurial evolve
479 :raise BranchDoesNotExistError: If given ``branch_name`` does not
480 :raise BranchDoesNotExistError: If given ``branch_name`` does not
480 exist.
481 exist.
481 :raise CommitDoesNotExistError: If commit for given ``start`` or
482 :raise CommitDoesNotExistError: If commit for given ``start`` or
482 ``end`` could not be found.
483 ``end`` could not be found.
483 """
484 """
484 # actually we should check now if it's not an empty repo
485 # actually we should check now if it's not an empty repo
485 branch_ancestors = False
486 branch_ancestors = False
486 if self.is_empty():
487 if self.is_empty():
487 raise EmptyRepositoryError("There are no commits yet")
488 raise EmptyRepositoryError("There are no commits yet")
488 self._validate_branch_name(branch_name)
489 self._validate_branch_name(branch_name)
489
490
490 if start_id is not None:
491 if start_id is not None:
491 self._validate_commit_id(start_id)
492 self._validate_commit_id(start_id)
492 c_start = self.get_commit(commit_id=start_id)
493 c_start = self.get_commit(commit_id=start_id)
493 start_pos = self._commit_ids[c_start.raw_id]
494 start_pos = self._commit_ids[c_start.raw_id]
494 else:
495 else:
495 start_pos = None
496 start_pos = None
496
497
497 if end_id is not None:
498 if end_id is not None:
498 self._validate_commit_id(end_id)
499 self._validate_commit_id(end_id)
499 c_end = self.get_commit(commit_id=end_id)
500 c_end = self.get_commit(commit_id=end_id)
500 end_pos = max(0, self._commit_ids[c_end.raw_id])
501 end_pos = max(0, self._commit_ids[c_end.raw_id])
501 else:
502 else:
502 end_pos = None
503 end_pos = None
503
504
504 if None not in [start_id, end_id] and start_pos > end_pos:
505 if None not in [start_id, end_id] and start_pos > end_pos:
505 raise RepositoryError(
506 raise RepositoryError(
506 "Start commit '%s' cannot be after end commit '%s'" %
507 "Start commit '%s' cannot be after end commit '%s'" %
507 (start_id, end_id))
508 (start_id, end_id))
508
509
509 if end_pos is not None:
510 if end_pos is not None:
510 end_pos += 1
511 end_pos += 1
511
512
512 commit_filter = []
513 commit_filter = []
514
513 if branch_name and not branch_ancestors:
515 if branch_name and not branch_ancestors:
514 commit_filter.append('branch("%s")' % branch_name)
516 commit_filter.append('branch("%s")' % (branch_name,))
515 elif branch_name and branch_ancestors:
517 elif branch_name and branch_ancestors:
516 commit_filter.append('ancestors(branch("%s"))' % branch_name)
518 commit_filter.append('ancestors(branch("%s"))' % (branch_name,))
519
517 if start_date and not end_date:
520 if start_date and not end_date:
518 commit_filter.append('date(">%s")' % start_date)
521 commit_filter.append('date(">%s")' % (start_date,))
519 if end_date and not start_date:
522 if end_date and not start_date:
520 commit_filter.append('date("<%s")' % end_date)
523 commit_filter.append('date("<%s")' % (end_date,))
521 if start_date and end_date:
524 if start_date and end_date:
522 commit_filter.append(
525 commit_filter.append(
523 'date(">%s") and date("<%s")' % (start_date, end_date))
526 'date(">%s") and date("<%s")' % (start_date, end_date))
524
527
528 if not show_hidden:
529 commit_filter.append('not obsolete()')
530 commit_filter.append('not hidden()')
531
525 # TODO: johbo: Figure out a simpler way for this solution
532 # TODO: johbo: Figure out a simpler way for this solution
526 collection_generator = CollectionGenerator
533 collection_generator = CollectionGenerator
527 if commit_filter:
534 if commit_filter:
528 commit_filter = map(safe_str, commit_filter)
535 commit_filter = ' and '.join(map(safe_str, commit_filter))
529 revisions = self._remote.rev_range(commit_filter)
536 revisions = self._remote.rev_range([commit_filter])
530 collection_generator = MercurialIndexBasedCollectionGenerator
537 collection_generator = MercurialIndexBasedCollectionGenerator
531 else:
538 else:
532 revisions = self.commit_ids
539 revisions = self.commit_ids
533
540
534 if start_pos or end_pos:
541 if start_pos or end_pos:
535 revisions = revisions[start_pos:end_pos]
542 revisions = revisions[start_pos:end_pos]
536
543
537 return collection_generator(self, revisions, pre_load=pre_load)
544 return collection_generator(self, revisions, pre_load=pre_load)
538
545
539 def pull(self, url, commit_ids=None):
546 def pull(self, url, commit_ids=None):
540 """
547 """
541 Tries to pull changes from external location.
548 Tries to pull changes from external location.
542
549
543 :param commit_ids: Optional. Can be set to a list of commit ids
550 :param commit_ids: Optional. Can be set to a list of commit ids
544 which shall be pulled from the other repository.
551 which shall be pulled from the other repository.
545 """
552 """
546 url = self._get_url(url)
553 url = self._get_url(url)
547 self._remote.pull(url, commit_ids=commit_ids)
554 self._remote.pull(url, commit_ids=commit_ids)
548 self._remote.invalidate_vcs_cache()
555 self._remote.invalidate_vcs_cache()
549
556
550 def _local_clone(self, clone_path):
557 def _local_clone(self, clone_path):
551 """
558 """
552 Create a local clone of the current repo.
559 Create a local clone of the current repo.
553 """
560 """
554 self._remote.clone(self.path, clone_path, update_after_clone=True,
561 self._remote.clone(self.path, clone_path, update_after_clone=True,
555 hooks=False)
562 hooks=False)
556
563
557 def _update(self, revision, clean=False):
564 def _update(self, revision, clean=False):
558 """
565 """
559 Update the working copy to the specified revision.
566 Update the working copy to the specified revision.
560 """
567 """
561 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
568 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
562 self._remote.update(revision, clean=clean)
569 self._remote.update(revision, clean=clean)
563
570
564 def _identify(self):
571 def _identify(self):
565 """
572 """
566 Return the current state of the working directory.
573 Return the current state of the working directory.
567 """
574 """
568 return self._remote.identify().strip().rstrip('+')
575 return self._remote.identify().strip().rstrip('+')
569
576
570 def _heads(self, branch=None):
577 def _heads(self, branch=None):
571 """
578 """
572 Return the commit ids of the repository heads.
579 Return the commit ids of the repository heads.
573 """
580 """
574 return self._remote.heads(branch=branch).strip().split(' ')
581 return self._remote.heads(branch=branch).strip().split(' ')
575
582
576 def _ancestor(self, revision1, revision2):
583 def _ancestor(self, revision1, revision2):
577 """
584 """
578 Return the common ancestor of the two revisions.
585 Return the common ancestor of the two revisions.
579 """
586 """
580 return self._remote.ancestor(revision1, revision2)
587 return self._remote.ancestor(revision1, revision2)
581
588
582 def _local_push(
589 def _local_push(
583 self, revision, repository_path, push_branches=False,
590 self, revision, repository_path, push_branches=False,
584 enable_hooks=False):
591 enable_hooks=False):
585 """
592 """
586 Push the given revision to the specified repository.
593 Push the given revision to the specified repository.
587
594
588 :param push_branches: allow to create branches in the target repo.
595 :param push_branches: allow to create branches in the target repo.
589 """
596 """
590 self._remote.push(
597 self._remote.push(
591 [revision], repository_path, hooks=enable_hooks,
598 [revision], repository_path, hooks=enable_hooks,
592 push_branches=push_branches)
599 push_branches=push_branches)
593
600
594 def _local_merge(self, target_ref, merge_message, user_name, user_email,
601 def _local_merge(self, target_ref, merge_message, user_name, user_email,
595 source_ref, use_rebase=False, dry_run=False):
602 source_ref, use_rebase=False, dry_run=False):
596 """
603 """
597 Merge the given source_revision into the checked out revision.
604 Merge the given source_revision into the checked out revision.
598
605
599 Returns the commit id of the merge and a boolean indicating if the
606 Returns the commit id of the merge and a boolean indicating if the
600 commit needs to be pushed.
607 commit needs to be pushed.
601 """
608 """
602 self._update(target_ref.commit_id)
609 self._update(target_ref.commit_id)
603
610
604 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
611 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
605 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
612 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
606
613
607 if ancestor == source_ref.commit_id:
614 if ancestor == source_ref.commit_id:
608 # Nothing to do, the changes were already integrated
615 # Nothing to do, the changes were already integrated
609 return target_ref.commit_id, False
616 return target_ref.commit_id, False
610
617
611 elif ancestor == target_ref.commit_id and is_the_same_branch:
618 elif ancestor == target_ref.commit_id and is_the_same_branch:
612 # In this case we should force a commit message
619 # In this case we should force a commit message
613 return source_ref.commit_id, True
620 return source_ref.commit_id, True
614
621
615 if use_rebase:
622 if use_rebase:
616 try:
623 try:
617 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
624 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
618 target_ref.commit_id)
625 target_ref.commit_id)
619 self.bookmark(bookmark_name, revision=source_ref.commit_id)
626 self.bookmark(bookmark_name, revision=source_ref.commit_id)
620 self._remote.rebase(
627 self._remote.rebase(
621 source=source_ref.commit_id, dest=target_ref.commit_id)
628 source=source_ref.commit_id, dest=target_ref.commit_id)
622 self._remote.invalidate_vcs_cache()
629 self._remote.invalidate_vcs_cache()
623 self._update(bookmark_name)
630 self._update(bookmark_name)
624 return self._identify(), True
631 return self._identify(), True
625 except RepositoryError:
632 except RepositoryError:
626 # The rebase-abort may raise another exception which 'hides'
633 # The rebase-abort may raise another exception which 'hides'
627 # the original one, therefore we log it here.
634 # the original one, therefore we log it here.
628 log.exception('Error while rebasing shadow repo during merge.')
635 log.exception('Error while rebasing shadow repo during merge.')
629
636
630 # Cleanup any rebase leftovers
637 # Cleanup any rebase leftovers
631 self._remote.invalidate_vcs_cache()
638 self._remote.invalidate_vcs_cache()
632 self._remote.rebase(abort=True)
639 self._remote.rebase(abort=True)
633 self._remote.invalidate_vcs_cache()
640 self._remote.invalidate_vcs_cache()
634 self._remote.update(clean=True)
641 self._remote.update(clean=True)
635 raise
642 raise
636 else:
643 else:
637 try:
644 try:
638 self._remote.merge(source_ref.commit_id)
645 self._remote.merge(source_ref.commit_id)
639 self._remote.invalidate_vcs_cache()
646 self._remote.invalidate_vcs_cache()
640 self._remote.commit(
647 self._remote.commit(
641 message=safe_str(merge_message),
648 message=safe_str(merge_message),
642 username=safe_str('%s <%s>' % (user_name, user_email)))
649 username=safe_str('%s <%s>' % (user_name, user_email)))
643 self._remote.invalidate_vcs_cache()
650 self._remote.invalidate_vcs_cache()
644 return self._identify(), True
651 return self._identify(), True
645 except RepositoryError:
652 except RepositoryError:
646 # Cleanup any merge leftovers
653 # Cleanup any merge leftovers
647 self._remote.update(clean=True)
654 self._remote.update(clean=True)
648 raise
655 raise
649
656
650 def _local_close(self, target_ref, user_name, user_email,
657 def _local_close(self, target_ref, user_name, user_email,
651 source_ref, close_message=''):
658 source_ref, close_message=''):
652 """
659 """
653 Close the branch of the given source_revision
660 Close the branch of the given source_revision
654
661
655 Returns the commit id of the close and a boolean indicating if the
662 Returns the commit id of the close and a boolean indicating if the
656 commit needs to be pushed.
663 commit needs to be pushed.
657 """
664 """
658 self._update(source_ref.commit_id)
665 self._update(source_ref.commit_id)
659 message = close_message or "Closing branch: `{}`".format(source_ref.name)
666 message = close_message or "Closing branch: `{}`".format(source_ref.name)
660 try:
667 try:
661 self._remote.commit(
668 self._remote.commit(
662 message=safe_str(message),
669 message=safe_str(message),
663 username=safe_str('%s <%s>' % (user_name, user_email)),
670 username=safe_str('%s <%s>' % (user_name, user_email)),
664 close_branch=True)
671 close_branch=True)
665 self._remote.invalidate_vcs_cache()
672 self._remote.invalidate_vcs_cache()
666 return self._identify(), True
673 return self._identify(), True
667 except RepositoryError:
674 except RepositoryError:
668 # Cleanup any commit leftovers
675 # Cleanup any commit leftovers
669 self._remote.update(clean=True)
676 self._remote.update(clean=True)
670 raise
677 raise
671
678
672 def _is_the_same_branch(self, target_ref, source_ref):
679 def _is_the_same_branch(self, target_ref, source_ref):
673 return (
680 return (
674 self._get_branch_name(target_ref) ==
681 self._get_branch_name(target_ref) ==
675 self._get_branch_name(source_ref))
682 self._get_branch_name(source_ref))
676
683
677 def _get_branch_name(self, ref):
684 def _get_branch_name(self, ref):
678 if ref.type == 'branch':
685 if ref.type == 'branch':
679 return ref.name
686 return ref.name
680 return self._remote.ctx_branch(ref.commit_id)
687 return self._remote.ctx_branch(ref.commit_id)
681
688
682 def _get_shadow_repository_path(self, workspace_id):
689 def _get_shadow_repository_path(self, workspace_id):
683 # The name of the shadow repository must start with '.', so it is
690 # The name of the shadow repository must start with '.', so it is
684 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
691 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
685 return os.path.join(
692 return os.path.join(
686 os.path.dirname(self.path),
693 os.path.dirname(self.path),
687 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
694 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
688
695
689 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
696 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
690 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
697 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
691 if not os.path.exists(shadow_repository_path):
698 if not os.path.exists(shadow_repository_path):
692 self._local_clone(shadow_repository_path)
699 self._local_clone(shadow_repository_path)
693 log.debug(
700 log.debug(
694 'Prepared shadow repository in %s', shadow_repository_path)
701 'Prepared shadow repository in %s', shadow_repository_path)
695
702
696 return shadow_repository_path
703 return shadow_repository_path
697
704
698 def cleanup_merge_workspace(self, workspace_id):
705 def cleanup_merge_workspace(self, workspace_id):
699 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
706 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
700 shutil.rmtree(shadow_repository_path, ignore_errors=True)
707 shutil.rmtree(shadow_repository_path, ignore_errors=True)
701
708
702 def _merge_repo(self, shadow_repository_path, target_ref,
709 def _merge_repo(self, shadow_repository_path, target_ref,
703 source_repo, source_ref, merge_message,
710 source_repo, source_ref, merge_message,
704 merger_name, merger_email, dry_run=False,
711 merger_name, merger_email, dry_run=False,
705 use_rebase=False, close_branch=False):
712 use_rebase=False, close_branch=False):
706
713
707 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
714 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
708 'rebase' if use_rebase else 'merge', dry_run)
715 'rebase' if use_rebase else 'merge', dry_run)
709 if target_ref.commit_id not in self._heads():
716 if target_ref.commit_id not in self._heads():
710 return MergeResponse(
717 return MergeResponse(
711 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
718 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
712
719
713 try:
720 try:
714 if (target_ref.type == 'branch' and
721 if (target_ref.type == 'branch' and
715 len(self._heads(target_ref.name)) != 1):
722 len(self._heads(target_ref.name)) != 1):
716 return MergeResponse(
723 return MergeResponse(
717 False, False, None,
724 False, False, None,
718 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
725 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
719 except CommitDoesNotExistError:
726 except CommitDoesNotExistError:
720 log.exception('Failure when looking up branch heads on hg target')
727 log.exception('Failure when looking up branch heads on hg target')
721 return MergeResponse(
728 return MergeResponse(
722 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
729 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
723
730
724 shadow_repo = self._get_shadow_instance(shadow_repository_path)
731 shadow_repo = self._get_shadow_instance(shadow_repository_path)
725
732
726 log.debug('Pulling in target reference %s', target_ref)
733 log.debug('Pulling in target reference %s', target_ref)
727 self._validate_pull_reference(target_ref)
734 self._validate_pull_reference(target_ref)
728 shadow_repo._local_pull(self.path, target_ref)
735 shadow_repo._local_pull(self.path, target_ref)
729 try:
736 try:
730 log.debug('Pulling in source reference %s', source_ref)
737 log.debug('Pulling in source reference %s', source_ref)
731 source_repo._validate_pull_reference(source_ref)
738 source_repo._validate_pull_reference(source_ref)
732 shadow_repo._local_pull(source_repo.path, source_ref)
739 shadow_repo._local_pull(source_repo.path, source_ref)
733 except CommitDoesNotExistError:
740 except CommitDoesNotExistError:
734 log.exception('Failure when doing local pull on hg shadow repo')
741 log.exception('Failure when doing local pull on hg shadow repo')
735 return MergeResponse(
742 return MergeResponse(
736 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
743 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
737
744
738 merge_ref = None
745 merge_ref = None
739 merge_commit_id = None
746 merge_commit_id = None
740 close_commit_id = None
747 close_commit_id = None
741 merge_failure_reason = MergeFailureReason.NONE
748 merge_failure_reason = MergeFailureReason.NONE
742
749
743 # enforce that close branch should be used only in case we source from
750 # enforce that close branch should be used only in case we source from
744 # an actual Branch
751 # an actual Branch
745 close_branch = close_branch and source_ref.type == 'branch'
752 close_branch = close_branch and source_ref.type == 'branch'
746
753
747 # don't allow to close branch if source and target are the same
754 # don't allow to close branch if source and target are the same
748 close_branch = close_branch and source_ref.name != target_ref.name
755 close_branch = close_branch and source_ref.name != target_ref.name
749
756
750 needs_push_on_close = False
757 needs_push_on_close = False
751 if close_branch and not use_rebase and not dry_run:
758 if close_branch and not use_rebase and not dry_run:
752 try:
759 try:
753 close_commit_id, needs_push_on_close = shadow_repo._local_close(
760 close_commit_id, needs_push_on_close = shadow_repo._local_close(
754 target_ref, merger_name, merger_email, source_ref)
761 target_ref, merger_name, merger_email, source_ref)
755 merge_possible = True
762 merge_possible = True
756 except RepositoryError:
763 except RepositoryError:
757 log.exception(
764 log.exception(
758 'Failure when doing close branch on hg shadow repo')
765 'Failure when doing close branch on hg shadow repo')
759 merge_possible = False
766 merge_possible = False
760 merge_failure_reason = MergeFailureReason.MERGE_FAILED
767 merge_failure_reason = MergeFailureReason.MERGE_FAILED
761 else:
768 else:
762 merge_possible = True
769 merge_possible = True
763
770
764 if merge_possible:
771 if merge_possible:
765 try:
772 try:
766 merge_commit_id, needs_push = shadow_repo._local_merge(
773 merge_commit_id, needs_push = shadow_repo._local_merge(
767 target_ref, merge_message, merger_name, merger_email,
774 target_ref, merge_message, merger_name, merger_email,
768 source_ref, use_rebase=use_rebase, dry_run=dry_run)
775 source_ref, use_rebase=use_rebase, dry_run=dry_run)
769 merge_possible = True
776 merge_possible = True
770
777
771 # read the state of the close action, if it
778 # read the state of the close action, if it
772 # maybe required a push
779 # maybe required a push
773 needs_push = needs_push or needs_push_on_close
780 needs_push = needs_push or needs_push_on_close
774
781
775 # Set a bookmark pointing to the merge commit. This bookmark
782 # Set a bookmark pointing to the merge commit. This bookmark
776 # may be used to easily identify the last successful merge
783 # may be used to easily identify the last successful merge
777 # commit in the shadow repository.
784 # commit in the shadow repository.
778 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
785 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
779 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
786 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
780 except SubrepoMergeError:
787 except SubrepoMergeError:
781 log.exception(
788 log.exception(
782 'Subrepo merge error during local merge on hg shadow repo.')
789 'Subrepo merge error during local merge on hg shadow repo.')
783 merge_possible = False
790 merge_possible = False
784 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
791 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
785 needs_push = False
792 needs_push = False
786 except RepositoryError:
793 except RepositoryError:
787 log.exception('Failure when doing local merge on hg shadow repo')
794 log.exception('Failure when doing local merge on hg shadow repo')
788 merge_possible = False
795 merge_possible = False
789 merge_failure_reason = MergeFailureReason.MERGE_FAILED
796 merge_failure_reason = MergeFailureReason.MERGE_FAILED
790 needs_push = False
797 needs_push = False
791
798
792 if merge_possible and not dry_run:
799 if merge_possible and not dry_run:
793 if needs_push:
800 if needs_push:
794 # In case the target is a bookmark, update it, so after pushing
801 # In case the target is a bookmark, update it, so after pushing
795 # the bookmarks is also updated in the target.
802 # the bookmarks is also updated in the target.
796 if target_ref.type == 'book':
803 if target_ref.type == 'book':
797 shadow_repo.bookmark(
804 shadow_repo.bookmark(
798 target_ref.name, revision=merge_commit_id)
805 target_ref.name, revision=merge_commit_id)
799 try:
806 try:
800 shadow_repo_with_hooks = self._get_shadow_instance(
807 shadow_repo_with_hooks = self._get_shadow_instance(
801 shadow_repository_path,
808 shadow_repository_path,
802 enable_hooks=True)
809 enable_hooks=True)
803 # This is the actual merge action, we push from shadow
810 # This is the actual merge action, we push from shadow
804 # into origin.
811 # into origin.
805 # Note: the push_branches option will push any new branch
812 # Note: the push_branches option will push any new branch
806 # defined in the source repository to the target. This may
813 # defined in the source repository to the target. This may
807 # be dangerous as branches are permanent in Mercurial.
814 # be dangerous as branches are permanent in Mercurial.
808 # This feature was requested in issue #441.
815 # This feature was requested in issue #441.
809 shadow_repo_with_hooks._local_push(
816 shadow_repo_with_hooks._local_push(
810 merge_commit_id, self.path, push_branches=True,
817 merge_commit_id, self.path, push_branches=True,
811 enable_hooks=True)
818 enable_hooks=True)
812
819
813 # maybe we also need to push the close_commit_id
820 # maybe we also need to push the close_commit_id
814 if close_commit_id:
821 if close_commit_id:
815 shadow_repo_with_hooks._local_push(
822 shadow_repo_with_hooks._local_push(
816 close_commit_id, self.path, push_branches=True,
823 close_commit_id, self.path, push_branches=True,
817 enable_hooks=True)
824 enable_hooks=True)
818 merge_succeeded = True
825 merge_succeeded = True
819 except RepositoryError:
826 except RepositoryError:
820 log.exception(
827 log.exception(
821 'Failure when doing local push from the shadow '
828 'Failure when doing local push from the shadow '
822 'repository to the target repository.')
829 'repository to the target repository.')
823 merge_succeeded = False
830 merge_succeeded = False
824 merge_failure_reason = MergeFailureReason.PUSH_FAILED
831 merge_failure_reason = MergeFailureReason.PUSH_FAILED
825 else:
832 else:
826 merge_succeeded = True
833 merge_succeeded = True
827 else:
834 else:
828 merge_succeeded = False
835 merge_succeeded = False
829
836
830 return MergeResponse(
837 return MergeResponse(
831 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
838 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
832
839
833 def _get_shadow_instance(
840 def _get_shadow_instance(
834 self, shadow_repository_path, enable_hooks=False):
841 self, shadow_repository_path, enable_hooks=False):
835 config = self.config.copy()
842 config = self.config.copy()
836 if not enable_hooks:
843 if not enable_hooks:
837 config.clear_section('hooks')
844 config.clear_section('hooks')
838 return MercurialRepository(shadow_repository_path, config)
845 return MercurialRepository(shadow_repository_path, config)
839
846
840 def _validate_pull_reference(self, reference):
847 def _validate_pull_reference(self, reference):
841 if not (reference.name in self.bookmarks or
848 if not (reference.name in self.bookmarks or
842 reference.name in self.branches or
849 reference.name in self.branches or
843 self.get_commit(reference.commit_id)):
850 self.get_commit(reference.commit_id)):
844 raise CommitDoesNotExistError(
851 raise CommitDoesNotExistError(
845 'Unknown branch, bookmark or commit id')
852 'Unknown branch, bookmark or commit id')
846
853
847 def _local_pull(self, repository_path, reference):
854 def _local_pull(self, repository_path, reference):
848 """
855 """
849 Fetch a branch, bookmark or commit from a local repository.
856 Fetch a branch, bookmark or commit from a local repository.
850 """
857 """
851 repository_path = os.path.abspath(repository_path)
858 repository_path = os.path.abspath(repository_path)
852 if repository_path == self.path:
859 if repository_path == self.path:
853 raise ValueError('Cannot pull from the same repository')
860 raise ValueError('Cannot pull from the same repository')
854
861
855 reference_type_to_option_name = {
862 reference_type_to_option_name = {
856 'book': 'bookmark',
863 'book': 'bookmark',
857 'branch': 'branch',
864 'branch': 'branch',
858 }
865 }
859 option_name = reference_type_to_option_name.get(
866 option_name = reference_type_to_option_name.get(
860 reference.type, 'revision')
867 reference.type, 'revision')
861
868
862 if option_name == 'revision':
869 if option_name == 'revision':
863 ref = reference.commit_id
870 ref = reference.commit_id
864 else:
871 else:
865 ref = reference.name
872 ref = reference.name
866
873
867 options = {option_name: [ref]}
874 options = {option_name: [ref]}
868 self._remote.pull_cmd(repository_path, hooks=False, **options)
875 self._remote.pull_cmd(repository_path, hooks=False, **options)
869 self._remote.invalidate_vcs_cache()
876 self._remote.invalidate_vcs_cache()
870
877
871 def bookmark(self, bookmark, revision=None):
878 def bookmark(self, bookmark, revision=None):
872 if isinstance(bookmark, unicode):
879 if isinstance(bookmark, unicode):
873 bookmark = safe_str(bookmark)
880 bookmark = safe_str(bookmark)
874 self._remote.bookmark(bookmark, revision=revision)
881 self._remote.bookmark(bookmark, revision=revision)
875 self._remote.invalidate_vcs_cache()
882 self._remote.invalidate_vcs_cache()
876
883
877
884
878 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
885 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
879
886
880 def _commit_factory(self, commit_id):
887 def _commit_factory(self, commit_id):
881 return self.repo.get_commit(
888 return self.repo.get_commit(
882 commit_idx=commit_id, pre_load=self.pre_load)
889 commit_idx=commit_id, pre_load=self.pre_load)
@@ -1,339 +1,339 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 SVN repository module
22 SVN repository module
23 """
23 """
24
24
25 import logging
25 import logging
26 import os
26 import os
27 import urllib
27 import urllib
28
28
29 from zope.cachedescriptors.property import Lazy as LazyProperty
29 from zope.cachedescriptors.property import Lazy as LazyProperty
30
30
31 from rhodecode.lib.compat import OrderedDict
31 from rhodecode.lib.compat import OrderedDict
32 from rhodecode.lib.datelib import date_astimestamp
32 from rhodecode.lib.datelib import date_astimestamp
33 from rhodecode.lib.utils import safe_str, safe_unicode
33 from rhodecode.lib.utils import safe_str, safe_unicode
34 from rhodecode.lib.vcs import connection, path as vcspath
34 from rhodecode.lib.vcs import connection, path as vcspath
35 from rhodecode.lib.vcs.backends import base
35 from rhodecode.lib.vcs.backends import base
36 from rhodecode.lib.vcs.backends.svn.commit import (
36 from rhodecode.lib.vcs.backends.svn.commit import (
37 SubversionCommit, _date_from_svn_properties)
37 SubversionCommit, _date_from_svn_properties)
38 from rhodecode.lib.vcs.backends.svn.diff import SubversionDiff
38 from rhodecode.lib.vcs.backends.svn.diff import SubversionDiff
39 from rhodecode.lib.vcs.backends.svn.inmemory import SubversionInMemoryCommit
39 from rhodecode.lib.vcs.backends.svn.inmemory import SubversionInMemoryCommit
40 from rhodecode.lib.vcs.conf import settings
40 from rhodecode.lib.vcs.conf import settings
41 from rhodecode.lib.vcs.exceptions import (
41 from rhodecode.lib.vcs.exceptions import (
42 CommitDoesNotExistError, EmptyRepositoryError, RepositoryError,
42 CommitDoesNotExistError, EmptyRepositoryError, RepositoryError,
43 VCSError, NodeDoesNotExistError)
43 VCSError, NodeDoesNotExistError)
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class SubversionRepository(base.BaseRepository):
49 class SubversionRepository(base.BaseRepository):
50 """
50 """
51 Subversion backend implementation
51 Subversion backend implementation
52
52
53 .. important::
53 .. important::
54
54
55 It is very important to distinguish the commit index and the commit id
55 It is very important to distinguish the commit index and the commit id
56 which is assigned by Subversion. The first one is always handled as an
56 which is assigned by Subversion. The first one is always handled as an
57 `int` by this implementation. The commit id assigned by Subversion on
57 `int` by this implementation. The commit id assigned by Subversion on
58 the other side will always be a `str`.
58 the other side will always be a `str`.
59
59
60 There is a specific trap since the first commit will have the index
60 There is a specific trap since the first commit will have the index
61 ``0`` but the svn id will be ``"1"``.
61 ``0`` but the svn id will be ``"1"``.
62
62
63 """
63 """
64
64
65 # Note: Subversion does not really have a default branch name.
65 # Note: Subversion does not really have a default branch name.
66 DEFAULT_BRANCH_NAME = None
66 DEFAULT_BRANCH_NAME = None
67
67
68 contact = base.BaseRepository.DEFAULT_CONTACT
68 contact = base.BaseRepository.DEFAULT_CONTACT
69 description = base.BaseRepository.DEFAULT_DESCRIPTION
69 description = base.BaseRepository.DEFAULT_DESCRIPTION
70
70
71 def __init__(self, repo_path, config=None, create=False, src_url=None,
71 def __init__(self, repo_path, config=None, create=False, src_url=None,
72 **kwargs):
72 **kwargs):
73 self.path = safe_str(os.path.abspath(repo_path))
73 self.path = safe_str(os.path.abspath(repo_path))
74 self.config = config if config else base.Config()
74 self.config = config if config else base.Config()
75 self._remote = connection.Svn(
75 self._remote = connection.Svn(
76 self.path, self.config)
76 self.path, self.config)
77
77
78 self._init_repo(create, src_url)
78 self._init_repo(create, src_url)
79
79
80 self.bookmarks = {}
80 self.bookmarks = {}
81
81
82 def _init_repo(self, create, src_url):
82 def _init_repo(self, create, src_url):
83 if create and os.path.exists(self.path):
83 if create and os.path.exists(self.path):
84 raise RepositoryError(
84 raise RepositoryError(
85 "Cannot create repository at %s, location already exist"
85 "Cannot create repository at %s, location already exist"
86 % self.path)
86 % self.path)
87
87
88 if create:
88 if create:
89 self._remote.create_repository(settings.SVN_COMPATIBLE_VERSION)
89 self._remote.create_repository(settings.SVN_COMPATIBLE_VERSION)
90 if src_url:
90 if src_url:
91 src_url = _sanitize_url(src_url)
91 src_url = _sanitize_url(src_url)
92 self._remote.import_remote_repository(src_url)
92 self._remote.import_remote_repository(src_url)
93 else:
93 else:
94 self._check_path()
94 self._check_path()
95
95
96 @LazyProperty
96 @LazyProperty
97 def commit_ids(self):
97 def commit_ids(self):
98 head = self._remote.lookup(None)
98 head = self._remote.lookup(None)
99 return [str(r) for r in xrange(1, head + 1)]
99 return [str(r) for r in xrange(1, head + 1)]
100
100
101 @LazyProperty
101 @LazyProperty
102 def branches(self):
102 def branches(self):
103 return self._tags_or_branches('vcs_svn_branch')
103 return self._tags_or_branches('vcs_svn_branch')
104
104
105 @LazyProperty
105 @LazyProperty
106 def branches_closed(self):
106 def branches_closed(self):
107 return {}
107 return {}
108
108
109 @LazyProperty
109 @LazyProperty
110 def branches_all(self):
110 def branches_all(self):
111 # TODO: johbo: Implement proper branch support
111 # TODO: johbo: Implement proper branch support
112 all_branches = {}
112 all_branches = {}
113 all_branches.update(self.branches)
113 all_branches.update(self.branches)
114 all_branches.update(self.branches_closed)
114 all_branches.update(self.branches_closed)
115 return all_branches
115 return all_branches
116
116
117 @LazyProperty
117 @LazyProperty
118 def tags(self):
118 def tags(self):
119 return self._tags_or_branches('vcs_svn_tag')
119 return self._tags_or_branches('vcs_svn_tag')
120
120
121 def _tags_or_branches(self, config_section):
121 def _tags_or_branches(self, config_section):
122 found_items = {}
122 found_items = {}
123
123
124 if self.is_empty():
124 if self.is_empty():
125 return {}
125 return {}
126
126
127 for pattern in self._patterns_from_section(config_section):
127 for pattern in self._patterns_from_section(config_section):
128 pattern = vcspath.sanitize(pattern)
128 pattern = vcspath.sanitize(pattern)
129 tip = self.get_commit()
129 tip = self.get_commit()
130 try:
130 try:
131 if pattern.endswith('*'):
131 if pattern.endswith('*'):
132 basedir = tip.get_node(vcspath.dirname(pattern))
132 basedir = tip.get_node(vcspath.dirname(pattern))
133 directories = basedir.dirs
133 directories = basedir.dirs
134 else:
134 else:
135 directories = (tip.get_node(pattern), )
135 directories = (tip.get_node(pattern), )
136 except NodeDoesNotExistError:
136 except NodeDoesNotExistError:
137 continue
137 continue
138 found_items.update(
138 found_items.update(
139 (safe_unicode(n.path),
139 (safe_unicode(n.path),
140 self.commit_ids[-1])
140 self.commit_ids[-1])
141 for n in directories)
141 for n in directories)
142
142
143 def get_name(item):
143 def get_name(item):
144 return item[0]
144 return item[0]
145
145
146 return OrderedDict(sorted(found_items.items(), key=get_name))
146 return OrderedDict(sorted(found_items.items(), key=get_name))
147
147
148 def _patterns_from_section(self, section):
148 def _patterns_from_section(self, section):
149 return (pattern for key, pattern in self.config.items(section))
149 return (pattern for key, pattern in self.config.items(section))
150
150
151 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
151 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
152 if self != repo2:
152 if self != repo2:
153 raise ValueError(
153 raise ValueError(
154 "Subversion does not support getting common ancestor of"
154 "Subversion does not support getting common ancestor of"
155 " different repositories.")
155 " different repositories.")
156
156
157 if int(commit_id1) < int(commit_id2):
157 if int(commit_id1) < int(commit_id2):
158 return commit_id1
158 return commit_id1
159 return commit_id2
159 return commit_id2
160
160
161 def verify(self):
161 def verify(self):
162 verify = self._remote.verify()
162 verify = self._remote.verify()
163
163
164 self._remote.invalidate_vcs_cache()
164 self._remote.invalidate_vcs_cache()
165 return verify
165 return verify
166
166
167 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
167 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
168 # TODO: johbo: Implement better comparison, this is a very naive
168 # TODO: johbo: Implement better comparison, this is a very naive
169 # version which does not allow to compare branches, tags or folders
169 # version which does not allow to compare branches, tags or folders
170 # at all.
170 # at all.
171 if repo2 != self:
171 if repo2 != self:
172 raise ValueError(
172 raise ValueError(
173 "Subversion does not support comparison of of different "
173 "Subversion does not support comparison of of different "
174 "repositories.")
174 "repositories.")
175
175
176 if commit_id1 == commit_id2:
176 if commit_id1 == commit_id2:
177 return []
177 return []
178
178
179 commit_idx1 = self._get_commit_idx(commit_id1)
179 commit_idx1 = self._get_commit_idx(commit_id1)
180 commit_idx2 = self._get_commit_idx(commit_id2)
180 commit_idx2 = self._get_commit_idx(commit_id2)
181
181
182 commits = [
182 commits = [
183 self.get_commit(commit_idx=idx)
183 self.get_commit(commit_idx=idx)
184 for idx in range(commit_idx1 + 1, commit_idx2 + 1)]
184 for idx in range(commit_idx1 + 1, commit_idx2 + 1)]
185
185
186 return commits
186 return commits
187
187
188 def _get_commit_idx(self, commit_id):
188 def _get_commit_idx(self, commit_id):
189 try:
189 try:
190 svn_rev = int(commit_id)
190 svn_rev = int(commit_id)
191 except:
191 except:
192 # TODO: johbo: this might be only one case, HEAD, check this
192 # TODO: johbo: this might be only one case, HEAD, check this
193 svn_rev = self._remote.lookup(commit_id)
193 svn_rev = self._remote.lookup(commit_id)
194 commit_idx = svn_rev - 1
194 commit_idx = svn_rev - 1
195 if commit_idx >= len(self.commit_ids):
195 if commit_idx >= len(self.commit_ids):
196 raise CommitDoesNotExistError(
196 raise CommitDoesNotExistError(
197 "Commit at index %s does not exist." % (commit_idx, ))
197 "Commit at index %s does not exist." % (commit_idx, ))
198 return commit_idx
198 return commit_idx
199
199
200 @staticmethod
200 @staticmethod
201 def check_url(url, config):
201 def check_url(url, config):
202 """
202 """
203 Check if `url` is a valid source to import a Subversion repository.
203 Check if `url` is a valid source to import a Subversion repository.
204 """
204 """
205 # convert to URL if it's a local directory
205 # convert to URL if it's a local directory
206 if os.path.isdir(url):
206 if os.path.isdir(url):
207 url = 'file://' + urllib.pathname2url(url)
207 url = 'file://' + urllib.pathname2url(url)
208 return connection.Svn.check_url(url, config.serialize())
208 return connection.Svn.check_url(url, config.serialize())
209
209
210 @staticmethod
210 @staticmethod
211 def is_valid_repository(path):
211 def is_valid_repository(path):
212 try:
212 try:
213 SubversionRepository(path)
213 SubversionRepository(path)
214 return True
214 return True
215 except VCSError:
215 except VCSError:
216 pass
216 pass
217 return False
217 return False
218
218
219 def _check_path(self):
219 def _check_path(self):
220 if not os.path.exists(self.path):
220 if not os.path.exists(self.path):
221 raise VCSError('Path "%s" does not exist!' % (self.path, ))
221 raise VCSError('Path "%s" does not exist!' % (self.path, ))
222 if not self._remote.is_path_valid_repository(self.path):
222 if not self._remote.is_path_valid_repository(self.path):
223 raise VCSError(
223 raise VCSError(
224 'Path "%s" does not contain a Subversion repository' %
224 'Path "%s" does not contain a Subversion repository' %
225 (self.path, ))
225 (self.path, ))
226
226
227 @LazyProperty
227 @LazyProperty
228 def last_change(self):
228 def last_change(self):
229 """
229 """
230 Returns last change made on this repository as
230 Returns last change made on this repository as
231 `datetime.datetime` object.
231 `datetime.datetime` object.
232 """
232 """
233 # Subversion always has a first commit which has id "0" and contains
233 # Subversion always has a first commit which has id "0" and contains
234 # what we are looking for.
234 # what we are looking for.
235 last_id = len(self.commit_ids)
235 last_id = len(self.commit_ids)
236 properties = self._remote.revision_properties(last_id)
236 properties = self._remote.revision_properties(last_id)
237 return _date_from_svn_properties(properties)
237 return _date_from_svn_properties(properties)
238
238
239 @LazyProperty
239 @LazyProperty
240 def in_memory_commit(self):
240 def in_memory_commit(self):
241 return SubversionInMemoryCommit(self)
241 return SubversionInMemoryCommit(self)
242
242
243 def get_hook_location(self):
243 def get_hook_location(self):
244 """
244 """
245 returns absolute path to location where hooks are stored
245 returns absolute path to location where hooks are stored
246 """
246 """
247 return os.path.join(self.path, 'hooks')
247 return os.path.join(self.path, 'hooks')
248
248
249 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
249 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
250 if self.is_empty():
250 if self.is_empty():
251 raise EmptyRepositoryError("There are no commits yet")
251 raise EmptyRepositoryError("There are no commits yet")
252 if commit_id is not None:
252 if commit_id is not None:
253 self._validate_commit_id(commit_id)
253 self._validate_commit_id(commit_id)
254 elif commit_idx is not None:
254 elif commit_idx is not None:
255 self._validate_commit_idx(commit_idx)
255 self._validate_commit_idx(commit_idx)
256 try:
256 try:
257 commit_id = self.commit_ids[commit_idx]
257 commit_id = self.commit_ids[commit_idx]
258 except IndexError:
258 except IndexError:
259 raise CommitDoesNotExistError
259 raise CommitDoesNotExistError
260
260
261 commit_id = self._sanitize_commit_id(commit_id)
261 commit_id = self._sanitize_commit_id(commit_id)
262 commit = SubversionCommit(repository=self, commit_id=commit_id)
262 commit = SubversionCommit(repository=self, commit_id=commit_id)
263 return commit
263 return commit
264
264
265 def get_commits(
265 def get_commits(
266 self, start_id=None, end_id=None, start_date=None, end_date=None,
266 self, start_id=None, end_id=None, start_date=None, end_date=None,
267 branch_name=None, pre_load=None):
267 branch_name=None, show_hidden=False, pre_load=None):
268 if self.is_empty():
268 if self.is_empty():
269 raise EmptyRepositoryError("There are no commit_ids yet")
269 raise EmptyRepositoryError("There are no commit_ids yet")
270 self._validate_branch_name(branch_name)
270 self._validate_branch_name(branch_name)
271
271
272 if start_id is not None:
272 if start_id is not None:
273 self._validate_commit_id(start_id)
273 self._validate_commit_id(start_id)
274 if end_id is not None:
274 if end_id is not None:
275 self._validate_commit_id(end_id)
275 self._validate_commit_id(end_id)
276
276
277 start_raw_id = self._sanitize_commit_id(start_id)
277 start_raw_id = self._sanitize_commit_id(start_id)
278 start_pos = self.commit_ids.index(start_raw_id) if start_id else None
278 start_pos = self.commit_ids.index(start_raw_id) if start_id else None
279 end_raw_id = self._sanitize_commit_id(end_id)
279 end_raw_id = self._sanitize_commit_id(end_id)
280 end_pos = max(0, self.commit_ids.index(end_raw_id)) if end_id else None
280 end_pos = max(0, self.commit_ids.index(end_raw_id)) if end_id else None
281
281
282 if None not in [start_id, end_id] and start_pos > end_pos:
282 if None not in [start_id, end_id] and start_pos > end_pos:
283 raise RepositoryError(
283 raise RepositoryError(
284 "Start commit '%s' cannot be after end commit '%s'" %
284 "Start commit '%s' cannot be after end commit '%s'" %
285 (start_id, end_id))
285 (start_id, end_id))
286 if end_pos is not None:
286 if end_pos is not None:
287 end_pos += 1
287 end_pos += 1
288
288
289 # Date based filtering
289 # Date based filtering
290 if start_date or end_date:
290 if start_date or end_date:
291 start_raw_id, end_raw_id = self._remote.lookup_interval(
291 start_raw_id, end_raw_id = self._remote.lookup_interval(
292 date_astimestamp(start_date) if start_date else None,
292 date_astimestamp(start_date) if start_date else None,
293 date_astimestamp(end_date) if end_date else None)
293 date_astimestamp(end_date) if end_date else None)
294 start_pos = start_raw_id - 1
294 start_pos = start_raw_id - 1
295 end_pos = end_raw_id
295 end_pos = end_raw_id
296
296
297 commit_ids = self.commit_ids
297 commit_ids = self.commit_ids
298
298
299 # TODO: johbo: Reconsider impact of DEFAULT_BRANCH_NAME here
299 # TODO: johbo: Reconsider impact of DEFAULT_BRANCH_NAME here
300 if branch_name not in [None, self.DEFAULT_BRANCH_NAME]:
300 if branch_name not in [None, self.DEFAULT_BRANCH_NAME]:
301 svn_rev = long(self.commit_ids[-1])
301 svn_rev = long(self.commit_ids[-1])
302 commit_ids = self._remote.node_history(
302 commit_ids = self._remote.node_history(
303 path=branch_name, revision=svn_rev, limit=None)
303 path=branch_name, revision=svn_rev, limit=None)
304 commit_ids = [str(i) for i in reversed(commit_ids)]
304 commit_ids = [str(i) for i in reversed(commit_ids)]
305
305
306 if start_pos or end_pos:
306 if start_pos or end_pos:
307 commit_ids = commit_ids[start_pos:end_pos]
307 commit_ids = commit_ids[start_pos:end_pos]
308 return base.CollectionGenerator(self, commit_ids, pre_load=pre_load)
308 return base.CollectionGenerator(self, commit_ids, pre_load=pre_load)
309
309
310 def _sanitize_commit_id(self, commit_id):
310 def _sanitize_commit_id(self, commit_id):
311 if commit_id and commit_id.isdigit():
311 if commit_id and commit_id.isdigit():
312 if int(commit_id) <= len(self.commit_ids):
312 if int(commit_id) <= len(self.commit_ids):
313 return commit_id
313 return commit_id
314 else:
314 else:
315 raise CommitDoesNotExistError(
315 raise CommitDoesNotExistError(
316 "Commit %s does not exist." % (commit_id, ))
316 "Commit %s does not exist." % (commit_id, ))
317 if commit_id not in [
317 if commit_id not in [
318 None, 'HEAD', 'tip', self.DEFAULT_BRANCH_NAME]:
318 None, 'HEAD', 'tip', self.DEFAULT_BRANCH_NAME]:
319 raise CommitDoesNotExistError(
319 raise CommitDoesNotExistError(
320 "Commit id %s not understood." % (commit_id, ))
320 "Commit id %s not understood." % (commit_id, ))
321 svn_rev = self._remote.lookup('HEAD')
321 svn_rev = self._remote.lookup('HEAD')
322 return str(svn_rev)
322 return str(svn_rev)
323
323
324 def get_diff(
324 def get_diff(
325 self, commit1, commit2, path=None, ignore_whitespace=False,
325 self, commit1, commit2, path=None, ignore_whitespace=False,
326 context=3, path1=None):
326 context=3, path1=None):
327 self._validate_diff_commits(commit1, commit2)
327 self._validate_diff_commits(commit1, commit2)
328 svn_rev1 = long(commit1.raw_id)
328 svn_rev1 = long(commit1.raw_id)
329 svn_rev2 = long(commit2.raw_id)
329 svn_rev2 = long(commit2.raw_id)
330 diff = self._remote.diff(
330 diff = self._remote.diff(
331 svn_rev1, svn_rev2, path1=path1, path2=path,
331 svn_rev1, svn_rev2, path1=path1, path2=path,
332 ignore_whitespace=ignore_whitespace, context=context)
332 ignore_whitespace=ignore_whitespace, context=context)
333 return SubversionDiff(diff)
333 return SubversionDiff(diff)
334
334
335
335
336 def _sanitize_url(url):
336 def _sanitize_url(url):
337 if '://' not in url:
337 if '://' not in url:
338 url = 'file://' + urllib.pathname2url(url)
338 url = 'file://' + urllib.pathname2url(url)
339 return url
339 return url
@@ -1,2403 +1,2408 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'fonts';
8 @import 'fonts';
9 @import 'variables';
9 @import 'variables';
10 @import 'bootstrap-variables';
10 @import 'bootstrap-variables';
11 @import 'form-bootstrap';
11 @import 'form-bootstrap';
12 @import 'codemirror';
12 @import 'codemirror';
13 @import 'legacy_code_styles';
13 @import 'legacy_code_styles';
14 @import 'readme-box';
14 @import 'readme-box';
15 @import 'progress-bar';
15 @import 'progress-bar';
16
16
17 @import 'type';
17 @import 'type';
18 @import 'alerts';
18 @import 'alerts';
19 @import 'buttons';
19 @import 'buttons';
20 @import 'tags';
20 @import 'tags';
21 @import 'code-block';
21 @import 'code-block';
22 @import 'examples';
22 @import 'examples';
23 @import 'login';
23 @import 'login';
24 @import 'main-content';
24 @import 'main-content';
25 @import 'select2';
25 @import 'select2';
26 @import 'comments';
26 @import 'comments';
27 @import 'panels-bootstrap';
27 @import 'panels-bootstrap';
28 @import 'panels';
28 @import 'panels';
29 @import 'deform';
29 @import 'deform';
30
30
31 //--- BASE ------------------//
31 //--- BASE ------------------//
32 .noscript-error {
32 .noscript-error {
33 top: 0;
33 top: 0;
34 left: 0;
34 left: 0;
35 width: 100%;
35 width: 100%;
36 z-index: 101;
36 z-index: 101;
37 text-align: center;
37 text-align: center;
38 font-family: @text-semibold;
38 font-family: @text-semibold;
39 font-size: 120%;
39 font-size: 120%;
40 color: white;
40 color: white;
41 background-color: @alert2;
41 background-color: @alert2;
42 padding: 5px 0 5px 0;
42 padding: 5px 0 5px 0;
43 }
43 }
44
44
45 html {
45 html {
46 display: table;
46 display: table;
47 height: 100%;
47 height: 100%;
48 width: 100%;
48 width: 100%;
49 }
49 }
50
50
51 body {
51 body {
52 display: table-cell;
52 display: table-cell;
53 width: 100%;
53 width: 100%;
54 }
54 }
55
55
56 //--- LAYOUT ------------------//
56 //--- LAYOUT ------------------//
57
57
58 .hidden{
58 .hidden{
59 display: none !important;
59 display: none !important;
60 }
60 }
61
61
62 .box{
62 .box{
63 float: left;
63 float: left;
64 width: 100%;
64 width: 100%;
65 }
65 }
66
66
67 .browser-header {
67 .browser-header {
68 clear: both;
68 clear: both;
69 }
69 }
70 .main {
70 .main {
71 clear: both;
71 clear: both;
72 padding:0 0 @pagepadding;
72 padding:0 0 @pagepadding;
73 height: auto;
73 height: auto;
74
74
75 &:after { //clearfix
75 &:after { //clearfix
76 content:"";
76 content:"";
77 clear:both;
77 clear:both;
78 width:100%;
78 width:100%;
79 display:block;
79 display:block;
80 }
80 }
81 }
81 }
82
82
83 .action-link{
83 .action-link{
84 margin-left: @padding;
84 margin-left: @padding;
85 padding-left: @padding;
85 padding-left: @padding;
86 border-left: @border-thickness solid @border-default-color;
86 border-left: @border-thickness solid @border-default-color;
87 }
87 }
88
88
89 input + .action-link, .action-link.first{
89 input + .action-link, .action-link.first{
90 border-left: none;
90 border-left: none;
91 }
91 }
92
92
93 .action-link.last{
93 .action-link.last{
94 margin-right: @padding;
94 margin-right: @padding;
95 padding-right: @padding;
95 padding-right: @padding;
96 }
96 }
97
97
98 .action-link.active,
98 .action-link.active,
99 .action-link.active a{
99 .action-link.active a{
100 color: @grey4;
100 color: @grey4;
101 }
101 }
102
102
103 .action-link.disabled {
104 color: @grey4;
105 cursor: inherit;
106 }
107
103 .clipboard-action {
108 .clipboard-action {
104 cursor: pointer;
109 cursor: pointer;
105 }
110 }
106
111
107 ul.simple-list{
112 ul.simple-list{
108 list-style: none;
113 list-style: none;
109 margin: 0;
114 margin: 0;
110 padding: 0;
115 padding: 0;
111 }
116 }
112
117
113 .main-content {
118 .main-content {
114 padding-bottom: @pagepadding;
119 padding-bottom: @pagepadding;
115 }
120 }
116
121
117 .wide-mode-wrapper {
122 .wide-mode-wrapper {
118 max-width:4000px !important;
123 max-width:4000px !important;
119 }
124 }
120
125
121 .wrapper {
126 .wrapper {
122 position: relative;
127 position: relative;
123 max-width: @wrapper-maxwidth;
128 max-width: @wrapper-maxwidth;
124 margin: 0 auto;
129 margin: 0 auto;
125 }
130 }
126
131
127 #content {
132 #content {
128 clear: both;
133 clear: both;
129 padding: 0 @contentpadding;
134 padding: 0 @contentpadding;
130 }
135 }
131
136
132 .advanced-settings-fields{
137 .advanced-settings-fields{
133 input{
138 input{
134 margin-left: @textmargin;
139 margin-left: @textmargin;
135 margin-right: @padding/2;
140 margin-right: @padding/2;
136 }
141 }
137 }
142 }
138
143
139 .cs_files_title {
144 .cs_files_title {
140 margin: @pagepadding 0 0;
145 margin: @pagepadding 0 0;
141 }
146 }
142
147
143 input.inline[type="file"] {
148 input.inline[type="file"] {
144 display: inline;
149 display: inline;
145 }
150 }
146
151
147 .error_page {
152 .error_page {
148 margin: 10% auto;
153 margin: 10% auto;
149
154
150 h1 {
155 h1 {
151 color: @grey2;
156 color: @grey2;
152 }
157 }
153
158
154 .alert {
159 .alert {
155 margin: @padding 0;
160 margin: @padding 0;
156 }
161 }
157
162
158 .error-branding {
163 .error-branding {
159 font-family: @text-semibold;
164 font-family: @text-semibold;
160 color: @grey4;
165 color: @grey4;
161 }
166 }
162
167
163 .error_message {
168 .error_message {
164 font-family: @text-regular;
169 font-family: @text-regular;
165 }
170 }
166
171
167 .sidebar {
172 .sidebar {
168 min-height: 275px;
173 min-height: 275px;
169 margin: 0;
174 margin: 0;
170 padding: 0 0 @sidebarpadding @sidebarpadding;
175 padding: 0 0 @sidebarpadding @sidebarpadding;
171 border: none;
176 border: none;
172 }
177 }
173
178
174 .main-content {
179 .main-content {
175 position: relative;
180 position: relative;
176 margin: 0 @sidebarpadding @sidebarpadding;
181 margin: 0 @sidebarpadding @sidebarpadding;
177 padding: 0 0 0 @sidebarpadding;
182 padding: 0 0 0 @sidebarpadding;
178 border-left: @border-thickness solid @grey5;
183 border-left: @border-thickness solid @grey5;
179
184
180 @media (max-width:767px) {
185 @media (max-width:767px) {
181 clear: both;
186 clear: both;
182 width: 100%;
187 width: 100%;
183 margin: 0;
188 margin: 0;
184 border: none;
189 border: none;
185 }
190 }
186 }
191 }
187
192
188 .inner-column {
193 .inner-column {
189 float: left;
194 float: left;
190 width: 29.75%;
195 width: 29.75%;
191 min-height: 150px;
196 min-height: 150px;
192 margin: @sidebarpadding 2% 0 0;
197 margin: @sidebarpadding 2% 0 0;
193 padding: 0 2% 0 0;
198 padding: 0 2% 0 0;
194 border-right: @border-thickness solid @grey5;
199 border-right: @border-thickness solid @grey5;
195
200
196 @media (max-width:767px) {
201 @media (max-width:767px) {
197 clear: both;
202 clear: both;
198 width: 100%;
203 width: 100%;
199 border: none;
204 border: none;
200 }
205 }
201
206
202 ul {
207 ul {
203 padding-left: 1.25em;
208 padding-left: 1.25em;
204 }
209 }
205
210
206 &:last-child {
211 &:last-child {
207 margin: @sidebarpadding 0 0;
212 margin: @sidebarpadding 0 0;
208 border: none;
213 border: none;
209 }
214 }
210
215
211 h4 {
216 h4 {
212 margin: 0 0 @padding;
217 margin: 0 0 @padding;
213 font-family: @text-semibold;
218 font-family: @text-semibold;
214 }
219 }
215 }
220 }
216 }
221 }
217 .error-page-logo {
222 .error-page-logo {
218 width: 130px;
223 width: 130px;
219 height: 160px;
224 height: 160px;
220 }
225 }
221
226
222 // HEADER
227 // HEADER
223 .header {
228 .header {
224
229
225 // TODO: johbo: Fix login pages, so that they work without a min-height
230 // TODO: johbo: Fix login pages, so that they work without a min-height
226 // for the header and then remove the min-height. I chose a smaller value
231 // for the header and then remove the min-height. I chose a smaller value
227 // intentionally here to avoid rendering issues in the main navigation.
232 // intentionally here to avoid rendering issues in the main navigation.
228 min-height: 49px;
233 min-height: 49px;
229
234
230 position: relative;
235 position: relative;
231 vertical-align: bottom;
236 vertical-align: bottom;
232 padding: 0 @header-padding;
237 padding: 0 @header-padding;
233 background-color: @grey2;
238 background-color: @grey2;
234 color: @grey5;
239 color: @grey5;
235
240
236 .title {
241 .title {
237 overflow: visible;
242 overflow: visible;
238 }
243 }
239
244
240 &:before,
245 &:before,
241 &:after {
246 &:after {
242 content: "";
247 content: "";
243 clear: both;
248 clear: both;
244 width: 100%;
249 width: 100%;
245 }
250 }
246
251
247 // TODO: johbo: Avoids breaking "Repositories" chooser
252 // TODO: johbo: Avoids breaking "Repositories" chooser
248 .select2-container .select2-choice .select2-arrow {
253 .select2-container .select2-choice .select2-arrow {
249 display: none;
254 display: none;
250 }
255 }
251 }
256 }
252
257
253 #header-inner {
258 #header-inner {
254 &.title {
259 &.title {
255 margin: 0;
260 margin: 0;
256 }
261 }
257 &:before,
262 &:before,
258 &:after {
263 &:after {
259 content: "";
264 content: "";
260 clear: both;
265 clear: both;
261 }
266 }
262 }
267 }
263
268
264 // Gists
269 // Gists
265 #files_data {
270 #files_data {
266 clear: both; //for firefox
271 clear: both; //for firefox
267 }
272 }
268 #gistid {
273 #gistid {
269 margin-right: @padding;
274 margin-right: @padding;
270 }
275 }
271
276
272 // Global Settings Editor
277 // Global Settings Editor
273 .textarea.editor {
278 .textarea.editor {
274 float: left;
279 float: left;
275 position: relative;
280 position: relative;
276 max-width: @texteditor-width;
281 max-width: @texteditor-width;
277
282
278 select {
283 select {
279 position: absolute;
284 position: absolute;
280 top:10px;
285 top:10px;
281 right:0;
286 right:0;
282 }
287 }
283
288
284 .CodeMirror {
289 .CodeMirror {
285 margin: 0;
290 margin: 0;
286 }
291 }
287
292
288 .help-block {
293 .help-block {
289 margin: 0 0 @padding;
294 margin: 0 0 @padding;
290 padding:.5em;
295 padding:.5em;
291 background-color: @grey6;
296 background-color: @grey6;
292 &.pre-formatting {
297 &.pre-formatting {
293 white-space: pre;
298 white-space: pre;
294 }
299 }
295 }
300 }
296 }
301 }
297
302
298 ul.auth_plugins {
303 ul.auth_plugins {
299 margin: @padding 0 @padding @legend-width;
304 margin: @padding 0 @padding @legend-width;
300 padding: 0;
305 padding: 0;
301
306
302 li {
307 li {
303 margin-bottom: @padding;
308 margin-bottom: @padding;
304 line-height: 1em;
309 line-height: 1em;
305 list-style-type: none;
310 list-style-type: none;
306
311
307 .auth_buttons .btn {
312 .auth_buttons .btn {
308 margin-right: @padding;
313 margin-right: @padding;
309 }
314 }
310
315
311 &:before { content: none; }
316 &:before { content: none; }
312 }
317 }
313 }
318 }
314
319
315
320
316 // My Account PR list
321 // My Account PR list
317
322
318 #show_closed {
323 #show_closed {
319 margin: 0 1em 0 0;
324 margin: 0 1em 0 0;
320 }
325 }
321
326
322 .pullrequestlist {
327 .pullrequestlist {
323 .closed {
328 .closed {
324 background-color: @grey6;
329 background-color: @grey6;
325 }
330 }
326 .td-status {
331 .td-status {
327 padding-left: .5em;
332 padding-left: .5em;
328 }
333 }
329 .log-container .truncate {
334 .log-container .truncate {
330 height: 2.75em;
335 height: 2.75em;
331 white-space: pre-line;
336 white-space: pre-line;
332 }
337 }
333 table.rctable .user {
338 table.rctable .user {
334 padding-left: 0;
339 padding-left: 0;
335 }
340 }
336 table.rctable {
341 table.rctable {
337 td.td-description,
342 td.td-description,
338 .rc-user {
343 .rc-user {
339 min-width: auto;
344 min-width: auto;
340 }
345 }
341 }
346 }
342 }
347 }
343
348
344 // Pull Requests
349 // Pull Requests
345
350
346 .pullrequests_section_head {
351 .pullrequests_section_head {
347 display: block;
352 display: block;
348 clear: both;
353 clear: both;
349 margin: @padding 0;
354 margin: @padding 0;
350 font-family: @text-bold;
355 font-family: @text-bold;
351 }
356 }
352
357
353 .pr-origininfo, .pr-targetinfo {
358 .pr-origininfo, .pr-targetinfo {
354 position: relative;
359 position: relative;
355
360
356 .tag {
361 .tag {
357 display: inline-block;
362 display: inline-block;
358 margin: 0 1em .5em 0;
363 margin: 0 1em .5em 0;
359 }
364 }
360
365
361 .clone-url {
366 .clone-url {
362 display: inline-block;
367 display: inline-block;
363 margin: 0 0 .5em 0;
368 margin: 0 0 .5em 0;
364 padding: 0;
369 padding: 0;
365 line-height: 1.2em;
370 line-height: 1.2em;
366 }
371 }
367 }
372 }
368
373
369 .pr-mergeinfo {
374 .pr-mergeinfo {
370 min-width: 95% !important;
375 min-width: 95% !important;
371 padding: 0 !important;
376 padding: 0 !important;
372 border: 0;
377 border: 0;
373 }
378 }
374 .pr-mergeinfo-copy {
379 .pr-mergeinfo-copy {
375 padding: 0 0;
380 padding: 0 0;
376 }
381 }
377
382
378 .pr-pullinfo {
383 .pr-pullinfo {
379 min-width: 95% !important;
384 min-width: 95% !important;
380 padding: 0 !important;
385 padding: 0 !important;
381 border: 0;
386 border: 0;
382 }
387 }
383 .pr-pullinfo-copy {
388 .pr-pullinfo-copy {
384 padding: 0 0;
389 padding: 0 0;
385 }
390 }
386
391
387
392
388 #pr-title-input {
393 #pr-title-input {
389 width: 72%;
394 width: 72%;
390 font-size: 1em;
395 font-size: 1em;
391 font-family: @text-bold;
396 font-family: @text-bold;
392 margin: 0;
397 margin: 0;
393 padding: 0 0 0 @padding/4;
398 padding: 0 0 0 @padding/4;
394 line-height: 1.7em;
399 line-height: 1.7em;
395 color: @text-color;
400 color: @text-color;
396 letter-spacing: .02em;
401 letter-spacing: .02em;
397 }
402 }
398
403
399 #pullrequest_title {
404 #pullrequest_title {
400 width: 100%;
405 width: 100%;
401 box-sizing: border-box;
406 box-sizing: border-box;
402 }
407 }
403
408
404 #pr_open_message {
409 #pr_open_message {
405 border: @border-thickness solid #fff;
410 border: @border-thickness solid #fff;
406 border-radius: @border-radius;
411 border-radius: @border-radius;
407 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
412 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
408 text-align: left;
413 text-align: left;
409 overflow: hidden;
414 overflow: hidden;
410 }
415 }
411
416
412 .pr-submit-button {
417 .pr-submit-button {
413 float: right;
418 float: right;
414 margin: 0 0 0 5px;
419 margin: 0 0 0 5px;
415 }
420 }
416
421
417 .pr-spacing-container {
422 .pr-spacing-container {
418 padding: 20px;
423 padding: 20px;
419 clear: both
424 clear: both
420 }
425 }
421
426
422 #pr-description-input {
427 #pr-description-input {
423 margin-bottom: 0;
428 margin-bottom: 0;
424 }
429 }
425
430
426 .pr-description-label {
431 .pr-description-label {
427 vertical-align: top;
432 vertical-align: top;
428 }
433 }
429
434
430 .perms_section_head {
435 .perms_section_head {
431 min-width: 625px;
436 min-width: 625px;
432
437
433 h2 {
438 h2 {
434 margin-bottom: 0;
439 margin-bottom: 0;
435 }
440 }
436
441
437 .label-checkbox {
442 .label-checkbox {
438 float: left;
443 float: left;
439 }
444 }
440
445
441 &.field {
446 &.field {
442 margin: @space 0 @padding;
447 margin: @space 0 @padding;
443 }
448 }
444
449
445 &:first-child.field {
450 &:first-child.field {
446 margin-top: 0;
451 margin-top: 0;
447
452
448 .label {
453 .label {
449 margin-top: 0;
454 margin-top: 0;
450 padding-top: 0;
455 padding-top: 0;
451 }
456 }
452
457
453 .radios {
458 .radios {
454 padding-top: 0;
459 padding-top: 0;
455 }
460 }
456 }
461 }
457
462
458 .radios {
463 .radios {
459 position: relative;
464 position: relative;
460 width: 405px;
465 width: 405px;
461 }
466 }
462 }
467 }
463
468
464 //--- MODULES ------------------//
469 //--- MODULES ------------------//
465
470
466
471
467 // Server Announcement
472 // Server Announcement
468 #server-announcement {
473 #server-announcement {
469 width: 95%;
474 width: 95%;
470 margin: @padding auto;
475 margin: @padding auto;
471 padding: @padding;
476 padding: @padding;
472 border-width: 2px;
477 border-width: 2px;
473 border-style: solid;
478 border-style: solid;
474 .border-radius(2px);
479 .border-radius(2px);
475 font-family: @text-bold;
480 font-family: @text-bold;
476
481
477 &.info { border-color: @alert4; background-color: @alert4-inner; }
482 &.info { border-color: @alert4; background-color: @alert4-inner; }
478 &.warning { border-color: @alert3; background-color: @alert3-inner; }
483 &.warning { border-color: @alert3; background-color: @alert3-inner; }
479 &.error { border-color: @alert2; background-color: @alert2-inner; }
484 &.error { border-color: @alert2; background-color: @alert2-inner; }
480 &.success { border-color: @alert1; background-color: @alert1-inner; }
485 &.success { border-color: @alert1; background-color: @alert1-inner; }
481 &.neutral { border-color: @grey3; background-color: @grey6; }
486 &.neutral { border-color: @grey3; background-color: @grey6; }
482 }
487 }
483
488
484 // Fixed Sidebar Column
489 // Fixed Sidebar Column
485 .sidebar-col-wrapper {
490 .sidebar-col-wrapper {
486 padding-left: @sidebar-all-width;
491 padding-left: @sidebar-all-width;
487
492
488 .sidebar {
493 .sidebar {
489 width: @sidebar-width;
494 width: @sidebar-width;
490 margin-left: -@sidebar-all-width;
495 margin-left: -@sidebar-all-width;
491 }
496 }
492 }
497 }
493
498
494 .sidebar-col-wrapper.scw-small {
499 .sidebar-col-wrapper.scw-small {
495 padding-left: @sidebar-small-all-width;
500 padding-left: @sidebar-small-all-width;
496
501
497 .sidebar {
502 .sidebar {
498 width: @sidebar-small-width;
503 width: @sidebar-small-width;
499 margin-left: -@sidebar-small-all-width;
504 margin-left: -@sidebar-small-all-width;
500 }
505 }
501 }
506 }
502
507
503
508
504 // FOOTER
509 // FOOTER
505 #footer {
510 #footer {
506 padding: 0;
511 padding: 0;
507 text-align: center;
512 text-align: center;
508 vertical-align: middle;
513 vertical-align: middle;
509 color: @grey2;
514 color: @grey2;
510 background-color: @grey6;
515 background-color: @grey6;
511
516
512 p {
517 p {
513 margin: 0;
518 margin: 0;
514 padding: 1em;
519 padding: 1em;
515 line-height: 1em;
520 line-height: 1em;
516 }
521 }
517
522
518 .server-instance { //server instance
523 .server-instance { //server instance
519 display: none;
524 display: none;
520 }
525 }
521
526
522 .title {
527 .title {
523 float: none;
528 float: none;
524 margin: 0 auto;
529 margin: 0 auto;
525 }
530 }
526 }
531 }
527
532
528 button.close {
533 button.close {
529 padding: 0;
534 padding: 0;
530 cursor: pointer;
535 cursor: pointer;
531 background: transparent;
536 background: transparent;
532 border: 0;
537 border: 0;
533 .box-shadow(none);
538 .box-shadow(none);
534 -webkit-appearance: none;
539 -webkit-appearance: none;
535 }
540 }
536
541
537 .close {
542 .close {
538 float: right;
543 float: right;
539 font-size: 21px;
544 font-size: 21px;
540 font-family: @text-bootstrap;
545 font-family: @text-bootstrap;
541 line-height: 1em;
546 line-height: 1em;
542 font-weight: bold;
547 font-weight: bold;
543 color: @grey2;
548 color: @grey2;
544
549
545 &:hover,
550 &:hover,
546 &:focus {
551 &:focus {
547 color: @grey1;
552 color: @grey1;
548 text-decoration: none;
553 text-decoration: none;
549 cursor: pointer;
554 cursor: pointer;
550 }
555 }
551 }
556 }
552
557
553 // GRID
558 // GRID
554 .sorting,
559 .sorting,
555 .sorting_desc,
560 .sorting_desc,
556 .sorting_asc {
561 .sorting_asc {
557 cursor: pointer;
562 cursor: pointer;
558 }
563 }
559 .sorting_desc:after {
564 .sorting_desc:after {
560 content: "\00A0\25B2";
565 content: "\00A0\25B2";
561 font-size: .75em;
566 font-size: .75em;
562 }
567 }
563 .sorting_asc:after {
568 .sorting_asc:after {
564 content: "\00A0\25BC";
569 content: "\00A0\25BC";
565 font-size: .68em;
570 font-size: .68em;
566 }
571 }
567
572
568
573
569 .user_auth_tokens {
574 .user_auth_tokens {
570
575
571 &.truncate {
576 &.truncate {
572 white-space: nowrap;
577 white-space: nowrap;
573 overflow: hidden;
578 overflow: hidden;
574 text-overflow: ellipsis;
579 text-overflow: ellipsis;
575 }
580 }
576
581
577 .fields .field .input {
582 .fields .field .input {
578 margin: 0;
583 margin: 0;
579 }
584 }
580
585
581 input#description {
586 input#description {
582 width: 100px;
587 width: 100px;
583 margin: 0;
588 margin: 0;
584 }
589 }
585
590
586 .drop-menu {
591 .drop-menu {
587 // TODO: johbo: Remove this, should work out of the box when
592 // TODO: johbo: Remove this, should work out of the box when
588 // having multiple inputs inline
593 // having multiple inputs inline
589 margin: 0 0 0 5px;
594 margin: 0 0 0 5px;
590 }
595 }
591 }
596 }
592 #user_list_table {
597 #user_list_table {
593 .closed {
598 .closed {
594 background-color: @grey6;
599 background-color: @grey6;
595 }
600 }
596 }
601 }
597
602
598
603
599 input {
604 input {
600 &.disabled {
605 &.disabled {
601 opacity: .5;
606 opacity: .5;
602 }
607 }
603 }
608 }
604
609
605 // remove extra padding in firefox
610 // remove extra padding in firefox
606 input::-moz-focus-inner { border:0; padding:0 }
611 input::-moz-focus-inner { border:0; padding:0 }
607
612
608 .adjacent input {
613 .adjacent input {
609 margin-bottom: @padding;
614 margin-bottom: @padding;
610 }
615 }
611
616
612 .permissions_boxes {
617 .permissions_boxes {
613 display: block;
618 display: block;
614 }
619 }
615
620
616 //TODO: lisa: this should be in tables
621 //TODO: lisa: this should be in tables
617 .show_more_col {
622 .show_more_col {
618 width: 20px;
623 width: 20px;
619 }
624 }
620
625
621 //FORMS
626 //FORMS
622
627
623 .medium-inline,
628 .medium-inline,
624 input#description.medium-inline {
629 input#description.medium-inline {
625 display: inline;
630 display: inline;
626 width: @medium-inline-input-width;
631 width: @medium-inline-input-width;
627 min-width: 100px;
632 min-width: 100px;
628 }
633 }
629
634
630 select {
635 select {
631 //reset
636 //reset
632 -webkit-appearance: none;
637 -webkit-appearance: none;
633 -moz-appearance: none;
638 -moz-appearance: none;
634
639
635 display: inline-block;
640 display: inline-block;
636 height: 28px;
641 height: 28px;
637 width: auto;
642 width: auto;
638 margin: 0 @padding @padding 0;
643 margin: 0 @padding @padding 0;
639 padding: 0 18px 0 8px;
644 padding: 0 18px 0 8px;
640 line-height:1em;
645 line-height:1em;
641 font-size: @basefontsize;
646 font-size: @basefontsize;
642 border: @border-thickness solid @rcblue;
647 border: @border-thickness solid @rcblue;
643 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
648 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
644 color: @rcblue;
649 color: @rcblue;
645
650
646 &:after {
651 &:after {
647 content: "\00A0\25BE";
652 content: "\00A0\25BE";
648 }
653 }
649
654
650 &:focus {
655 &:focus {
651 outline: none;
656 outline: none;
652 }
657 }
653 }
658 }
654
659
655 option {
660 option {
656 &:focus {
661 &:focus {
657 outline: none;
662 outline: none;
658 }
663 }
659 }
664 }
660
665
661 input,
666 input,
662 textarea {
667 textarea {
663 padding: @input-padding;
668 padding: @input-padding;
664 border: @input-border-thickness solid @border-highlight-color;
669 border: @input-border-thickness solid @border-highlight-color;
665 .border-radius (@border-radius);
670 .border-radius (@border-radius);
666 font-family: @text-light;
671 font-family: @text-light;
667 font-size: @basefontsize;
672 font-size: @basefontsize;
668
673
669 &.input-sm {
674 &.input-sm {
670 padding: 5px;
675 padding: 5px;
671 }
676 }
672
677
673 &#description {
678 &#description {
674 min-width: @input-description-minwidth;
679 min-width: @input-description-minwidth;
675 min-height: 1em;
680 min-height: 1em;
676 padding: 10px;
681 padding: 10px;
677 }
682 }
678 }
683 }
679
684
680 .field-sm {
685 .field-sm {
681 input,
686 input,
682 textarea {
687 textarea {
683 padding: 5px;
688 padding: 5px;
684 }
689 }
685 }
690 }
686
691
687 textarea {
692 textarea {
688 display: block;
693 display: block;
689 clear: both;
694 clear: both;
690 width: 100%;
695 width: 100%;
691 min-height: 100px;
696 min-height: 100px;
692 margin-bottom: @padding;
697 margin-bottom: @padding;
693 .box-sizing(border-box);
698 .box-sizing(border-box);
694 overflow: auto;
699 overflow: auto;
695 }
700 }
696
701
697 label {
702 label {
698 font-family: @text-light;
703 font-family: @text-light;
699 }
704 }
700
705
701 // GRAVATARS
706 // GRAVATARS
702 // centers gravatar on username to the right
707 // centers gravatar on username to the right
703
708
704 .gravatar {
709 .gravatar {
705 display: inline;
710 display: inline;
706 min-width: 16px;
711 min-width: 16px;
707 min-height: 16px;
712 min-height: 16px;
708 margin: -5px 0;
713 margin: -5px 0;
709 padding: 0;
714 padding: 0;
710 line-height: 1em;
715 line-height: 1em;
711 border: 1px solid @grey4;
716 border: 1px solid @grey4;
712 box-sizing: content-box;
717 box-sizing: content-box;
713
718
714 &.gravatar-large {
719 &.gravatar-large {
715 margin: -0.5em .25em -0.5em 0;
720 margin: -0.5em .25em -0.5em 0;
716 }
721 }
717
722
718 & + .user {
723 & + .user {
719 display: inline;
724 display: inline;
720 margin: 0;
725 margin: 0;
721 padding: 0 0 0 .17em;
726 padding: 0 0 0 .17em;
722 line-height: 1em;
727 line-height: 1em;
723 }
728 }
724 }
729 }
725
730
726 .user-inline-data {
731 .user-inline-data {
727 display: inline-block;
732 display: inline-block;
728 float: left;
733 float: left;
729 padding-left: .5em;
734 padding-left: .5em;
730 line-height: 1.3em;
735 line-height: 1.3em;
731 }
736 }
732
737
733 .rc-user { // gravatar + user wrapper
738 .rc-user { // gravatar + user wrapper
734 float: left;
739 float: left;
735 position: relative;
740 position: relative;
736 min-width: 100px;
741 min-width: 100px;
737 max-width: 200px;
742 max-width: 200px;
738 min-height: (@gravatar-size + @border-thickness * 2); // account for border
743 min-height: (@gravatar-size + @border-thickness * 2); // account for border
739 display: block;
744 display: block;
740 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
745 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
741
746
742
747
743 .gravatar {
748 .gravatar {
744 display: block;
749 display: block;
745 position: absolute;
750 position: absolute;
746 top: 0;
751 top: 0;
747 left: 0;
752 left: 0;
748 min-width: @gravatar-size;
753 min-width: @gravatar-size;
749 min-height: @gravatar-size;
754 min-height: @gravatar-size;
750 margin: 0;
755 margin: 0;
751 }
756 }
752
757
753 .user {
758 .user {
754 display: block;
759 display: block;
755 max-width: 175px;
760 max-width: 175px;
756 padding-top: 2px;
761 padding-top: 2px;
757 overflow: hidden;
762 overflow: hidden;
758 text-overflow: ellipsis;
763 text-overflow: ellipsis;
759 }
764 }
760 }
765 }
761
766
762 .gist-gravatar,
767 .gist-gravatar,
763 .journal_container {
768 .journal_container {
764 .gravatar-large {
769 .gravatar-large {
765 margin: 0 .5em -10px 0;
770 margin: 0 .5em -10px 0;
766 }
771 }
767 }
772 }
768
773
769
774
770 // ADMIN SETTINGS
775 // ADMIN SETTINGS
771
776
772 // Tag Patterns
777 // Tag Patterns
773 .tag_patterns {
778 .tag_patterns {
774 .tag_input {
779 .tag_input {
775 margin-bottom: @padding;
780 margin-bottom: @padding;
776 }
781 }
777 }
782 }
778
783
779 .locked_input {
784 .locked_input {
780 position: relative;
785 position: relative;
781
786
782 input {
787 input {
783 display: inline;
788 display: inline;
784 margin: 3px 5px 0px 0px;
789 margin: 3px 5px 0px 0px;
785 }
790 }
786
791
787 br {
792 br {
788 display: none;
793 display: none;
789 }
794 }
790
795
791 .error-message {
796 .error-message {
792 float: left;
797 float: left;
793 width: 100%;
798 width: 100%;
794 }
799 }
795
800
796 .lock_input_button {
801 .lock_input_button {
797 display: inline;
802 display: inline;
798 }
803 }
799
804
800 .help-block {
805 .help-block {
801 clear: both;
806 clear: both;
802 }
807 }
803 }
808 }
804
809
805 // Notifications
810 // Notifications
806
811
807 .notifications_buttons {
812 .notifications_buttons {
808 margin: 0 0 @space 0;
813 margin: 0 0 @space 0;
809 padding: 0;
814 padding: 0;
810
815
811 .btn {
816 .btn {
812 display: inline-block;
817 display: inline-block;
813 }
818 }
814 }
819 }
815
820
816 .notification-list {
821 .notification-list {
817
822
818 div {
823 div {
819 display: inline-block;
824 display: inline-block;
820 vertical-align: middle;
825 vertical-align: middle;
821 }
826 }
822
827
823 .container {
828 .container {
824 display: block;
829 display: block;
825 margin: 0 0 @padding 0;
830 margin: 0 0 @padding 0;
826 }
831 }
827
832
828 .delete-notifications {
833 .delete-notifications {
829 margin-left: @padding;
834 margin-left: @padding;
830 text-align: right;
835 text-align: right;
831 cursor: pointer;
836 cursor: pointer;
832 }
837 }
833
838
834 .read-notifications {
839 .read-notifications {
835 margin-left: @padding/2;
840 margin-left: @padding/2;
836 text-align: right;
841 text-align: right;
837 width: 35px;
842 width: 35px;
838 cursor: pointer;
843 cursor: pointer;
839 }
844 }
840
845
841 .icon-minus-sign {
846 .icon-minus-sign {
842 color: @alert2;
847 color: @alert2;
843 }
848 }
844
849
845 .icon-ok-sign {
850 .icon-ok-sign {
846 color: @alert1;
851 color: @alert1;
847 }
852 }
848 }
853 }
849
854
850 .user_settings {
855 .user_settings {
851 float: left;
856 float: left;
852 clear: both;
857 clear: both;
853 display: block;
858 display: block;
854 width: 100%;
859 width: 100%;
855
860
856 .gravatar_box {
861 .gravatar_box {
857 margin-bottom: @padding;
862 margin-bottom: @padding;
858
863
859 &:after {
864 &:after {
860 content: " ";
865 content: " ";
861 clear: both;
866 clear: both;
862 width: 100%;
867 width: 100%;
863 }
868 }
864 }
869 }
865
870
866 .fields .field {
871 .fields .field {
867 clear: both;
872 clear: both;
868 }
873 }
869 }
874 }
870
875
871 .advanced_settings {
876 .advanced_settings {
872 margin-bottom: @space;
877 margin-bottom: @space;
873
878
874 .help-block {
879 .help-block {
875 margin-left: 0;
880 margin-left: 0;
876 }
881 }
877
882
878 button + .help-block {
883 button + .help-block {
879 margin-top: @padding;
884 margin-top: @padding;
880 }
885 }
881 }
886 }
882
887
883 // admin settings radio buttons and labels
888 // admin settings radio buttons and labels
884 .label-2 {
889 .label-2 {
885 float: left;
890 float: left;
886 width: @label2-width;
891 width: @label2-width;
887
892
888 label {
893 label {
889 color: @grey1;
894 color: @grey1;
890 }
895 }
891 }
896 }
892 .checkboxes {
897 .checkboxes {
893 float: left;
898 float: left;
894 width: @checkboxes-width;
899 width: @checkboxes-width;
895 margin-bottom: @padding;
900 margin-bottom: @padding;
896
901
897 .checkbox {
902 .checkbox {
898 width: 100%;
903 width: 100%;
899
904
900 label {
905 label {
901 margin: 0;
906 margin: 0;
902 padding: 0;
907 padding: 0;
903 }
908 }
904 }
909 }
905
910
906 .checkbox + .checkbox {
911 .checkbox + .checkbox {
907 display: inline-block;
912 display: inline-block;
908 }
913 }
909
914
910 label {
915 label {
911 margin-right: 1em;
916 margin-right: 1em;
912 }
917 }
913 }
918 }
914
919
915 // CHANGELOG
920 // CHANGELOG
916 .container_header {
921 .container_header {
917 float: left;
922 float: left;
918 display: block;
923 display: block;
919 width: 100%;
924 width: 100%;
920 margin: @padding 0 @padding;
925 margin: @padding 0 @padding;
921
926
922 #filter_changelog {
927 #filter_changelog {
923 float: left;
928 float: left;
924 margin-right: @padding;
929 margin-right: @padding;
925 }
930 }
926
931
927 .breadcrumbs_light {
932 .breadcrumbs_light {
928 display: inline-block;
933 display: inline-block;
929 }
934 }
930 }
935 }
931
936
932 .info_box {
937 .info_box {
933 float: right;
938 float: right;
934 }
939 }
935
940
936
941
937 #graph_nodes {
942 #graph_nodes {
938 padding-top: 43px;
943 padding-top: 43px;
939 }
944 }
940
945
941 #graph_content{
946 #graph_content{
942
947
943 // adjust for table headers so that graph renders properly
948 // adjust for table headers so that graph renders properly
944 // #graph_nodes padding - table cell padding
949 // #graph_nodes padding - table cell padding
945 padding-top: (@space - (@basefontsize * 2.4));
950 padding-top: (@space - (@basefontsize * 2.4));
946
951
947 &.graph_full_width {
952 &.graph_full_width {
948 width: 100%;
953 width: 100%;
949 max-width: 100%;
954 max-width: 100%;
950 }
955 }
951 }
956 }
952
957
953 #graph {
958 #graph {
954 .flag_status {
959 .flag_status {
955 margin: 0;
960 margin: 0;
956 }
961 }
957
962
958 .pagination-left {
963 .pagination-left {
959 float: left;
964 float: left;
960 clear: both;
965 clear: both;
961 }
966 }
962
967
963 .log-container {
968 .log-container {
964 max-width: 345px;
969 max-width: 345px;
965
970
966 .message{
971 .message{
967 max-width: 340px;
972 max-width: 340px;
968 }
973 }
969 }
974 }
970
975
971 .graph-col-wrapper {
976 .graph-col-wrapper {
972 padding-left: 110px;
977 padding-left: 110px;
973
978
974 #graph_nodes {
979 #graph_nodes {
975 width: 100px;
980 width: 100px;
976 margin-left: -110px;
981 margin-left: -110px;
977 float: left;
982 float: left;
978 clear: left;
983 clear: left;
979 }
984 }
980 }
985 }
981
986
982 .load-more-commits {
987 .load-more-commits {
983 text-align: center;
988 text-align: center;
984 }
989 }
985 .load-more-commits:hover {
990 .load-more-commits:hover {
986 background-color: @grey7;
991 background-color: @grey7;
987 }
992 }
988 .load-more-commits {
993 .load-more-commits {
989 a {
994 a {
990 display: block;
995 display: block;
991 }
996 }
992 }
997 }
993 }
998 }
994
999
995 #filter_changelog {
1000 #filter_changelog {
996 float: left;
1001 float: left;
997 }
1002 }
998
1003
999
1004
1000 //--- THEME ------------------//
1005 //--- THEME ------------------//
1001
1006
1002 #logo {
1007 #logo {
1003 float: left;
1008 float: left;
1004 margin: 9px 0 0 0;
1009 margin: 9px 0 0 0;
1005
1010
1006 .header {
1011 .header {
1007 background-color: transparent;
1012 background-color: transparent;
1008 }
1013 }
1009
1014
1010 a {
1015 a {
1011 display: inline-block;
1016 display: inline-block;
1012 }
1017 }
1013
1018
1014 img {
1019 img {
1015 height:30px;
1020 height:30px;
1016 }
1021 }
1017 }
1022 }
1018
1023
1019 .logo-wrapper {
1024 .logo-wrapper {
1020 float:left;
1025 float:left;
1021 }
1026 }
1022
1027
1023 .branding{
1028 .branding{
1024 float: left;
1029 float: left;
1025 padding: 9px 2px;
1030 padding: 9px 2px;
1026 line-height: 1em;
1031 line-height: 1em;
1027 font-size: @navigation-fontsize;
1032 font-size: @navigation-fontsize;
1028 }
1033 }
1029
1034
1030 img {
1035 img {
1031 border: none;
1036 border: none;
1032 outline: none;
1037 outline: none;
1033 }
1038 }
1034 user-profile-header
1039 user-profile-header
1035 label {
1040 label {
1036
1041
1037 input[type="checkbox"] {
1042 input[type="checkbox"] {
1038 margin-right: 1em;
1043 margin-right: 1em;
1039 }
1044 }
1040 input[type="radio"] {
1045 input[type="radio"] {
1041 margin-right: 1em;
1046 margin-right: 1em;
1042 }
1047 }
1043 }
1048 }
1044
1049
1045 .flag_status {
1050 .flag_status {
1046 margin: 2px 8px 6px 2px;
1051 margin: 2px 8px 6px 2px;
1047 &.under_review {
1052 &.under_review {
1048 .circle(5px, @alert3);
1053 .circle(5px, @alert3);
1049 }
1054 }
1050 &.approved {
1055 &.approved {
1051 .circle(5px, @alert1);
1056 .circle(5px, @alert1);
1052 }
1057 }
1053 &.rejected,
1058 &.rejected,
1054 &.forced_closed{
1059 &.forced_closed{
1055 .circle(5px, @alert2);
1060 .circle(5px, @alert2);
1056 }
1061 }
1057 &.not_reviewed {
1062 &.not_reviewed {
1058 .circle(5px, @grey5);
1063 .circle(5px, @grey5);
1059 }
1064 }
1060 }
1065 }
1061
1066
1062 .flag_status_comment_box {
1067 .flag_status_comment_box {
1063 margin: 5px 6px 0px 2px;
1068 margin: 5px 6px 0px 2px;
1064 }
1069 }
1065 .test_pattern_preview {
1070 .test_pattern_preview {
1066 margin: @space 0;
1071 margin: @space 0;
1067
1072
1068 p {
1073 p {
1069 margin-bottom: 0;
1074 margin-bottom: 0;
1070 border-bottom: @border-thickness solid @border-default-color;
1075 border-bottom: @border-thickness solid @border-default-color;
1071 color: @grey3;
1076 color: @grey3;
1072 }
1077 }
1073
1078
1074 .btn {
1079 .btn {
1075 margin-bottom: @padding;
1080 margin-bottom: @padding;
1076 }
1081 }
1077 }
1082 }
1078 #test_pattern_result {
1083 #test_pattern_result {
1079 display: none;
1084 display: none;
1080 &:extend(pre);
1085 &:extend(pre);
1081 padding: .9em;
1086 padding: .9em;
1082 color: @grey3;
1087 color: @grey3;
1083 background-color: @grey7;
1088 background-color: @grey7;
1084 border-right: @border-thickness solid @border-default-color;
1089 border-right: @border-thickness solid @border-default-color;
1085 border-bottom: @border-thickness solid @border-default-color;
1090 border-bottom: @border-thickness solid @border-default-color;
1086 border-left: @border-thickness solid @border-default-color;
1091 border-left: @border-thickness solid @border-default-color;
1087 }
1092 }
1088
1093
1089 #repo_vcs_settings {
1094 #repo_vcs_settings {
1090 #inherit_overlay_vcs_default {
1095 #inherit_overlay_vcs_default {
1091 display: none;
1096 display: none;
1092 }
1097 }
1093 #inherit_overlay_vcs_custom {
1098 #inherit_overlay_vcs_custom {
1094 display: custom;
1099 display: custom;
1095 }
1100 }
1096 &.inherited {
1101 &.inherited {
1097 #inherit_overlay_vcs_default {
1102 #inherit_overlay_vcs_default {
1098 display: block;
1103 display: block;
1099 }
1104 }
1100 #inherit_overlay_vcs_custom {
1105 #inherit_overlay_vcs_custom {
1101 display: none;
1106 display: none;
1102 }
1107 }
1103 }
1108 }
1104 }
1109 }
1105
1110
1106 .issue-tracker-link {
1111 .issue-tracker-link {
1107 color: @rcblue;
1112 color: @rcblue;
1108 }
1113 }
1109
1114
1110 // Issue Tracker Table Show/Hide
1115 // Issue Tracker Table Show/Hide
1111 #repo_issue_tracker {
1116 #repo_issue_tracker {
1112 #inherit_overlay {
1117 #inherit_overlay {
1113 display: none;
1118 display: none;
1114 }
1119 }
1115 #custom_overlay {
1120 #custom_overlay {
1116 display: custom;
1121 display: custom;
1117 }
1122 }
1118 &.inherited {
1123 &.inherited {
1119 #inherit_overlay {
1124 #inherit_overlay {
1120 display: block;
1125 display: block;
1121 }
1126 }
1122 #custom_overlay {
1127 #custom_overlay {
1123 display: none;
1128 display: none;
1124 }
1129 }
1125 }
1130 }
1126 }
1131 }
1127 table.issuetracker {
1132 table.issuetracker {
1128 &.readonly {
1133 &.readonly {
1129 tr, td {
1134 tr, td {
1130 color: @grey3;
1135 color: @grey3;
1131 }
1136 }
1132 }
1137 }
1133 .edit {
1138 .edit {
1134 display: none;
1139 display: none;
1135 }
1140 }
1136 .editopen {
1141 .editopen {
1137 .edit {
1142 .edit {
1138 display: inline;
1143 display: inline;
1139 }
1144 }
1140 .entry {
1145 .entry {
1141 display: none;
1146 display: none;
1142 }
1147 }
1143 }
1148 }
1144 tr td.td-action {
1149 tr td.td-action {
1145 min-width: 117px;
1150 min-width: 117px;
1146 }
1151 }
1147 td input {
1152 td input {
1148 max-width: none;
1153 max-width: none;
1149 min-width: 30px;
1154 min-width: 30px;
1150 width: 80%;
1155 width: 80%;
1151 }
1156 }
1152 .issuetracker_pref input {
1157 .issuetracker_pref input {
1153 width: 40%;
1158 width: 40%;
1154 }
1159 }
1155 input.edit_issuetracker_update {
1160 input.edit_issuetracker_update {
1156 margin-right: 0;
1161 margin-right: 0;
1157 width: auto;
1162 width: auto;
1158 }
1163 }
1159 }
1164 }
1160
1165
1161 table.integrations {
1166 table.integrations {
1162 .td-icon {
1167 .td-icon {
1163 width: 20px;
1168 width: 20px;
1164 .integration-icon {
1169 .integration-icon {
1165 height: 20px;
1170 height: 20px;
1166 width: 20px;
1171 width: 20px;
1167 }
1172 }
1168 }
1173 }
1169 }
1174 }
1170
1175
1171 .integrations {
1176 .integrations {
1172 a.integration-box {
1177 a.integration-box {
1173 color: @text-color;
1178 color: @text-color;
1174 &:hover {
1179 &:hover {
1175 .panel {
1180 .panel {
1176 background: #fbfbfb;
1181 background: #fbfbfb;
1177 }
1182 }
1178 }
1183 }
1179 .integration-icon {
1184 .integration-icon {
1180 width: 30px;
1185 width: 30px;
1181 height: 30px;
1186 height: 30px;
1182 margin-right: 20px;
1187 margin-right: 20px;
1183 float: left;
1188 float: left;
1184 }
1189 }
1185
1190
1186 .panel-body {
1191 .panel-body {
1187 padding: 10px;
1192 padding: 10px;
1188 }
1193 }
1189 .panel {
1194 .panel {
1190 margin-bottom: 10px;
1195 margin-bottom: 10px;
1191 }
1196 }
1192 h2 {
1197 h2 {
1193 display: inline-block;
1198 display: inline-block;
1194 margin: 0;
1199 margin: 0;
1195 min-width: 140px;
1200 min-width: 140px;
1196 }
1201 }
1197 }
1202 }
1198 a.integration-box.dummy-integration {
1203 a.integration-box.dummy-integration {
1199 color: @grey4
1204 color: @grey4
1200 }
1205 }
1201 }
1206 }
1202
1207
1203 //Permissions Settings
1208 //Permissions Settings
1204 #add_perm {
1209 #add_perm {
1205 margin: 0 0 @padding;
1210 margin: 0 0 @padding;
1206 cursor: pointer;
1211 cursor: pointer;
1207 }
1212 }
1208
1213
1209 .perm_ac {
1214 .perm_ac {
1210 input {
1215 input {
1211 width: 95%;
1216 width: 95%;
1212 }
1217 }
1213 }
1218 }
1214
1219
1215 .autocomplete-suggestions {
1220 .autocomplete-suggestions {
1216 width: auto !important; // overrides autocomplete.js
1221 width: auto !important; // overrides autocomplete.js
1217 margin: 0;
1222 margin: 0;
1218 border: @border-thickness solid @rcblue;
1223 border: @border-thickness solid @rcblue;
1219 border-radius: @border-radius;
1224 border-radius: @border-radius;
1220 color: @rcblue;
1225 color: @rcblue;
1221 background-color: white;
1226 background-color: white;
1222 }
1227 }
1223 .autocomplete-selected {
1228 .autocomplete-selected {
1224 background: #F0F0F0;
1229 background: #F0F0F0;
1225 }
1230 }
1226 .ac-container-wrap {
1231 .ac-container-wrap {
1227 margin: 0;
1232 margin: 0;
1228 padding: 8px;
1233 padding: 8px;
1229 border-bottom: @border-thickness solid @rclightblue;
1234 border-bottom: @border-thickness solid @rclightblue;
1230 list-style-type: none;
1235 list-style-type: none;
1231 cursor: pointer;
1236 cursor: pointer;
1232
1237
1233 &:hover {
1238 &:hover {
1234 background-color: @rclightblue;
1239 background-color: @rclightblue;
1235 }
1240 }
1236
1241
1237 img {
1242 img {
1238 height: @gravatar-size;
1243 height: @gravatar-size;
1239 width: @gravatar-size;
1244 width: @gravatar-size;
1240 margin-right: 1em;
1245 margin-right: 1em;
1241 }
1246 }
1242
1247
1243 strong {
1248 strong {
1244 font-weight: normal;
1249 font-weight: normal;
1245 }
1250 }
1246 }
1251 }
1247
1252
1248 // Settings Dropdown
1253 // Settings Dropdown
1249 .user-menu .container {
1254 .user-menu .container {
1250 padding: 0 4px;
1255 padding: 0 4px;
1251 margin: 0;
1256 margin: 0;
1252 }
1257 }
1253
1258
1254 .user-menu .gravatar {
1259 .user-menu .gravatar {
1255 cursor: pointer;
1260 cursor: pointer;
1256 }
1261 }
1257
1262
1258 .codeblock {
1263 .codeblock {
1259 margin-bottom: @padding;
1264 margin-bottom: @padding;
1260 clear: both;
1265 clear: both;
1261
1266
1262 .stats{
1267 .stats{
1263 overflow: hidden;
1268 overflow: hidden;
1264 }
1269 }
1265
1270
1266 .message{
1271 .message{
1267 textarea{
1272 textarea{
1268 margin: 0;
1273 margin: 0;
1269 }
1274 }
1270 }
1275 }
1271
1276
1272 .code-header {
1277 .code-header {
1273 .stats {
1278 .stats {
1274 line-height: 2em;
1279 line-height: 2em;
1275
1280
1276 .revision_id {
1281 .revision_id {
1277 margin-left: 0;
1282 margin-left: 0;
1278 }
1283 }
1279 .buttons {
1284 .buttons {
1280 padding-right: 0;
1285 padding-right: 0;
1281 }
1286 }
1282 }
1287 }
1283
1288
1284 .item{
1289 .item{
1285 margin-right: 0.5em;
1290 margin-right: 0.5em;
1286 }
1291 }
1287 }
1292 }
1288
1293
1289 #editor_container{
1294 #editor_container{
1290 position: relative;
1295 position: relative;
1291 margin: @padding;
1296 margin: @padding;
1292 }
1297 }
1293 }
1298 }
1294
1299
1295 #file_history_container {
1300 #file_history_container {
1296 display: none;
1301 display: none;
1297 }
1302 }
1298
1303
1299 .file-history-inner {
1304 .file-history-inner {
1300 margin-bottom: 10px;
1305 margin-bottom: 10px;
1301 }
1306 }
1302
1307
1303 // Pull Requests
1308 // Pull Requests
1304 .summary-details {
1309 .summary-details {
1305 width: 72%;
1310 width: 72%;
1306 }
1311 }
1307 .pr-summary {
1312 .pr-summary {
1308 border-bottom: @border-thickness solid @grey5;
1313 border-bottom: @border-thickness solid @grey5;
1309 margin-bottom: @space;
1314 margin-bottom: @space;
1310 }
1315 }
1311 .reviewers-title {
1316 .reviewers-title {
1312 width: 25%;
1317 width: 25%;
1313 min-width: 200px;
1318 min-width: 200px;
1314 }
1319 }
1315 .reviewers {
1320 .reviewers {
1316 width: 25%;
1321 width: 25%;
1317 min-width: 200px;
1322 min-width: 200px;
1318 }
1323 }
1319 .reviewers ul li {
1324 .reviewers ul li {
1320 position: relative;
1325 position: relative;
1321 width: 100%;
1326 width: 100%;
1322 margin-bottom: 8px;
1327 margin-bottom: 8px;
1323 }
1328 }
1324
1329
1325 .reviewer_entry {
1330 .reviewer_entry {
1326 min-height: 55px;
1331 min-height: 55px;
1327 }
1332 }
1328
1333
1329 .reviewers_member {
1334 .reviewers_member {
1330 width: 100%;
1335 width: 100%;
1331 overflow: auto;
1336 overflow: auto;
1332 }
1337 }
1333 .reviewer_reason {
1338 .reviewer_reason {
1334 padding-left: 20px;
1339 padding-left: 20px;
1335 }
1340 }
1336 .reviewer_status {
1341 .reviewer_status {
1337 display: inline-block;
1342 display: inline-block;
1338 vertical-align: top;
1343 vertical-align: top;
1339 width: 7%;
1344 width: 7%;
1340 min-width: 20px;
1345 min-width: 20px;
1341 height: 1.2em;
1346 height: 1.2em;
1342 margin-top: 3px;
1347 margin-top: 3px;
1343 line-height: 1em;
1348 line-height: 1em;
1344 }
1349 }
1345
1350
1346 .reviewer_name {
1351 .reviewer_name {
1347 display: inline-block;
1352 display: inline-block;
1348 max-width: 83%;
1353 max-width: 83%;
1349 padding-right: 20px;
1354 padding-right: 20px;
1350 vertical-align: middle;
1355 vertical-align: middle;
1351 line-height: 1;
1356 line-height: 1;
1352
1357
1353 .rc-user {
1358 .rc-user {
1354 min-width: 0;
1359 min-width: 0;
1355 margin: -2px 1em 0 0;
1360 margin: -2px 1em 0 0;
1356 }
1361 }
1357
1362
1358 .reviewer {
1363 .reviewer {
1359 float: left;
1364 float: left;
1360 }
1365 }
1361 }
1366 }
1362
1367
1363 .reviewer_member_mandatory,
1368 .reviewer_member_mandatory,
1364 .reviewer_member_mandatory_remove,
1369 .reviewer_member_mandatory_remove,
1365 .reviewer_member_remove {
1370 .reviewer_member_remove {
1366 position: absolute;
1371 position: absolute;
1367 right: 0;
1372 right: 0;
1368 top: 0;
1373 top: 0;
1369 width: 16px;
1374 width: 16px;
1370 margin-bottom: 10px;
1375 margin-bottom: 10px;
1371 padding: 0;
1376 padding: 0;
1372 color: black;
1377 color: black;
1373 }
1378 }
1374
1379
1375 .reviewer_member_mandatory_remove {
1380 .reviewer_member_mandatory_remove {
1376 color: @grey4;
1381 color: @grey4;
1377 }
1382 }
1378
1383
1379 .reviewer_member_mandatory {
1384 .reviewer_member_mandatory {
1380 padding-top:20px;
1385 padding-top:20px;
1381 }
1386 }
1382
1387
1383 .reviewer_member_status {
1388 .reviewer_member_status {
1384 margin-top: 5px;
1389 margin-top: 5px;
1385 }
1390 }
1386 .pr-summary #summary{
1391 .pr-summary #summary{
1387 width: 100%;
1392 width: 100%;
1388 }
1393 }
1389 .pr-summary .action_button:hover {
1394 .pr-summary .action_button:hover {
1390 border: 0;
1395 border: 0;
1391 cursor: pointer;
1396 cursor: pointer;
1392 }
1397 }
1393 .pr-details-title {
1398 .pr-details-title {
1394 padding-bottom: 8px;
1399 padding-bottom: 8px;
1395 border-bottom: @border-thickness solid @grey5;
1400 border-bottom: @border-thickness solid @grey5;
1396
1401
1397 .action_button.disabled {
1402 .action_button.disabled {
1398 color: @grey4;
1403 color: @grey4;
1399 cursor: inherit;
1404 cursor: inherit;
1400 }
1405 }
1401 .action_button {
1406 .action_button {
1402 color: @rcblue;
1407 color: @rcblue;
1403 }
1408 }
1404 }
1409 }
1405 .pr-details-content {
1410 .pr-details-content {
1406 margin-top: @textmargin;
1411 margin-top: @textmargin;
1407 margin-bottom: @textmargin;
1412 margin-bottom: @textmargin;
1408 }
1413 }
1409 .pr-description {
1414 .pr-description {
1410 white-space:pre-wrap;
1415 white-space:pre-wrap;
1411 }
1416 }
1412
1417
1413 .pr-reviewer-rules {
1418 .pr-reviewer-rules {
1414 padding: 10px 0px 20px 0px;
1419 padding: 10px 0px 20px 0px;
1415 }
1420 }
1416
1421
1417 .group_members {
1422 .group_members {
1418 margin-top: 0;
1423 margin-top: 0;
1419 padding: 0;
1424 padding: 0;
1420 list-style: outside none none;
1425 list-style: outside none none;
1421
1426
1422 img {
1427 img {
1423 height: @gravatar-size;
1428 height: @gravatar-size;
1424 width: @gravatar-size;
1429 width: @gravatar-size;
1425 margin-right: .5em;
1430 margin-right: .5em;
1426 margin-left: 3px;
1431 margin-left: 3px;
1427 }
1432 }
1428
1433
1429 .to-delete {
1434 .to-delete {
1430 .user {
1435 .user {
1431 text-decoration: line-through;
1436 text-decoration: line-through;
1432 }
1437 }
1433 }
1438 }
1434 }
1439 }
1435
1440
1436 .compare_view_commits_title {
1441 .compare_view_commits_title {
1437 .disabled {
1442 .disabled {
1438 cursor: inherit;
1443 cursor: inherit;
1439 &:hover{
1444 &:hover{
1440 background-color: inherit;
1445 background-color: inherit;
1441 color: inherit;
1446 color: inherit;
1442 }
1447 }
1443 }
1448 }
1444 }
1449 }
1445
1450
1446 .subtitle-compare {
1451 .subtitle-compare {
1447 margin: -15px 0px 0px 0px;
1452 margin: -15px 0px 0px 0px;
1448 }
1453 }
1449
1454
1450 .comments-summary-td {
1455 .comments-summary-td {
1451 border-top: 1px dashed @grey5;
1456 border-top: 1px dashed @grey5;
1452 }
1457 }
1453
1458
1454 // new entry in group_members
1459 // new entry in group_members
1455 .td-author-new-entry {
1460 .td-author-new-entry {
1456 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1461 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1457 }
1462 }
1458
1463
1459 .usergroup_member_remove {
1464 .usergroup_member_remove {
1460 width: 16px;
1465 width: 16px;
1461 margin-bottom: 10px;
1466 margin-bottom: 10px;
1462 padding: 0;
1467 padding: 0;
1463 color: black !important;
1468 color: black !important;
1464 cursor: pointer;
1469 cursor: pointer;
1465 }
1470 }
1466
1471
1467 .reviewer_ac .ac-input {
1472 .reviewer_ac .ac-input {
1468 width: 92%;
1473 width: 92%;
1469 margin-bottom: 1em;
1474 margin-bottom: 1em;
1470 }
1475 }
1471
1476
1472 .compare_view_commits tr{
1477 .compare_view_commits tr{
1473 height: 20px;
1478 height: 20px;
1474 }
1479 }
1475 .compare_view_commits td {
1480 .compare_view_commits td {
1476 vertical-align: top;
1481 vertical-align: top;
1477 padding-top: 10px;
1482 padding-top: 10px;
1478 }
1483 }
1479 .compare_view_commits .author {
1484 .compare_view_commits .author {
1480 margin-left: 5px;
1485 margin-left: 5px;
1481 }
1486 }
1482
1487
1483 .compare_view_commits {
1488 .compare_view_commits {
1484 .color-a {
1489 .color-a {
1485 color: @alert1;
1490 color: @alert1;
1486 }
1491 }
1487
1492
1488 .color-c {
1493 .color-c {
1489 color: @color3;
1494 color: @color3;
1490 }
1495 }
1491
1496
1492 .color-r {
1497 .color-r {
1493 color: @color5;
1498 color: @color5;
1494 }
1499 }
1495
1500
1496 .color-a-bg {
1501 .color-a-bg {
1497 background-color: @alert1;
1502 background-color: @alert1;
1498 }
1503 }
1499
1504
1500 .color-c-bg {
1505 .color-c-bg {
1501 background-color: @alert3;
1506 background-color: @alert3;
1502 }
1507 }
1503
1508
1504 .color-r-bg {
1509 .color-r-bg {
1505 background-color: @alert2;
1510 background-color: @alert2;
1506 }
1511 }
1507
1512
1508 .color-a-border {
1513 .color-a-border {
1509 border: 1px solid @alert1;
1514 border: 1px solid @alert1;
1510 }
1515 }
1511
1516
1512 .color-c-border {
1517 .color-c-border {
1513 border: 1px solid @alert3;
1518 border: 1px solid @alert3;
1514 }
1519 }
1515
1520
1516 .color-r-border {
1521 .color-r-border {
1517 border: 1px solid @alert2;
1522 border: 1px solid @alert2;
1518 }
1523 }
1519
1524
1520 .commit-change-indicator {
1525 .commit-change-indicator {
1521 width: 15px;
1526 width: 15px;
1522 height: 15px;
1527 height: 15px;
1523 position: relative;
1528 position: relative;
1524 left: 15px;
1529 left: 15px;
1525 }
1530 }
1526
1531
1527 .commit-change-content {
1532 .commit-change-content {
1528 text-align: center;
1533 text-align: center;
1529 vertical-align: middle;
1534 vertical-align: middle;
1530 line-height: 15px;
1535 line-height: 15px;
1531 }
1536 }
1532 }
1537 }
1533
1538
1534 .compare_view_files {
1539 .compare_view_files {
1535 width: 100%;
1540 width: 100%;
1536
1541
1537 td {
1542 td {
1538 vertical-align: middle;
1543 vertical-align: middle;
1539 }
1544 }
1540 }
1545 }
1541
1546
1542 .compare_view_filepath {
1547 .compare_view_filepath {
1543 color: @grey1;
1548 color: @grey1;
1544 }
1549 }
1545
1550
1546 .show_more {
1551 .show_more {
1547 display: inline-block;
1552 display: inline-block;
1548 position: relative;
1553 position: relative;
1549 vertical-align: middle;
1554 vertical-align: middle;
1550 width: 4px;
1555 width: 4px;
1551 height: @basefontsize;
1556 height: @basefontsize;
1552
1557
1553 &:after {
1558 &:after {
1554 content: "\00A0\25BE";
1559 content: "\00A0\25BE";
1555 display: inline-block;
1560 display: inline-block;
1556 width:10px;
1561 width:10px;
1557 line-height: 5px;
1562 line-height: 5px;
1558 font-size: 12px;
1563 font-size: 12px;
1559 cursor: pointer;
1564 cursor: pointer;
1560 }
1565 }
1561 }
1566 }
1562
1567
1563 .journal_more .show_more {
1568 .journal_more .show_more {
1564 display: inline;
1569 display: inline;
1565
1570
1566 &:after {
1571 &:after {
1567 content: none;
1572 content: none;
1568 }
1573 }
1569 }
1574 }
1570
1575
1571 .open .show_more:after,
1576 .open .show_more:after,
1572 .select2-dropdown-open .show_more:after {
1577 .select2-dropdown-open .show_more:after {
1573 .rotate(180deg);
1578 .rotate(180deg);
1574 margin-left: 4px;
1579 margin-left: 4px;
1575 }
1580 }
1576
1581
1577
1582
1578 .compare_view_commits .collapse_commit:after {
1583 .compare_view_commits .collapse_commit:after {
1579 cursor: pointer;
1584 cursor: pointer;
1580 content: "\00A0\25B4";
1585 content: "\00A0\25B4";
1581 margin-left: -3px;
1586 margin-left: -3px;
1582 font-size: 17px;
1587 font-size: 17px;
1583 color: @grey4;
1588 color: @grey4;
1584 }
1589 }
1585
1590
1586 .diff_links {
1591 .diff_links {
1587 margin-left: 8px;
1592 margin-left: 8px;
1588 }
1593 }
1589
1594
1590 div.ancestor {
1595 div.ancestor {
1591 margin: -30px 0px;
1596 margin: -30px 0px;
1592 }
1597 }
1593
1598
1594 .cs_icon_td input[type="checkbox"] {
1599 .cs_icon_td input[type="checkbox"] {
1595 display: none;
1600 display: none;
1596 }
1601 }
1597
1602
1598 .cs_icon_td .expand_file_icon:after {
1603 .cs_icon_td .expand_file_icon:after {
1599 cursor: pointer;
1604 cursor: pointer;
1600 content: "\00A0\25B6";
1605 content: "\00A0\25B6";
1601 font-size: 12px;
1606 font-size: 12px;
1602 color: @grey4;
1607 color: @grey4;
1603 }
1608 }
1604
1609
1605 .cs_icon_td .collapse_file_icon:after {
1610 .cs_icon_td .collapse_file_icon:after {
1606 cursor: pointer;
1611 cursor: pointer;
1607 content: "\00A0\25BC";
1612 content: "\00A0\25BC";
1608 font-size: 12px;
1613 font-size: 12px;
1609 color: @grey4;
1614 color: @grey4;
1610 }
1615 }
1611
1616
1612 /*new binary
1617 /*new binary
1613 NEW_FILENODE = 1
1618 NEW_FILENODE = 1
1614 DEL_FILENODE = 2
1619 DEL_FILENODE = 2
1615 MOD_FILENODE = 3
1620 MOD_FILENODE = 3
1616 RENAMED_FILENODE = 4
1621 RENAMED_FILENODE = 4
1617 COPIED_FILENODE = 5
1622 COPIED_FILENODE = 5
1618 CHMOD_FILENODE = 6
1623 CHMOD_FILENODE = 6
1619 BIN_FILENODE = 7
1624 BIN_FILENODE = 7
1620 */
1625 */
1621 .cs_files_expand {
1626 .cs_files_expand {
1622 font-size: @basefontsize + 5px;
1627 font-size: @basefontsize + 5px;
1623 line-height: 1.8em;
1628 line-height: 1.8em;
1624 float: right;
1629 float: right;
1625 }
1630 }
1626
1631
1627 .cs_files_expand span{
1632 .cs_files_expand span{
1628 color: @rcblue;
1633 color: @rcblue;
1629 cursor: pointer;
1634 cursor: pointer;
1630 }
1635 }
1631 .cs_files {
1636 .cs_files {
1632 clear: both;
1637 clear: both;
1633 padding-bottom: @padding;
1638 padding-bottom: @padding;
1634
1639
1635 .cur_cs {
1640 .cur_cs {
1636 margin: 10px 2px;
1641 margin: 10px 2px;
1637 font-weight: bold;
1642 font-weight: bold;
1638 }
1643 }
1639
1644
1640 .node {
1645 .node {
1641 float: left;
1646 float: left;
1642 }
1647 }
1643
1648
1644 .changes {
1649 .changes {
1645 float: right;
1650 float: right;
1646 color: white;
1651 color: white;
1647 font-size: @basefontsize - 4px;
1652 font-size: @basefontsize - 4px;
1648 margin-top: 4px;
1653 margin-top: 4px;
1649 opacity: 0.6;
1654 opacity: 0.6;
1650 filter: Alpha(opacity=60); /* IE8 and earlier */
1655 filter: Alpha(opacity=60); /* IE8 and earlier */
1651
1656
1652 .added {
1657 .added {
1653 background-color: @alert1;
1658 background-color: @alert1;
1654 float: left;
1659 float: left;
1655 text-align: center;
1660 text-align: center;
1656 }
1661 }
1657
1662
1658 .deleted {
1663 .deleted {
1659 background-color: @alert2;
1664 background-color: @alert2;
1660 float: left;
1665 float: left;
1661 text-align: center;
1666 text-align: center;
1662 }
1667 }
1663
1668
1664 .bin {
1669 .bin {
1665 background-color: @alert1;
1670 background-color: @alert1;
1666 text-align: center;
1671 text-align: center;
1667 }
1672 }
1668
1673
1669 /*new binary*/
1674 /*new binary*/
1670 .bin.bin1 {
1675 .bin.bin1 {
1671 background-color: @alert1;
1676 background-color: @alert1;
1672 text-align: center;
1677 text-align: center;
1673 }
1678 }
1674
1679
1675 /*deleted binary*/
1680 /*deleted binary*/
1676 .bin.bin2 {
1681 .bin.bin2 {
1677 background-color: @alert2;
1682 background-color: @alert2;
1678 text-align: center;
1683 text-align: center;
1679 }
1684 }
1680
1685
1681 /*mod binary*/
1686 /*mod binary*/
1682 .bin.bin3 {
1687 .bin.bin3 {
1683 background-color: @grey2;
1688 background-color: @grey2;
1684 text-align: center;
1689 text-align: center;
1685 }
1690 }
1686
1691
1687 /*rename file*/
1692 /*rename file*/
1688 .bin.bin4 {
1693 .bin.bin4 {
1689 background-color: @alert4;
1694 background-color: @alert4;
1690 text-align: center;
1695 text-align: center;
1691 }
1696 }
1692
1697
1693 /*copied file*/
1698 /*copied file*/
1694 .bin.bin5 {
1699 .bin.bin5 {
1695 background-color: @alert4;
1700 background-color: @alert4;
1696 text-align: center;
1701 text-align: center;
1697 }
1702 }
1698
1703
1699 /*chmod file*/
1704 /*chmod file*/
1700 .bin.bin6 {
1705 .bin.bin6 {
1701 background-color: @grey2;
1706 background-color: @grey2;
1702 text-align: center;
1707 text-align: center;
1703 }
1708 }
1704 }
1709 }
1705 }
1710 }
1706
1711
1707 .cs_files .cs_added, .cs_files .cs_A,
1712 .cs_files .cs_added, .cs_files .cs_A,
1708 .cs_files .cs_added, .cs_files .cs_M,
1713 .cs_files .cs_added, .cs_files .cs_M,
1709 .cs_files .cs_added, .cs_files .cs_D {
1714 .cs_files .cs_added, .cs_files .cs_D {
1710 height: 16px;
1715 height: 16px;
1711 padding-right: 10px;
1716 padding-right: 10px;
1712 margin-top: 7px;
1717 margin-top: 7px;
1713 text-align: left;
1718 text-align: left;
1714 }
1719 }
1715
1720
1716 .cs_icon_td {
1721 .cs_icon_td {
1717 min-width: 16px;
1722 min-width: 16px;
1718 width: 16px;
1723 width: 16px;
1719 }
1724 }
1720
1725
1721 .pull-request-merge {
1726 .pull-request-merge {
1722 border: 1px solid @grey5;
1727 border: 1px solid @grey5;
1723 padding: 10px 0px 20px;
1728 padding: 10px 0px 20px;
1724 margin-top: 10px;
1729 margin-top: 10px;
1725 margin-bottom: 20px;
1730 margin-bottom: 20px;
1726 }
1731 }
1727
1732
1728 .pull-request-merge ul {
1733 .pull-request-merge ul {
1729 padding: 0px 0px;
1734 padding: 0px 0px;
1730 }
1735 }
1731
1736
1732 .pull-request-merge li:before{
1737 .pull-request-merge li:before{
1733 content:none;
1738 content:none;
1734 }
1739 }
1735
1740
1736 .pull-request-merge .pull-request-wrap {
1741 .pull-request-merge .pull-request-wrap {
1737 height: auto;
1742 height: auto;
1738 padding: 0px 0px;
1743 padding: 0px 0px;
1739 text-align: right;
1744 text-align: right;
1740 }
1745 }
1741
1746
1742 .pull-request-merge span {
1747 .pull-request-merge span {
1743 margin-right: 5px;
1748 margin-right: 5px;
1744 }
1749 }
1745
1750
1746 .pull-request-merge-actions {
1751 .pull-request-merge-actions {
1747 min-height: 30px;
1752 min-height: 30px;
1748 padding: 0px 0px;
1753 padding: 0px 0px;
1749 }
1754 }
1750
1755
1751 .pull-request-merge-info {
1756 .pull-request-merge-info {
1752 padding: 0px 5px 5px 0px;
1757 padding: 0px 5px 5px 0px;
1753 }
1758 }
1754
1759
1755 .merge-status {
1760 .merge-status {
1756 margin-right: 5px;
1761 margin-right: 5px;
1757 }
1762 }
1758
1763
1759 .merge-message {
1764 .merge-message {
1760 font-size: 1.2em
1765 font-size: 1.2em
1761 }
1766 }
1762
1767
1763 .merge-message.success i,
1768 .merge-message.success i,
1764 .merge-icon.success i {
1769 .merge-icon.success i {
1765 color:@alert1;
1770 color:@alert1;
1766 }
1771 }
1767
1772
1768 .merge-message.warning i,
1773 .merge-message.warning i,
1769 .merge-icon.warning i {
1774 .merge-icon.warning i {
1770 color: @alert3;
1775 color: @alert3;
1771 }
1776 }
1772
1777
1773 .merge-message.error i,
1778 .merge-message.error i,
1774 .merge-icon.error i {
1779 .merge-icon.error i {
1775 color:@alert2;
1780 color:@alert2;
1776 }
1781 }
1777
1782
1778 .pr-versions {
1783 .pr-versions {
1779 font-size: 1.1em;
1784 font-size: 1.1em;
1780
1785
1781 table {
1786 table {
1782 padding: 0px 5px;
1787 padding: 0px 5px;
1783 }
1788 }
1784
1789
1785 td {
1790 td {
1786 line-height: 15px;
1791 line-height: 15px;
1787 }
1792 }
1788
1793
1789 .flag_status {
1794 .flag_status {
1790 margin: 0;
1795 margin: 0;
1791 }
1796 }
1792
1797
1793 .compare-radio-button {
1798 .compare-radio-button {
1794 position: relative;
1799 position: relative;
1795 top: -3px;
1800 top: -3px;
1796 }
1801 }
1797 }
1802 }
1798
1803
1799
1804
1800 #close_pull_request {
1805 #close_pull_request {
1801 margin-right: 0px;
1806 margin-right: 0px;
1802 }
1807 }
1803
1808
1804 .empty_data {
1809 .empty_data {
1805 color: @grey4;
1810 color: @grey4;
1806 }
1811 }
1807
1812
1808 #changeset_compare_view_content {
1813 #changeset_compare_view_content {
1809 margin-bottom: @space;
1814 margin-bottom: @space;
1810 clear: both;
1815 clear: both;
1811 width: 100%;
1816 width: 100%;
1812 box-sizing: border-box;
1817 box-sizing: border-box;
1813 .border-radius(@border-radius);
1818 .border-radius(@border-radius);
1814
1819
1815 .help-block {
1820 .help-block {
1816 margin: @padding 0;
1821 margin: @padding 0;
1817 color: @text-color;
1822 color: @text-color;
1818 &.pre-formatting {
1823 &.pre-formatting {
1819 white-space: pre;
1824 white-space: pre;
1820 }
1825 }
1821 }
1826 }
1822
1827
1823 .empty_data {
1828 .empty_data {
1824 margin: @padding 0;
1829 margin: @padding 0;
1825 }
1830 }
1826
1831
1827 .alert {
1832 .alert {
1828 margin-bottom: @space;
1833 margin-bottom: @space;
1829 }
1834 }
1830 }
1835 }
1831
1836
1832 .table_disp {
1837 .table_disp {
1833 .status {
1838 .status {
1834 width: auto;
1839 width: auto;
1835
1840
1836 .flag_status {
1841 .flag_status {
1837 float: left;
1842 float: left;
1838 }
1843 }
1839 }
1844 }
1840 }
1845 }
1841
1846
1842
1847
1843 .creation_in_progress {
1848 .creation_in_progress {
1844 color: @grey4
1849 color: @grey4
1845 }
1850 }
1846
1851
1847 .status_box_menu {
1852 .status_box_menu {
1848 margin: 0;
1853 margin: 0;
1849 }
1854 }
1850
1855
1851 .notification-table{
1856 .notification-table{
1852 margin-bottom: @space;
1857 margin-bottom: @space;
1853 display: table;
1858 display: table;
1854 width: 100%;
1859 width: 100%;
1855
1860
1856 .container{
1861 .container{
1857 display: table-row;
1862 display: table-row;
1858
1863
1859 .notification-header{
1864 .notification-header{
1860 border-bottom: @border-thickness solid @border-default-color;
1865 border-bottom: @border-thickness solid @border-default-color;
1861 }
1866 }
1862
1867
1863 .notification-subject{
1868 .notification-subject{
1864 display: table-cell;
1869 display: table-cell;
1865 }
1870 }
1866 }
1871 }
1867 }
1872 }
1868
1873
1869 // Notifications
1874 // Notifications
1870 .notification-header{
1875 .notification-header{
1871 display: table;
1876 display: table;
1872 width: 100%;
1877 width: 100%;
1873 padding: floor(@basefontsize/2) 0;
1878 padding: floor(@basefontsize/2) 0;
1874 line-height: 1em;
1879 line-height: 1em;
1875
1880
1876 .desc, .delete-notifications, .read-notifications{
1881 .desc, .delete-notifications, .read-notifications{
1877 display: table-cell;
1882 display: table-cell;
1878 text-align: left;
1883 text-align: left;
1879 }
1884 }
1880
1885
1881 .desc{
1886 .desc{
1882 width: 1163px;
1887 width: 1163px;
1883 }
1888 }
1884
1889
1885 .delete-notifications, .read-notifications{
1890 .delete-notifications, .read-notifications{
1886 width: 35px;
1891 width: 35px;
1887 min-width: 35px; //fixes when only one button is displayed
1892 min-width: 35px; //fixes when only one button is displayed
1888 }
1893 }
1889 }
1894 }
1890
1895
1891 .notification-body {
1896 .notification-body {
1892 .markdown-block,
1897 .markdown-block,
1893 .rst-block {
1898 .rst-block {
1894 padding: @padding 0;
1899 padding: @padding 0;
1895 }
1900 }
1896
1901
1897 .notification-subject {
1902 .notification-subject {
1898 padding: @textmargin 0;
1903 padding: @textmargin 0;
1899 border-bottom: @border-thickness solid @border-default-color;
1904 border-bottom: @border-thickness solid @border-default-color;
1900 }
1905 }
1901 }
1906 }
1902
1907
1903
1908
1904 .notifications_buttons{
1909 .notifications_buttons{
1905 float: right;
1910 float: right;
1906 }
1911 }
1907
1912
1908 #notification-status{
1913 #notification-status{
1909 display: inline;
1914 display: inline;
1910 }
1915 }
1911
1916
1912 // Repositories
1917 // Repositories
1913
1918
1914 #summary.fields{
1919 #summary.fields{
1915 display: table;
1920 display: table;
1916
1921
1917 .field{
1922 .field{
1918 display: table-row;
1923 display: table-row;
1919
1924
1920 .label-summary{
1925 .label-summary{
1921 display: table-cell;
1926 display: table-cell;
1922 min-width: @label-summary-minwidth;
1927 min-width: @label-summary-minwidth;
1923 padding-top: @padding/2;
1928 padding-top: @padding/2;
1924 padding-bottom: @padding/2;
1929 padding-bottom: @padding/2;
1925 padding-right: @padding/2;
1930 padding-right: @padding/2;
1926 }
1931 }
1927
1932
1928 .input{
1933 .input{
1929 display: table-cell;
1934 display: table-cell;
1930 padding: @padding/2;
1935 padding: @padding/2;
1931
1936
1932 input{
1937 input{
1933 min-width: 29em;
1938 min-width: 29em;
1934 padding: @padding/4;
1939 padding: @padding/4;
1935 }
1940 }
1936 }
1941 }
1937 .statistics, .downloads{
1942 .statistics, .downloads{
1938 .disabled{
1943 .disabled{
1939 color: @grey4;
1944 color: @grey4;
1940 }
1945 }
1941 }
1946 }
1942 }
1947 }
1943 }
1948 }
1944
1949
1945 #summary{
1950 #summary{
1946 width: 70%;
1951 width: 70%;
1947 }
1952 }
1948
1953
1949
1954
1950 // Journal
1955 // Journal
1951 .journal.title {
1956 .journal.title {
1952 h5 {
1957 h5 {
1953 float: left;
1958 float: left;
1954 margin: 0;
1959 margin: 0;
1955 width: 70%;
1960 width: 70%;
1956 }
1961 }
1957
1962
1958 ul {
1963 ul {
1959 float: right;
1964 float: right;
1960 display: inline-block;
1965 display: inline-block;
1961 margin: 0;
1966 margin: 0;
1962 width: 30%;
1967 width: 30%;
1963 text-align: right;
1968 text-align: right;
1964
1969
1965 li {
1970 li {
1966 display: inline;
1971 display: inline;
1967 font-size: @journal-fontsize;
1972 font-size: @journal-fontsize;
1968 line-height: 1em;
1973 line-height: 1em;
1969
1974
1970 &:before { content: none; }
1975 &:before { content: none; }
1971 }
1976 }
1972 }
1977 }
1973 }
1978 }
1974
1979
1975 .filterexample {
1980 .filterexample {
1976 position: absolute;
1981 position: absolute;
1977 top: 95px;
1982 top: 95px;
1978 left: @contentpadding;
1983 left: @contentpadding;
1979 color: @rcblue;
1984 color: @rcblue;
1980 font-size: 11px;
1985 font-size: 11px;
1981 font-family: @text-regular;
1986 font-family: @text-regular;
1982 cursor: help;
1987 cursor: help;
1983
1988
1984 &:hover {
1989 &:hover {
1985 color: @rcdarkblue;
1990 color: @rcdarkblue;
1986 }
1991 }
1987
1992
1988 @media (max-width:768px) {
1993 @media (max-width:768px) {
1989 position: relative;
1994 position: relative;
1990 top: auto;
1995 top: auto;
1991 left: auto;
1996 left: auto;
1992 display: block;
1997 display: block;
1993 }
1998 }
1994 }
1999 }
1995
2000
1996
2001
1997 #journal{
2002 #journal{
1998 margin-bottom: @space;
2003 margin-bottom: @space;
1999
2004
2000 .journal_day{
2005 .journal_day{
2001 margin-bottom: @textmargin/2;
2006 margin-bottom: @textmargin/2;
2002 padding-bottom: @textmargin/2;
2007 padding-bottom: @textmargin/2;
2003 font-size: @journal-fontsize;
2008 font-size: @journal-fontsize;
2004 border-bottom: @border-thickness solid @border-default-color;
2009 border-bottom: @border-thickness solid @border-default-color;
2005 }
2010 }
2006
2011
2007 .journal_container{
2012 .journal_container{
2008 margin-bottom: @space;
2013 margin-bottom: @space;
2009
2014
2010 .journal_user{
2015 .journal_user{
2011 display: inline-block;
2016 display: inline-block;
2012 }
2017 }
2013 .journal_action_container{
2018 .journal_action_container{
2014 display: block;
2019 display: block;
2015 margin-top: @textmargin;
2020 margin-top: @textmargin;
2016
2021
2017 div{
2022 div{
2018 display: inline;
2023 display: inline;
2019 }
2024 }
2020
2025
2021 div.journal_action_params{
2026 div.journal_action_params{
2022 display: block;
2027 display: block;
2023 }
2028 }
2024
2029
2025 div.journal_repo:after{
2030 div.journal_repo:after{
2026 content: "\A";
2031 content: "\A";
2027 white-space: pre;
2032 white-space: pre;
2028 }
2033 }
2029
2034
2030 div.date{
2035 div.date{
2031 display: block;
2036 display: block;
2032 margin-bottom: @textmargin;
2037 margin-bottom: @textmargin;
2033 }
2038 }
2034 }
2039 }
2035 }
2040 }
2036 }
2041 }
2037
2042
2038 // Files
2043 // Files
2039 .edit-file-title {
2044 .edit-file-title {
2040 border-bottom: @border-thickness solid @border-default-color;
2045 border-bottom: @border-thickness solid @border-default-color;
2041
2046
2042 .breadcrumbs {
2047 .breadcrumbs {
2043 margin-bottom: 0;
2048 margin-bottom: 0;
2044 }
2049 }
2045 }
2050 }
2046
2051
2047 .edit-file-fieldset {
2052 .edit-file-fieldset {
2048 margin-top: @sidebarpadding;
2053 margin-top: @sidebarpadding;
2049
2054
2050 .fieldset {
2055 .fieldset {
2051 .left-label {
2056 .left-label {
2052 width: 13%;
2057 width: 13%;
2053 }
2058 }
2054 .right-content {
2059 .right-content {
2055 width: 87%;
2060 width: 87%;
2056 max-width: 100%;
2061 max-width: 100%;
2057 }
2062 }
2058 .filename-label {
2063 .filename-label {
2059 margin-top: 13px;
2064 margin-top: 13px;
2060 }
2065 }
2061 .commit-message-label {
2066 .commit-message-label {
2062 margin-top: 4px;
2067 margin-top: 4px;
2063 }
2068 }
2064 .file-upload-input {
2069 .file-upload-input {
2065 input {
2070 input {
2066 display: none;
2071 display: none;
2067 }
2072 }
2068 margin-top: 10px;
2073 margin-top: 10px;
2069 }
2074 }
2070 .file-upload-label {
2075 .file-upload-label {
2071 margin-top: 10px;
2076 margin-top: 10px;
2072 }
2077 }
2073 p {
2078 p {
2074 margin-top: 5px;
2079 margin-top: 5px;
2075 }
2080 }
2076
2081
2077 }
2082 }
2078 .custom-path-link {
2083 .custom-path-link {
2079 margin-left: 5px;
2084 margin-left: 5px;
2080 }
2085 }
2081 #commit {
2086 #commit {
2082 resize: vertical;
2087 resize: vertical;
2083 }
2088 }
2084 }
2089 }
2085
2090
2086 .delete-file-preview {
2091 .delete-file-preview {
2087 max-height: 250px;
2092 max-height: 250px;
2088 }
2093 }
2089
2094
2090 .new-file,
2095 .new-file,
2091 #filter_activate,
2096 #filter_activate,
2092 #filter_deactivate {
2097 #filter_deactivate {
2093 float: left;
2098 float: left;
2094 margin: 0 0 0 15px;
2099 margin: 0 0 0 15px;
2095 }
2100 }
2096
2101
2097 h3.files_location{
2102 h3.files_location{
2098 line-height: 2.4em;
2103 line-height: 2.4em;
2099 }
2104 }
2100
2105
2101 .browser-nav {
2106 .browser-nav {
2102 display: table;
2107 display: table;
2103 margin-bottom: @space;
2108 margin-bottom: @space;
2104
2109
2105
2110
2106 .info_box {
2111 .info_box {
2107 display: inline-table;
2112 display: inline-table;
2108 height: 2.5em;
2113 height: 2.5em;
2109
2114
2110 .browser-cur-rev, .info_box_elem {
2115 .browser-cur-rev, .info_box_elem {
2111 display: table-cell;
2116 display: table-cell;
2112 vertical-align: middle;
2117 vertical-align: middle;
2113 }
2118 }
2114
2119
2115 .info_box_elem {
2120 .info_box_elem {
2116 border-top: @border-thickness solid @rcblue;
2121 border-top: @border-thickness solid @rcblue;
2117 border-bottom: @border-thickness solid @rcblue;
2122 border-bottom: @border-thickness solid @rcblue;
2118
2123
2119 #at_rev, a {
2124 #at_rev, a {
2120 padding: 0.6em 0.9em;
2125 padding: 0.6em 0.9em;
2121 margin: 0;
2126 margin: 0;
2122 .box-shadow(none);
2127 .box-shadow(none);
2123 border: 0;
2128 border: 0;
2124 height: 12px;
2129 height: 12px;
2125 }
2130 }
2126
2131
2127 input#at_rev {
2132 input#at_rev {
2128 max-width: 50px;
2133 max-width: 50px;
2129 text-align: right;
2134 text-align: right;
2130 }
2135 }
2131
2136
2132 &.previous {
2137 &.previous {
2133 border: @border-thickness solid @rcblue;
2138 border: @border-thickness solid @rcblue;
2134 .disabled {
2139 .disabled {
2135 color: @grey4;
2140 color: @grey4;
2136 cursor: not-allowed;
2141 cursor: not-allowed;
2137 }
2142 }
2138 }
2143 }
2139
2144
2140 &.next {
2145 &.next {
2141 border: @border-thickness solid @rcblue;
2146 border: @border-thickness solid @rcblue;
2142 .disabled {
2147 .disabled {
2143 color: @grey4;
2148 color: @grey4;
2144 cursor: not-allowed;
2149 cursor: not-allowed;
2145 }
2150 }
2146 }
2151 }
2147 }
2152 }
2148
2153
2149 .browser-cur-rev {
2154 .browser-cur-rev {
2150
2155
2151 span{
2156 span{
2152 margin: 0;
2157 margin: 0;
2153 color: @rcblue;
2158 color: @rcblue;
2154 height: 12px;
2159 height: 12px;
2155 display: inline-block;
2160 display: inline-block;
2156 padding: 0.7em 1em ;
2161 padding: 0.7em 1em ;
2157 border: @border-thickness solid @rcblue;
2162 border: @border-thickness solid @rcblue;
2158 margin-right: @padding;
2163 margin-right: @padding;
2159 }
2164 }
2160 }
2165 }
2161 }
2166 }
2162
2167
2163 .search_activate {
2168 .search_activate {
2164 display: table-cell;
2169 display: table-cell;
2165 vertical-align: middle;
2170 vertical-align: middle;
2166
2171
2167 input, label{
2172 input, label{
2168 margin: 0;
2173 margin: 0;
2169 padding: 0;
2174 padding: 0;
2170 }
2175 }
2171
2176
2172 input{
2177 input{
2173 margin-left: @textmargin;
2178 margin-left: @textmargin;
2174 }
2179 }
2175
2180
2176 }
2181 }
2177 }
2182 }
2178
2183
2179 .browser-cur-rev{
2184 .browser-cur-rev{
2180 margin-bottom: @textmargin;
2185 margin-bottom: @textmargin;
2181 }
2186 }
2182
2187
2183 #node_filter_box_loading{
2188 #node_filter_box_loading{
2184 .info_text;
2189 .info_text;
2185 }
2190 }
2186
2191
2187 .browser-search {
2192 .browser-search {
2188 margin: -25px 0px 5px 0px;
2193 margin: -25px 0px 5px 0px;
2189 }
2194 }
2190
2195
2191 .node-filter {
2196 .node-filter {
2192 font-size: @repo-title-fontsize;
2197 font-size: @repo-title-fontsize;
2193 padding: 4px 0px 0px 0px;
2198 padding: 4px 0px 0px 0px;
2194
2199
2195 .node-filter-path {
2200 .node-filter-path {
2196 float: left;
2201 float: left;
2197 color: @grey4;
2202 color: @grey4;
2198 }
2203 }
2199 .node-filter-input {
2204 .node-filter-input {
2200 float: left;
2205 float: left;
2201 margin: -2px 0px 0px 2px;
2206 margin: -2px 0px 0px 2px;
2202 input {
2207 input {
2203 padding: 2px;
2208 padding: 2px;
2204 border: none;
2209 border: none;
2205 font-size: @repo-title-fontsize;
2210 font-size: @repo-title-fontsize;
2206 }
2211 }
2207 }
2212 }
2208 }
2213 }
2209
2214
2210
2215
2211 .browser-result{
2216 .browser-result{
2212 td a{
2217 td a{
2213 margin-left: 0.5em;
2218 margin-left: 0.5em;
2214 display: inline-block;
2219 display: inline-block;
2215
2220
2216 em{
2221 em{
2217 font-family: @text-bold;
2222 font-family: @text-bold;
2218 }
2223 }
2219 }
2224 }
2220 }
2225 }
2221
2226
2222 .browser-highlight{
2227 .browser-highlight{
2223 background-color: @grey5-alpha;
2228 background-color: @grey5-alpha;
2224 }
2229 }
2225
2230
2226
2231
2227 // Search
2232 // Search
2228
2233
2229 .search-form{
2234 .search-form{
2230 #q {
2235 #q {
2231 width: @search-form-width;
2236 width: @search-form-width;
2232 }
2237 }
2233 .fields{
2238 .fields{
2234 margin: 0 0 @space;
2239 margin: 0 0 @space;
2235 }
2240 }
2236
2241
2237 label{
2242 label{
2238 display: inline-block;
2243 display: inline-block;
2239 margin-right: @textmargin;
2244 margin-right: @textmargin;
2240 padding-top: 0.25em;
2245 padding-top: 0.25em;
2241 }
2246 }
2242
2247
2243
2248
2244 .results{
2249 .results{
2245 clear: both;
2250 clear: both;
2246 margin: 0 0 @padding;
2251 margin: 0 0 @padding;
2247 }
2252 }
2248 }
2253 }
2249
2254
2250 div.search-feedback-items {
2255 div.search-feedback-items {
2251 display: inline-block;
2256 display: inline-block;
2252 padding:0px 0px 0px 96px;
2257 padding:0px 0px 0px 96px;
2253 }
2258 }
2254
2259
2255 div.search-code-body {
2260 div.search-code-body {
2256 background-color: #ffffff; padding: 5px 0 5px 10px;
2261 background-color: #ffffff; padding: 5px 0 5px 10px;
2257 pre {
2262 pre {
2258 .match { background-color: #faffa6;}
2263 .match { background-color: #faffa6;}
2259 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2264 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2260 }
2265 }
2261 }
2266 }
2262
2267
2263 .expand_commit.search {
2268 .expand_commit.search {
2264 .show_more.open {
2269 .show_more.open {
2265 height: auto;
2270 height: auto;
2266 max-height: none;
2271 max-height: none;
2267 }
2272 }
2268 }
2273 }
2269
2274
2270 .search-results {
2275 .search-results {
2271
2276
2272 h2 {
2277 h2 {
2273 margin-bottom: 0;
2278 margin-bottom: 0;
2274 }
2279 }
2275 .codeblock {
2280 .codeblock {
2276 border: none;
2281 border: none;
2277 background: transparent;
2282 background: transparent;
2278 }
2283 }
2279
2284
2280 .codeblock-header {
2285 .codeblock-header {
2281 border: none;
2286 border: none;
2282 background: transparent;
2287 background: transparent;
2283 }
2288 }
2284
2289
2285 .code-body {
2290 .code-body {
2286 border: @border-thickness solid @border-default-color;
2291 border: @border-thickness solid @border-default-color;
2287 .border-radius(@border-radius);
2292 .border-radius(@border-radius);
2288 }
2293 }
2289
2294
2290 .td-commit {
2295 .td-commit {
2291 &:extend(pre);
2296 &:extend(pre);
2292 border-bottom: @border-thickness solid @border-default-color;
2297 border-bottom: @border-thickness solid @border-default-color;
2293 }
2298 }
2294
2299
2295 .message {
2300 .message {
2296 height: auto;
2301 height: auto;
2297 max-width: 350px;
2302 max-width: 350px;
2298 white-space: normal;
2303 white-space: normal;
2299 text-overflow: initial;
2304 text-overflow: initial;
2300 overflow: visible;
2305 overflow: visible;
2301
2306
2302 .match { background-color: #faffa6;}
2307 .match { background-color: #faffa6;}
2303 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2308 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2304 }
2309 }
2305
2310
2306 }
2311 }
2307
2312
2308 table.rctable td.td-search-results div {
2313 table.rctable td.td-search-results div {
2309 max-width: 100%;
2314 max-width: 100%;
2310 }
2315 }
2311
2316
2312 #tip-box, .tip-box{
2317 #tip-box, .tip-box{
2313 padding: @menupadding/2;
2318 padding: @menupadding/2;
2314 display: block;
2319 display: block;
2315 border: @border-thickness solid @border-highlight-color;
2320 border: @border-thickness solid @border-highlight-color;
2316 .border-radius(@border-radius);
2321 .border-radius(@border-radius);
2317 background-color: white;
2322 background-color: white;
2318 z-index: 99;
2323 z-index: 99;
2319 white-space: pre-wrap;
2324 white-space: pre-wrap;
2320 }
2325 }
2321
2326
2322 #linktt {
2327 #linktt {
2323 width: 79px;
2328 width: 79px;
2324 }
2329 }
2325
2330
2326 #help_kb .modal-content{
2331 #help_kb .modal-content{
2327 max-width: 750px;
2332 max-width: 750px;
2328 margin: 10% auto;
2333 margin: 10% auto;
2329
2334
2330 table{
2335 table{
2331 td,th{
2336 td,th{
2332 border-bottom: none;
2337 border-bottom: none;
2333 line-height: 2.5em;
2338 line-height: 2.5em;
2334 }
2339 }
2335 th{
2340 th{
2336 padding-bottom: @textmargin/2;
2341 padding-bottom: @textmargin/2;
2337 }
2342 }
2338 td.keys{
2343 td.keys{
2339 text-align: center;
2344 text-align: center;
2340 }
2345 }
2341 }
2346 }
2342
2347
2343 .block-left{
2348 .block-left{
2344 width: 45%;
2349 width: 45%;
2345 margin-right: 5%;
2350 margin-right: 5%;
2346 }
2351 }
2347 .modal-footer{
2352 .modal-footer{
2348 clear: both;
2353 clear: both;
2349 }
2354 }
2350 .key.tag{
2355 .key.tag{
2351 padding: 0.5em;
2356 padding: 0.5em;
2352 background-color: @rcblue;
2357 background-color: @rcblue;
2353 color: white;
2358 color: white;
2354 border-color: @rcblue;
2359 border-color: @rcblue;
2355 .box-shadow(none);
2360 .box-shadow(none);
2356 }
2361 }
2357 }
2362 }
2358
2363
2359
2364
2360
2365
2361 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2366 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2362
2367
2363 @import 'statistics-graph';
2368 @import 'statistics-graph';
2364 @import 'tables';
2369 @import 'tables';
2365 @import 'forms';
2370 @import 'forms';
2366 @import 'diff';
2371 @import 'diff';
2367 @import 'summary';
2372 @import 'summary';
2368 @import 'navigation';
2373 @import 'navigation';
2369
2374
2370 //--- SHOW/HIDE SECTIONS --//
2375 //--- SHOW/HIDE SECTIONS --//
2371
2376
2372 .btn-collapse {
2377 .btn-collapse {
2373 float: right;
2378 float: right;
2374 text-align: right;
2379 text-align: right;
2375 font-family: @text-light;
2380 font-family: @text-light;
2376 font-size: @basefontsize;
2381 font-size: @basefontsize;
2377 cursor: pointer;
2382 cursor: pointer;
2378 border: none;
2383 border: none;
2379 color: @rcblue;
2384 color: @rcblue;
2380 }
2385 }
2381
2386
2382 table.rctable,
2387 table.rctable,
2383 table.dataTable {
2388 table.dataTable {
2384 .btn-collapse {
2389 .btn-collapse {
2385 float: right;
2390 float: right;
2386 text-align: right;
2391 text-align: right;
2387 }
2392 }
2388 }
2393 }
2389
2394
2390
2395
2391 // TODO: johbo: Fix for IE10, this avoids that we see a border
2396 // TODO: johbo: Fix for IE10, this avoids that we see a border
2392 // and padding around checkboxes and radio boxes. Move to the right place,
2397 // and padding around checkboxes and radio boxes. Move to the right place,
2393 // or better: Remove this once we did the form refactoring.
2398 // or better: Remove this once we did the form refactoring.
2394 input[type=checkbox],
2399 input[type=checkbox],
2395 input[type=radio] {
2400 input[type=radio] {
2396 padding: 0;
2401 padding: 0;
2397 border: none;
2402 border: none;
2398 }
2403 }
2399
2404
2400 .toggle-ajax-spinner{
2405 .toggle-ajax-spinner{
2401 height: 16px;
2406 height: 16px;
2402 width: 16px;
2407 width: 16px;
2403 }
2408 }
@@ -1,297 +1,314 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.mako"/>
3 <%inherit file="/base/base.mako"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 ${_('%s Changelog') % c.repo_name}
6 ${_('%s Changelog') % c.repo_name}
7 %if c.changelog_for_path:
7 %if c.changelog_for_path:
8 /${c.changelog_for_path}
8 /${c.changelog_for_path}
9 %endif
9 %endif
10 %if c.rhodecode_name:
10 %if c.rhodecode_name:
11 &middot; ${h.branding(c.rhodecode_name)}
11 &middot; ${h.branding(c.rhodecode_name)}
12 %endif
12 %endif
13 </%def>
13 </%def>
14
14
15 <%def name="breadcrumbs_links()">
15 <%def name="breadcrumbs_links()">
16 %if c.changelog_for_path:
16 %if c.changelog_for_path:
17 /${c.changelog_for_path}
17 /${c.changelog_for_path}
18 %endif
18 %endif
19 </%def>
19 </%def>
20
20
21 <%def name="menu_bar_nav()">
21 <%def name="menu_bar_nav()">
22 ${self.menu_items(active='repositories')}
22 ${self.menu_items(active='repositories')}
23 </%def>
23 </%def>
24
24
25 <%def name="menu_bar_subnav()">
25 <%def name="menu_bar_subnav()">
26 ${self.repo_menu(active='changelog')}
26 ${self.repo_menu(active='changelog')}
27 </%def>
27 </%def>
28
28
29 <%def name="main()">
29 <%def name="main()">
30
30
31 <div class="box">
31 <div class="box">
32 <div class="title">
32 <div class="title">
33 ${self.repo_page_title(c.rhodecode_db_repo)}
33 ${self.repo_page_title(c.rhodecode_db_repo)}
34 <ul class="links">
34 <ul class="links">
35 <li>
35 <li>
36 <a href="#" class="btn btn-small" id="rev_range_container" style="display:none;"></a>
36 <a href="#" class="btn btn-small" id="rev_range_container" style="display:none;"></a>
37 %if c.rhodecode_db_repo.fork:
37 %if c.rhodecode_db_repo.fork:
38 <span>
38 <span>
39 <a id="compare_fork_button"
39 <a id="compare_fork_button"
40 title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
40 title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
41 class="btn btn-small"
41 class="btn btn-small"
42 href="${h.route_path('repo_compare',
42 href="${h.route_path('repo_compare',
43 repo_name=c.rhodecode_db_repo.fork.repo_name,
43 repo_name=c.rhodecode_db_repo.fork.repo_name,
44 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
44 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
45 source_ref=c.rhodecode_db_repo.landing_rev[1],
45 source_ref=c.rhodecode_db_repo.landing_rev[1],
46 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
46 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
47 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
47 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
48 _query=dict(merge=1, target_repo=c.repo_name))}"
48 _query=dict(merge=1, target_repo=c.repo_name))}"
49 >
49 >
50 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
50 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
51 </a>
51 </a>
52 </span>
52 </span>
53 %endif
53 %endif
54
54
55 ## pr open link
55 ## pr open link
56 %if h.is_hg(c.rhodecode_repo) or h.is_git(c.rhodecode_repo):
56 %if h.is_hg(c.rhodecode_repo) or h.is_git(c.rhodecode_repo):
57 <span>
57 <span>
58 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
58 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
59 ${_('Open new pull request')}
59 ${_('Open new pull request')}
60 </a>
60 </a>
61 </span>
61 </span>
62 %endif
62 %endif
63
63
64 ## clear selection
64 ## clear selection
65 <div title="${_('Clear selection')}" class="btn" id="rev_range_clear" style="display:none">
65 <div title="${_('Clear selection')}" class="btn" id="rev_range_clear" style="display:none">
66 ${_('Clear selection')}
66 ${_('Clear selection')}
67 </div>
67 </div>
68
68
69 </li>
69 </li>
70 </ul>
70 </ul>
71 </div>
71 </div>
72
72
73 % if c.pagination:
73 % if c.pagination:
74 <script type="text/javascript" src="${h.asset('js/src/plugins/jquery.commits-graph.js')}"></script>
74 <script type="text/javascript" src="${h.asset('js/src/plugins/jquery.commits-graph.js')}"></script>
75
75
76 <div class="graph-header">
76 <div class="graph-header">
77 <div id="filter_changelog">
77 <div id="filter_changelog">
78 ${h.hidden('branch_filter')}
78 ${h.hidden('branch_filter')}
79 %if c.selected_name:
79 %if c.selected_name:
80 <div class="btn btn-default" id="clear_filter" >
80 <div class="btn btn-default" id="clear_filter" >
81 ${_('Clear filter')}
81 ${_('Clear filter')}
82 </div>
82 </div>
83 %endif
83 %endif
84 </div>
84 </div>
85 ${self.breadcrumbs('breadcrumbs_light')}
85 ${self.breadcrumbs('breadcrumbs_light')}
86 <div class="pull-right">
87 % if h.is_hg(c.rhodecode_repo):
88 % if c.show_hidden:
89 <a class="action-link" href="${h.current_route_path(request, evolve=0)}">${_('Hide obsolete/hidden')}</a>
90 % else:
91 <a class="action-link" href="${h.current_route_path(request, evolve=1)}">${_('Show obsolete/hidden')}</a>
92 % endif
93 % else:
94 <span class="action-link disabled">${_('Show hidden')}</span>
95 % endif
96 </div>
86 <div id="commit-counter" data-total=${c.total_cs} class="pull-right">
97 <div id="commit-counter" data-total=${c.total_cs} class="pull-right">
87 ${_ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
98 ${_ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
88 </div>
99 </div>
89 </div>
100 </div>
90
101
91 <div id="graph">
102 <div id="graph">
92 <div class="graph-col-wrapper">
103 <div class="graph-col-wrapper">
93 <div id="graph_nodes">
104 <div id="graph_nodes">
94 <div id="graph_canvas"></div>
105 <div id="graph_canvas"></div>
95 </div>
106 </div>
96 <div id="graph_content" class="main-content graph_full_width">
107 <div id="graph_content" class="main-content graph_full_width">
97
108
98 <div class="table">
109 <div class="table">
99 <table id="changesets" class="rctable">
110 <table id="changesets" class="rctable">
100 <tr>
111 <tr>
101 ## checkbox
112 ## checkbox
102 <th></th>
113 <th></th>
103 <th colspan="2"></th>
114 <th colspan="2"></th>
104
115
105 <th>${_('Commit')}</th>
116 <th>${_('Commit')}</th>
117 ## Mercurial phase/evolve state
118 <th></th>
106 ## commit message expand arrow
119 ## commit message expand arrow
107 <th></th>
120 <th></th>
108 <th>${_('Commit Message')}</th>
121 <th>${_('Commit Message')}</th>
109
122
110 <th>${_('Age')}</th>
123 <th>${_('Age')}</th>
111 <th>${_('Author')}</th>
124 <th>${_('Author')}</th>
112
125
113 <th>${_('Refs')}</th>
126 <th>${_('Refs')}</th>
114 </tr>
127 </tr>
115
128
116 <tbody class="commits-range">
129 <tbody class="commits-range">
117 <%include file='changelog_elements.mako'/>
130 <%include file='changelog_elements.mako'/>
118 </tbody>
131 </tbody>
119 </table>
132 </table>
120 </div>
133 </div>
121 </div>
134 </div>
122 <div class="pagination-wh pagination-left">
135 <div class="pagination-wh pagination-left">
123 ${c.pagination.pager('$link_previous ~2~ $link_next')}
136 ${c.pagination.pager('$link_previous ~2~ $link_next')}
124 </div>
137 </div>
125 </div>
138 </div>
126
139
127 <script type="text/javascript">
140 <script type="text/javascript">
128 var cache = {};
141 var cache = {};
129 $(function(){
142 $(function(){
130
143
131 // Create links to commit ranges when range checkboxes are selected
144 // Create links to commit ranges when range checkboxes are selected
132 var $commitCheckboxes = $('.commit-range');
145 var $commitCheckboxes = $('.commit-range');
133 // cache elements
146 // cache elements
134 var $commitRangeContainer = $('#rev_range_container');
147 var $commitRangeContainer = $('#rev_range_container');
135 var $commitRangeClear = $('#rev_range_clear');
148 var $commitRangeClear = $('#rev_range_clear');
136
149
137 var checkboxRangeSelector = function(e){
150 var checkboxRangeSelector = function(e){
138 var selectedCheckboxes = [];
151 var selectedCheckboxes = [];
139 for (pos in $commitCheckboxes){
152 for (pos in $commitCheckboxes){
140 if($commitCheckboxes[pos].checked){
153 if($commitCheckboxes[pos].checked){
141 selectedCheckboxes.push($commitCheckboxes[pos]);
154 selectedCheckboxes.push($commitCheckboxes[pos]);
142 }
155 }
143 }
156 }
144 var open_new_pull_request = $('#open_new_pull_request');
157 var open_new_pull_request = $('#open_new_pull_request');
145 if(open_new_pull_request){
158 if(open_new_pull_request){
146 var selected_changes = selectedCheckboxes.length;
159 var selected_changes = selectedCheckboxes.length;
147 if (selected_changes > 1 || selected_changes == 1 && templateContext.repo_type != 'hg') {
160 if (selected_changes > 1 || selected_changes == 1 && templateContext.repo_type != 'hg') {
148 open_new_pull_request.hide();
161 open_new_pull_request.hide();
149 } else {
162 } else {
150 if (selected_changes == 1) {
163 if (selected_changes == 1) {
151 open_new_pull_request.html(_gettext('Open new pull request for selected commit'));
164 open_new_pull_request.html(_gettext('Open new pull request for selected commit'));
152 } else if (selected_changes == 0) {
165 } else if (selected_changes == 0) {
153 open_new_pull_request.html(_gettext('Open new pull request'));
166 open_new_pull_request.html(_gettext('Open new pull request'));
154 }
167 }
155 open_new_pull_request.show();
168 open_new_pull_request.show();
156 }
169 }
157 }
170 }
158
171
159 if (selectedCheckboxes.length>0){
172 if (selectedCheckboxes.length>0){
160 var revEnd = selectedCheckboxes[0].name;
173 var revEnd = selectedCheckboxes[0].name;
161 var revStart = selectedCheckboxes[selectedCheckboxes.length-1].name;
174 var revStart = selectedCheckboxes[selectedCheckboxes.length-1].name;
162 var url = pyroutes.url('repo_commit',
175 var url = pyroutes.url('repo_commit',
163 {'repo_name': '${c.repo_name}',
176 {'repo_name': '${c.repo_name}',
164 'commit_id': revStart+'...'+revEnd});
177 'commit_id': revStart+'...'+revEnd});
165
178
166 var link = (revStart == revEnd)
179 var link = (revStart == revEnd)
167 ? _gettext('Show selected commit __S')
180 ? _gettext('Show selected commit __S')
168 : _gettext('Show selected commits __S ... __E');
181 : _gettext('Show selected commits __S ... __E');
169
182
170 link = link.replace('__S', revStart.substr(0,6));
183 link = link.replace('__S', revStart.substr(0,6));
171 link = link.replace('__E', revEnd.substr(0,6));
184 link = link.replace('__E', revEnd.substr(0,6));
172
185
173 $commitRangeContainer
186 $commitRangeContainer
174 .attr('href',url)
187 .attr('href',url)
175 .html(link)
188 .html(link)
176 .show();
189 .show();
177
190
178 $commitRangeClear.show();
191 $commitRangeClear.show();
179 var _url = pyroutes.url('pullrequest_new',
192 var _url = pyroutes.url('pullrequest_new',
180 {'repo_name': '${c.repo_name}',
193 {'repo_name': '${c.repo_name}',
181 'commit': revEnd});
194 'commit': revEnd});
182 open_new_pull_request.attr('href', _url);
195 open_new_pull_request.attr('href', _url);
183 $('#compare_fork_button').hide();
196 $('#compare_fork_button').hide();
184 } else {
197 } else {
185 $commitRangeContainer.hide();
198 $commitRangeContainer.hide();
186 $commitRangeClear.hide();
199 $commitRangeClear.hide();
187
200
188 %if c.branch_name:
201 %if c.branch_name:
189 var _url = pyroutes.url('pullrequest_new',
202 var _url = pyroutes.url('pullrequest_new',
190 {'repo_name': '${c.repo_name}',
203 {'repo_name': '${c.repo_name}',
191 'branch':'${c.branch_name}'});
204 'branch':'${c.branch_name}'});
192 open_new_pull_request.attr('href', _url);
205 open_new_pull_request.attr('href', _url);
193 %else:
206 %else:
194 var _url = pyroutes.url('pullrequest_new',
207 var _url = pyroutes.url('pullrequest_new',
195 {'repo_name': '${c.repo_name}'});
208 {'repo_name': '${c.repo_name}'});
196 open_new_pull_request.attr('href', _url);
209 open_new_pull_request.attr('href', _url);
197 %endif
210 %endif
198 $('#compare_fork_button').show();
211 $('#compare_fork_button').show();
199 }
212 }
200 };
213 };
201
214
202 $commitCheckboxes.on('click', checkboxRangeSelector);
215 $commitCheckboxes.on('click', checkboxRangeSelector);
203
216
204 $commitRangeClear.on('click',function(e) {
217 $commitRangeClear.on('click',function(e) {
205 $commitCheckboxes.attr('checked', false);
218 $commitCheckboxes.attr('checked', false);
206 checkboxRangeSelector();
219 checkboxRangeSelector();
207 e.preventDefault();
220 e.preventDefault();
208 });
221 });
209
222
210 // make sure the buttons are consistent when navigate back and forth
223 // make sure the buttons are consistent when navigate back and forth
211 checkboxRangeSelector();
224 checkboxRangeSelector();
212
225
213 var msgs = $('.message');
226 var msgs = $('.message');
214 // get first element height
227 // get first element height
215 var el = $('#graph_content .container')[0];
228 var el = $('#graph_content .container')[0];
216 var row_h = el.clientHeight;
229 var row_h = el.clientHeight;
217 for (var i=0; i < msgs.length; i++) {
230 for (var i=0; i < msgs.length; i++) {
218 var m = msgs[i];
231 var m = msgs[i];
219
232
220 var h = m.clientHeight;
233 var h = m.clientHeight;
221 var pad = $(m).css('padding');
234 var pad = $(m).css('padding');
222 if (h > row_h) {
235 if (h > row_h) {
223 var offset = row_h - (h+12);
236 var offset = row_h - (h+12);
224 $(m.nextElementSibling).css('display','block');
237 $(m.nextElementSibling).css('display','block');
225 $(m.nextElementSibling).css('margin-top',offset+'px');
238 $(m.nextElementSibling).css('margin-top',offset+'px');
226 }
239 }
227 }
240 }
228
241
229 $("#clear_filter").on("click", function() {
242 $("#clear_filter").on("click", function() {
230 var filter = {'repo_name': '${c.repo_name}'};
243 var filter = {'repo_name': '${c.repo_name}'};
231 window.location = pyroutes.url('repo_changelog', filter);
244 window.location = pyroutes.url('repo_changelog', filter);
232 });
245 });
233
246
234 $("#branch_filter").select2({
247 $("#branch_filter").select2({
235 'dropdownAutoWidth': true,
248 'dropdownAutoWidth': true,
236 'width': 'resolve',
249 'width': 'resolve',
237 'placeholder': "${c.selected_name or _('Filter changelog')}",
250 'placeholder': "${c.selected_name or _('Filter changelog')}",
238 containerCssClass: "drop-menu",
251 containerCssClass: "drop-menu",
239 dropdownCssClass: "drop-menu-dropdown",
252 dropdownCssClass: "drop-menu-dropdown",
240 query: function(query){
253 query: function(query){
241 var key = 'cache';
254 var key = 'cache';
242 var cached = cache[key] ;
255 var cached = cache[key] ;
243 if(cached) {
256 if(cached) {
244 var data = {results: []};
257 var data = {results: []};
245 //filter results
258 //filter results
246 $.each(cached.results, function(){
259 $.each(cached.results, function(){
247 var section = this.text;
260 var section = this.text;
248 var children = [];
261 var children = [];
249 $.each(this.children, function(){
262 $.each(this.children, function(){
250 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
263 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
251 children.push({'id': this.id, 'text': this.text, 'type': this.type})
264 children.push({'id': this.id, 'text': this.text, 'type': this.type})
252 }
265 }
253 });
266 });
254 data.results.push({'text': section, 'children': children});
267 data.results.push({'text': section, 'children': children});
255 query.callback({results: data.results});
268 query.callback({results: data.results});
256 });
269 });
257 }else{
270 }else{
258 $.ajax({
271 $.ajax({
259 url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}),
272 url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}),
260 data: {},
273 data: {},
261 dataType: 'json',
274 dataType: 'json',
262 type: 'GET',
275 type: 'GET',
263 success: function(data) {
276 success: function(data) {
264 cache[key] = data;
277 cache[key] = data;
265 query.callback({results: data.results});
278 query.callback({results: data.results});
266 }
279 }
267 })
280 })
268 }
281 }
269 }
282 }
270 });
283 });
271 $('#branch_filter').on('change', function(e){
284 $('#branch_filter').on('change', function(e){
272 var data = $('#branch_filter').select2('data');
285 var data = $('#branch_filter').select2('data');
286 //type: branch_closed
273 var selected = data.text;
287 var selected = data.text;
274 var filter = {'repo_name': '${c.repo_name}'};
288 var filter = {'repo_name': '${c.repo_name}'};
275 if(data.type == 'branch' || data.type == 'branch_closed'){
289 if(data.type == 'branch' || data.type == 'branch_closed'){
276 filter["branch"] = selected;
290 filter["branch"] = selected;
291 if (data.type == 'branch_closed') {
292 filter["evolve"] = '1';
293 }
277 }
294 }
278 else if (data.type == 'book'){
295 else if (data.type == 'book'){
279 filter["bookmark"] = selected;
296 filter["bookmark"] = selected;
280 }
297 }
281 window.location = pyroutes.url('repo_changelog', filter);
298 window.location = pyroutes.url('repo_changelog', filter);
282 });
299 });
283
300
284 commitsController = new CommitsController();
301 commitsController = new CommitsController();
285 % if not c.changelog_for_path:
302 % if not c.changelog_for_path:
286 commitsController.reloadGraph();
303 commitsController.reloadGraph();
287 % endif
304 % endif
288
305
289 });
306 });
290
307
291 </script>
308 </script>
292 </div>
309 </div>
293 % else:
310 % else:
294 ${_('There are no changes yet')}
311 ${_('There are no changes yet')}
295 % endif
312 % endif
296 </div>
313 </div>
297 </%def>
314 </%def>
@@ -1,144 +1,146 b''
1 ## small box that displays changed/added/removed details fetched by AJAX
1 ## small box that displays changed/added/removed details fetched by AJAX
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4
4
5 % if c.prev_page:
5 % if c.prev_page:
6 <tr>
6 <tr>
7 <td colspan="9" class="load-more-commits">
7 <td colspan="9" class="load-more-commits">
8 <a class="prev-commits" href="#loadPrevCommits" onclick="commitsController.loadPrev(this, ${c.prev_page}, '${c.branch_name}', '${c.commit_id}', '${c.f_path}');return false">
8 <a class="prev-commits" href="#loadPrevCommits" onclick="commitsController.loadPrev(this, ${c.prev_page}, '${c.branch_name}', '${c.commit_id}', '${c.f_path}');return false">
9 ${_('load previous')}
9 ${_('load previous')}
10 </a>
10 </a>
11 </td>
11 </td>
12 </tr>
12 </tr>
13 % endif
13 % endif
14
14
15 % for cnt,commit in enumerate(c.pagination):
15 % for cnt,commit in enumerate(c.pagination):
16 <tr id="sha_${commit.raw_id}" class="changelogRow container ${'tablerow%s' % (cnt%2)}">
16 <tr id="sha_${commit.raw_id}" class="changelogRow container ${'tablerow%s' % (cnt%2)}">
17
17
18 <td class="td-checkbox">
18 <td class="td-checkbox">
19 ${h.checkbox(commit.raw_id,class_="commit-range")}
19 ${h.checkbox(commit.raw_id,class_="commit-range")}
20 </td>
20 </td>
21 <td class="td-status">
21 <td class="td-status">
22
22
23 %if c.statuses.get(commit.raw_id):
23 %if c.statuses.get(commit.raw_id):
24 <div class="changeset-status-ico">
24 <div class="changeset-status-ico">
25 %if c.statuses.get(commit.raw_id)[2]:
25 %if c.statuses.get(commit.raw_id)[2]:
26 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (h.commit_status_lbl(c.statuses.get(commit.raw_id)[0]), c.statuses.get(commit.raw_id)[2])}" href="${h.route_path('pullrequest_show',repo_name=c.statuses.get(commit.raw_id)[3],pull_request_id=c.statuses.get(commit.raw_id)[2])}">
26 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (h.commit_status_lbl(c.statuses.get(commit.raw_id)[0]), c.statuses.get(commit.raw_id)[2])}" href="${h.route_path('pullrequest_show',repo_name=c.statuses.get(commit.raw_id)[3],pull_request_id=c.statuses.get(commit.raw_id)[2])}">
27 <div class="${'flag_status {}'.format(c.statuses.get(commit.raw_id)[0])}"></div>
27 <div class="${'flag_status {}'.format(c.statuses.get(commit.raw_id)[0])}"></div>
28 </a>
28 </a>
29 %else:
29 %else:
30 <a class="tooltip" title="${_('Commit status: {}').format(h.commit_status_lbl(c.statuses.get(commit.raw_id)[0]))}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id,_anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
30 <a class="tooltip" title="${_('Commit status: {}').format(h.commit_status_lbl(c.statuses.get(commit.raw_id)[0]))}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id,_anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
31 <div class="${'flag_status {}'.format(c.statuses.get(commit.raw_id)[0])}"></div>
31 <div class="${'flag_status {}'.format(c.statuses.get(commit.raw_id)[0])}"></div>
32 </a>
32 </a>
33 %endif
33 %endif
34 </div>
34 </div>
35 %else:
35 %else:
36 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
36 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
37 %endif
37 %endif
38 </td>
38 </td>
39 <td class="td-comments comments-col">
39 <td class="td-comments comments-col">
40 %if c.comments.get(commit.raw_id):
40 %if c.comments.get(commit.raw_id):
41 <a title="${_('Commit has comments')}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id,_anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
41 <a title="${_('Commit has comments')}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id,_anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
42 <i class="icon-comment"></i> ${len(c.comments[commit.raw_id])}
42 <i class="icon-comment"></i> ${len(c.comments[commit.raw_id])}
43 </a>
43 </a>
44 %endif
44 %endif
45 </td>
45 </td>
46 <td class="td-hash">
46 <td class="td-hash">
47 <code>
47 <code>
48
48
49 <a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">
49 <a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">
50 <span class="${'commit_hash obsolete' if getattr(commit, 'obsolete', None) else 'commit_hash'}">${h.show_id(commit)}</span>
50 <span class="${'commit_hash obsolete' if getattr(commit, 'obsolete', None) else 'commit_hash'}">${h.show_id(commit)}</span>
51 </a>
51 </a>
52 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
52 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
53 </code>
54 </td>
55 <td class="td-tags tags-col">
56 ## phase
53 % if hasattr(commit, 'phase'):
57 % if hasattr(commit, 'phase'):
54 % if commit.phase != 'public':
58 % if commit.phase != 'public':
55 <span class="tag phase-${commit.phase} tooltip" title="${_('Commit phase')}">${commit.phase}</span>
59 <span class="tag phase-${commit.phase} tooltip" title="${_('Commit phase')}">${commit.phase}</span>
56 % endif
60 % endif
57 % endif
61 % endif
58
62
59 ## obsolete commits
63 ## obsolete commits
60 % if hasattr(commit, 'obsolete'):
64 % if hasattr(commit, 'obsolete'):
61 % if commit.obsolete:
65 % if commit.obsolete:
62 <span class="tag obsolete-${commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
66 <span class="tag obsolete-${commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
63 % endif
67 % endif
64 % endif
68 % endif
65
69
66 ## hidden commits
70 ## hidden commits
67 % if hasattr(commit, 'hidden'):
71 % if hasattr(commit, 'hidden'):
68 % if commit.hidden:
72 % if commit.hidden:
69 <span class="tag obsolete-${commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
73 <span class="tag obsolete-${commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
70 % endif
74 % endif
71 % endif
75 % endif
72
73 </code>
74 </td>
76 </td>
75 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
77 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
76 <div class="show_more_col">
78 <div class="show_more_col">
77 <i class="show_more"></i>&nbsp;
79 <i class="show_more"></i>&nbsp;
78 </div>
80 </div>
79 </td>
81 </td>
80 <td class="td-description mid">
82 <td class="td-description mid">
81 <div class="log-container truncate-wrap">
83 <div class="log-container truncate-wrap">
82 <div class="message truncate" id="c-${commit.raw_id}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
84 <div class="message truncate" id="c-${commit.raw_id}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
83 </div>
85 </div>
84 </td>
86 </td>
85
87
86 <td class="td-time">
88 <td class="td-time">
87 ${h.age_component(commit.date)}
89 ${h.age_component(commit.date)}
88 </td>
90 </td>
89 <td class="td-user">
91 <td class="td-user">
90 ${base.gravatar_with_user(commit.author)}
92 ${base.gravatar_with_user(commit.author)}
91 </td>
93 </td>
92
94
93 <td class="td-tags tags-col">
95 <td class="td-tags tags-col">
94 <div id="t-${commit.raw_id}">
96 <div id="t-${commit.raw_id}">
95
97
96 ## merge
98 ## merge
97 %if commit.merge:
99 %if commit.merge:
98 <span class="tag mergetag">
100 <span class="tag mergetag">
99 <i class="icon-merge"></i>${_('merge')}
101 <i class="icon-merge"></i>${_('merge')}
100 </span>
102 </span>
101 %endif
103 %endif
102
104
103 ## branch
105 ## branch
104 %if commit.branch:
106 %if commit.branch:
105 <span class="tag branchtag" title="${h.tooltip(_('Branch %s') % commit.branch)}">
107 <span class="tag branchtag" title="${h.tooltip(_('Branch %s') % commit.branch)}">
106 <a href="${h.route_path('repo_changelog',repo_name=c.repo_name,_query=dict(branch=commit.branch))}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
108 <a href="${h.route_path('repo_changelog',repo_name=c.repo_name,_query=dict(branch=commit.branch))}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
107 </span>
109 </span>
108 %endif
110 %endif
109
111
110 ## bookmarks
112 ## bookmarks
111 %if h.is_hg(c.rhodecode_repo):
113 %if h.is_hg(c.rhodecode_repo):
112 %for book in commit.bookmarks:
114 %for book in commit.bookmarks:
113 <span class="tag booktag" title="${h.tooltip(_('Bookmark %s') % book)}">
115 <span class="tag booktag" title="${h.tooltip(_('Bookmark %s') % book)}">
114 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id, _query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
116 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id, _query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
115 </span>
117 </span>
116 %endfor
118 %endfor
117 %endif
119 %endif
118
120
119 ## tags
121 ## tags
120 %for tag in commit.tags:
122 %for tag in commit.tags:
121 <span class="tag tagtag" title="${h.tooltip(_('Tag %s') % tag)}">
123 <span class="tag tagtag" title="${h.tooltip(_('Tag %s') % tag)}">
122 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id, _query=dict(at=tag))}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
124 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id, _query=dict(at=tag))}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
123 </span>
125 </span>
124 %endfor
126 %endfor
125
127
126 </div>
128 </div>
127 </td>
129 </td>
128 </tr>
130 </tr>
129 % endfor
131 % endfor
130
132
131 % if c.next_page:
133 % if c.next_page:
132 <tr>
134 <tr>
133 <td colspan="9" class="load-more-commits">
135 <td colspan="10" class="load-more-commits">
134 <a class="next-commits" href="#loadNextCommits" onclick="commitsController.loadNext(this, ${c.next_page}, '${c.branch_name}', '${c.commit_id}', '${c.f_path}');return false">
136 <a class="next-commits" href="#loadNextCommits" onclick="commitsController.loadNext(this, ${c.next_page}, '${c.branch_name}', '${c.commit_id}', '${c.f_path}');return false">
135 ${_('load next')}
137 ${_('load next')}
136 </a>
138 </a>
137 </td>
139 </td>
138 </tr>
140 </tr>
139 % endif
141 % endif
140 <tr class="chunk-graph-data" style="display:none"
142 <tr class="chunk-graph-data" style="display:none"
141 data-graph='${c.graph_data|n}'
143 data-graph='${c.graph_data|n}'
142 data-node='${c.prev_page}:${c.next_page}'
144 data-node='${c.prev_page}:${c.next_page}'
143 data-commits='${c.graph_commits|n}'>
145 data-commits='${c.graph_commits|n}'>
144 </tr> No newline at end of file
146 </tr>
@@ -1,575 +1,590 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import datetime
21 import datetime
22 import time
22 import time
23
23
24 import pytest
24 import pytest
25
25
26 from rhodecode.lib.vcs.backends.base import (
26 from rhodecode.lib.vcs.backends.base import (
27 CollectionGenerator, FILEMODE_DEFAULT, EmptyCommit)
27 CollectionGenerator, FILEMODE_DEFAULT, EmptyCommit)
28 from rhodecode.lib.vcs.exceptions import (
28 from rhodecode.lib.vcs.exceptions import (
29 BranchDoesNotExistError, CommitDoesNotExistError,
29 BranchDoesNotExistError, CommitDoesNotExistError,
30 RepositoryError, EmptyRepositoryError)
30 RepositoryError, EmptyRepositoryError)
31 from rhodecode.lib.vcs.nodes import (
31 from rhodecode.lib.vcs.nodes import (
32 FileNode, AddedFileNodesGenerator,
32 FileNode, AddedFileNodesGenerator,
33 ChangedFileNodesGenerator, RemovedFileNodesGenerator)
33 ChangedFileNodesGenerator, RemovedFileNodesGenerator)
34 from rhodecode.tests import get_new_dir
34 from rhodecode.tests import get_new_dir
35 from rhodecode.tests.vcs.base import BackendTestMixin
35 from rhodecode.tests.vcs.base import BackendTestMixin
36
36
37
37
38 class TestBaseChangeset:
38 class TestBaseChangeset:
39
39
40 def test_is_deprecated(self):
40 def test_is_deprecated(self):
41 from rhodecode.lib.vcs.backends.base import BaseChangeset
41 from rhodecode.lib.vcs.backends.base import BaseChangeset
42 pytest.deprecated_call(BaseChangeset)
42 pytest.deprecated_call(BaseChangeset)
43
43
44
44
45 class TestEmptyCommit:
45 class TestEmptyCommit:
46
46
47 def test_branch_without_alias_returns_none(self):
47 def test_branch_without_alias_returns_none(self):
48 commit = EmptyCommit()
48 commit = EmptyCommit()
49 assert commit.branch is None
49 assert commit.branch is None
50
50
51
51
52 class TestCommitsInNonEmptyRepo(BackendTestMixin):
52 class TestCommitsInNonEmptyRepo(BackendTestMixin):
53 recreate_repo_per_test = True
53 recreate_repo_per_test = True
54
54
55 @classmethod
55 @classmethod
56 def _get_commits(cls):
56 def _get_commits(cls):
57 start_date = datetime.datetime(2010, 1, 1, 20)
57 start_date = datetime.datetime(2010, 1, 1, 20)
58 for x in xrange(5):
58 for x in xrange(5):
59 yield {
59 yield {
60 'message': 'Commit %d' % x,
60 'message': 'Commit %d' % x,
61 'author': 'Joe Doe <joe.doe@example.com>',
61 'author': 'Joe Doe <joe.doe@example.com>',
62 'date': start_date + datetime.timedelta(hours=12 * x),
62 'date': start_date + datetime.timedelta(hours=12 * x),
63 'added': [
63 'added': [
64 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
64 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
65 ],
65 ],
66 }
66 }
67
67
68 def test_walk_returns_empty_list_in_case_of_file(self):
68 def test_walk_returns_empty_list_in_case_of_file(self):
69 result = list(self.tip.walk('file_0.txt'))
69 result = list(self.tip.walk('file_0.txt'))
70 assert result == []
70 assert result == []
71
71
72 @pytest.mark.backends("git", "hg")
72 @pytest.mark.backends("git", "hg")
73 def test_new_branch(self):
73 def test_new_branch(self):
74 self.imc.add(FileNode('docs/index.txt',
74 self.imc.add(FileNode('docs/index.txt',
75 content='Documentation\n'))
75 content='Documentation\n'))
76 foobar_tip = self.imc.commit(
76 foobar_tip = self.imc.commit(
77 message=u'New branch: foobar',
77 message=u'New branch: foobar',
78 author=u'joe',
78 author=u'joe',
79 branch='foobar',
79 branch='foobar',
80 )
80 )
81 assert 'foobar' in self.repo.branches
81 assert 'foobar' in self.repo.branches
82 assert foobar_tip.branch == 'foobar'
82 assert foobar_tip.branch == 'foobar'
83 # 'foobar' should be the only branch that contains the new commit
83 # 'foobar' should be the only branch that contains the new commit
84 branch = self.repo.branches.values()
84 branch = self.repo.branches.values()
85 assert branch[0] != branch[1]
85 assert branch[0] != branch[1]
86
86
87 @pytest.mark.backends("git", "hg")
87 @pytest.mark.backends("git", "hg")
88 def test_new_head_in_default_branch(self):
88 def test_new_head_in_default_branch(self):
89 tip = self.repo.get_commit()
89 tip = self.repo.get_commit()
90 self.imc.add(FileNode('docs/index.txt',
90 self.imc.add(FileNode('docs/index.txt',
91 content='Documentation\n'))
91 content='Documentation\n'))
92 foobar_tip = self.imc.commit(
92 foobar_tip = self.imc.commit(
93 message=u'New branch: foobar',
93 message=u'New branch: foobar',
94 author=u'joe',
94 author=u'joe',
95 branch='foobar',
95 branch='foobar',
96 parents=[tip],
96 parents=[tip],
97 )
97 )
98 self.imc.change(FileNode('docs/index.txt',
98 self.imc.change(FileNode('docs/index.txt',
99 content='Documentation\nand more...\n'))
99 content='Documentation\nand more...\n'))
100 newtip = self.imc.commit(
100 newtip = self.imc.commit(
101 message=u'At default branch',
101 message=u'At default branch',
102 author=u'joe',
102 author=u'joe',
103 branch=foobar_tip.branch,
103 branch=foobar_tip.branch,
104 parents=[foobar_tip],
104 parents=[foobar_tip],
105 )
105 )
106
106
107 newest_tip = self.imc.commit(
107 newest_tip = self.imc.commit(
108 message=u'Merged with %s' % foobar_tip.raw_id,
108 message=u'Merged with %s' % foobar_tip.raw_id,
109 author=u'joe',
109 author=u'joe',
110 branch=self.backend_class.DEFAULT_BRANCH_NAME,
110 branch=self.backend_class.DEFAULT_BRANCH_NAME,
111 parents=[newtip, foobar_tip],
111 parents=[newtip, foobar_tip],
112 )
112 )
113
113
114 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
114 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
115
115
116 @pytest.mark.backends("git", "hg")
116 @pytest.mark.backends("git", "hg")
117 def test_get_commits_respects_branch_name(self):
117 def test_get_commits_respects_branch_name(self):
118 """
118 """
119 * e1930d0 (HEAD, master) Back in default branch
119 * e1930d0 (HEAD, master) Back in default branch
120 | * e1930d0 (docs) New Branch: docs2
120 | * e1930d0 (docs) New Branch: docs2
121 | * dcc14fa New branch: docs
121 | * dcc14fa New branch: docs
122 |/
122 |/
123 * e63c41a Initial commit
123 * e63c41a Initial commit
124 ...
124 ...
125 * 624d3db Commit 0
125 * 624d3db Commit 0
126
126
127 :return:
127 :return:
128 """
128 """
129 DEFAULT_BRANCH = self.repo.DEFAULT_BRANCH_NAME
129 DEFAULT_BRANCH = self.repo.DEFAULT_BRANCH_NAME
130 TEST_BRANCH = 'docs'
130 TEST_BRANCH = 'docs'
131 org_tip = self.repo.get_commit()
131 org_tip = self.repo.get_commit()
132
132
133 self.imc.add(FileNode('readme.txt', content='Document\n'))
133 self.imc.add(FileNode('readme.txt', content='Document\n'))
134 initial = self.imc.commit(
134 initial = self.imc.commit(
135 message=u'Initial commit',
135 message=u'Initial commit',
136 author=u'joe',
136 author=u'joe',
137 parents=[org_tip],
137 parents=[org_tip],
138 branch=DEFAULT_BRANCH,)
138 branch=DEFAULT_BRANCH,)
139
139
140 self.imc.add(FileNode('newdoc.txt', content='foobar\n'))
140 self.imc.add(FileNode('newdoc.txt', content='foobar\n'))
141 docs_branch_commit1 = self.imc.commit(
141 docs_branch_commit1 = self.imc.commit(
142 message=u'New branch: docs',
142 message=u'New branch: docs',
143 author=u'joe',
143 author=u'joe',
144 parents=[initial],
144 parents=[initial],
145 branch=TEST_BRANCH,)
145 branch=TEST_BRANCH,)
146
146
147 self.imc.add(FileNode('newdoc2.txt', content='foobar2\n'))
147 self.imc.add(FileNode('newdoc2.txt', content='foobar2\n'))
148 docs_branch_commit2 = self.imc.commit(
148 docs_branch_commit2 = self.imc.commit(
149 message=u'New branch: docs2',
149 message=u'New branch: docs2',
150 author=u'joe',
150 author=u'joe',
151 parents=[docs_branch_commit1],
151 parents=[docs_branch_commit1],
152 branch=TEST_BRANCH,)
152 branch=TEST_BRANCH,)
153
153
154 self.imc.add(FileNode('newfile', content='hello world\n'))
154 self.imc.add(FileNode('newfile', content='hello world\n'))
155 self.imc.commit(
155 self.imc.commit(
156 message=u'Back in default branch',
156 message=u'Back in default branch',
157 author=u'joe',
157 author=u'joe',
158 parents=[initial],
158 parents=[initial],
159 branch=DEFAULT_BRANCH,)
159 branch=DEFAULT_BRANCH,)
160
160
161 default_branch_commits = self.repo.get_commits(
161 default_branch_commits = self.repo.get_commits(
162 branch_name=DEFAULT_BRANCH)
162 branch_name=DEFAULT_BRANCH)
163 assert docs_branch_commit1 not in list(default_branch_commits)
163 assert docs_branch_commit1 not in list(default_branch_commits)
164 assert docs_branch_commit2 not in list(default_branch_commits)
164 assert docs_branch_commit2 not in list(default_branch_commits)
165
165
166 docs_branch_commits = self.repo.get_commits(
166 docs_branch_commits = self.repo.get_commits(
167 start_id=self.repo.commit_ids[0], end_id=self.repo.commit_ids[-1],
167 start_id=self.repo.commit_ids[0], end_id=self.repo.commit_ids[-1],
168 branch_name=TEST_BRANCH)
168 branch_name=TEST_BRANCH)
169 assert docs_branch_commit1 in list(docs_branch_commits)
169 assert docs_branch_commit1 in list(docs_branch_commits)
170 assert docs_branch_commit2 in list(docs_branch_commits)
170 assert docs_branch_commit2 in list(docs_branch_commits)
171
171
172 @pytest.mark.backends("svn")
172 @pytest.mark.backends("svn")
173 def test_get_commits_respects_branch_name_svn(self, vcsbackend_svn):
173 def test_get_commits_respects_branch_name_svn(self, vcsbackend_svn):
174 repo = vcsbackend_svn['svn-simple-layout']
174 repo = vcsbackend_svn['svn-simple-layout']
175 commits = repo.get_commits(branch_name='trunk')
175 commits = repo.get_commits(branch_name='trunk')
176 commit_indexes = [c.idx for c in commits]
176 commit_indexes = [c.idx for c in commits]
177 assert commit_indexes == [1, 2, 3, 7, 12, 15]
177 assert commit_indexes == [1, 2, 3, 7, 12, 15]
178
178
179 def test_get_commit_by_branch(self):
179 def test_get_commit_by_branch(self):
180 for branch, commit_id in self.repo.branches.iteritems():
180 for branch, commit_id in self.repo.branches.iteritems():
181 assert commit_id == self.repo.get_commit(branch).raw_id
181 assert commit_id == self.repo.get_commit(branch).raw_id
182
182
183 def test_get_commit_by_tag(self):
183 def test_get_commit_by_tag(self):
184 for tag, commit_id in self.repo.tags.iteritems():
184 for tag, commit_id in self.repo.tags.iteritems():
185 assert commit_id == self.repo.get_commit(tag).raw_id
185 assert commit_id == self.repo.get_commit(tag).raw_id
186
186
187 def test_get_commit_parents(self):
187 def test_get_commit_parents(self):
188 repo = self.repo
188 repo = self.repo
189 for test_idx in [1, 2, 3]:
189 for test_idx in [1, 2, 3]:
190 commit = repo.get_commit(commit_idx=test_idx - 1)
190 commit = repo.get_commit(commit_idx=test_idx - 1)
191 assert [commit] == repo.get_commit(commit_idx=test_idx).parents
191 assert [commit] == repo.get_commit(commit_idx=test_idx).parents
192
192
193 def test_get_commit_children(self):
193 def test_get_commit_children(self):
194 repo = self.repo
194 repo = self.repo
195 for test_idx in [1, 2, 3]:
195 for test_idx in [1, 2, 3]:
196 commit = repo.get_commit(commit_idx=test_idx + 1)
196 commit = repo.get_commit(commit_idx=test_idx + 1)
197 assert [commit] == repo.get_commit(commit_idx=test_idx).children
197 assert [commit] == repo.get_commit(commit_idx=test_idx).children
198
198
199
199
200 class TestCommits(BackendTestMixin):
200 class TestCommits(BackendTestMixin):
201 recreate_repo_per_test = False
201 recreate_repo_per_test = False
202
202
203 @classmethod
203 @classmethod
204 def _get_commits(cls):
204 def _get_commits(cls):
205 start_date = datetime.datetime(2010, 1, 1, 20)
205 start_date = datetime.datetime(2010, 1, 1, 20)
206 for x in xrange(5):
206 for x in xrange(5):
207 yield {
207 yield {
208 'message': u'Commit %d' % x,
208 'message': u'Commit %d' % x,
209 'author': u'Joe Doe <joe.doe@example.com>',
209 'author': u'Joe Doe <joe.doe@example.com>',
210 'date': start_date + datetime.timedelta(hours=12 * x),
210 'date': start_date + datetime.timedelta(hours=12 * x),
211 'added': [
211 'added': [
212 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
212 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
213 ],
213 ],
214 }
214 }
215
215
216 def test_simple(self):
216 def test_simple(self):
217 tip = self.repo.get_commit()
217 tip = self.repo.get_commit()
218 assert tip.date, datetime.datetime(2010, 1, 3 == 20)
218 assert tip.date, datetime.datetime(2010, 1, 3 == 20)
219
219
220 def test_simple_serialized_commit(self):
220 def test_simple_serialized_commit(self):
221 tip = self.repo.get_commit()
221 tip = self.repo.get_commit()
222 # json.dumps(tip) uses .__json__() method
222 # json.dumps(tip) uses .__json__() method
223 data = tip.__json__()
223 data = tip.__json__()
224 assert 'branch' in data
224 assert 'branch' in data
225 assert data['revision']
225 assert data['revision']
226
226
227 def test_retrieve_tip(self):
227 def test_retrieve_tip(self):
228 tip = self.repo.get_commit('tip')
228 tip = self.repo.get_commit('tip')
229 assert tip == self.repo.get_commit()
229 assert tip == self.repo.get_commit()
230
230
231 def test_invalid(self):
231 def test_invalid(self):
232 with pytest.raises(CommitDoesNotExistError):
232 with pytest.raises(CommitDoesNotExistError):
233 self.repo.get_commit(commit_idx=123456789)
233 self.repo.get_commit(commit_idx=123456789)
234
234
235 def test_idx(self):
235 def test_idx(self):
236 commit = self.repo[0]
236 commit = self.repo[0]
237 assert commit.idx == 0
237 assert commit.idx == 0
238
238
239 def test_negative_idx(self):
239 def test_negative_idx(self):
240 commit = self.repo.get_commit(commit_idx=-1)
240 commit = self.repo.get_commit(commit_idx=-1)
241 assert commit.idx >= 0
241 assert commit.idx >= 0
242
242
243 def test_revision_is_deprecated(self):
243 def test_revision_is_deprecated(self):
244 def get_revision(commit):
244 def get_revision(commit):
245 return commit.revision
245 return commit.revision
246
246
247 commit = self.repo[0]
247 commit = self.repo[0]
248 pytest.deprecated_call(get_revision, commit)
248 pytest.deprecated_call(get_revision, commit)
249
249
250 def test_size(self):
250 def test_size(self):
251 tip = self.repo.get_commit()
251 tip = self.repo.get_commit()
252 size = 5 * len('Foobar N') # Size of 5 files
252 size = 5 * len('Foobar N') # Size of 5 files
253 assert tip.size == size
253 assert tip.size == size
254
254
255 def test_size_at_commit(self):
255 def test_size_at_commit(self):
256 tip = self.repo.get_commit()
256 tip = self.repo.get_commit()
257 size = 5 * len('Foobar N') # Size of 5 files
257 size = 5 * len('Foobar N') # Size of 5 files
258 assert self.repo.size_at_commit(tip.raw_id) == size
258 assert self.repo.size_at_commit(tip.raw_id) == size
259
259
260 def test_size_at_first_commit(self):
260 def test_size_at_first_commit(self):
261 commit = self.repo[0]
261 commit = self.repo[0]
262 size = len('Foobar N') # Size of 1 file
262 size = len('Foobar N') # Size of 1 file
263 assert self.repo.size_at_commit(commit.raw_id) == size
263 assert self.repo.size_at_commit(commit.raw_id) == size
264
264
265 def test_author(self):
265 def test_author(self):
266 tip = self.repo.get_commit()
266 tip = self.repo.get_commit()
267 assert_text_equal(tip.author, u'Joe Doe <joe.doe@example.com>')
267 assert_text_equal(tip.author, u'Joe Doe <joe.doe@example.com>')
268
268
269 def test_author_name(self):
269 def test_author_name(self):
270 tip = self.repo.get_commit()
270 tip = self.repo.get_commit()
271 assert_text_equal(tip.author_name, u'Joe Doe')
271 assert_text_equal(tip.author_name, u'Joe Doe')
272
272
273 def test_author_email(self):
273 def test_author_email(self):
274 tip = self.repo.get_commit()
274 tip = self.repo.get_commit()
275 assert_text_equal(tip.author_email, u'joe.doe@example.com')
275 assert_text_equal(tip.author_email, u'joe.doe@example.com')
276
276
277 def test_message(self):
277 def test_message(self):
278 tip = self.repo.get_commit()
278 tip = self.repo.get_commit()
279 assert_text_equal(tip.message, u'Commit 4')
279 assert_text_equal(tip.message, u'Commit 4')
280
280
281 def test_diff(self):
281 def test_diff(self):
282 tip = self.repo.get_commit()
282 tip = self.repo.get_commit()
283 diff = tip.diff()
283 diff = tip.diff()
284 assert "+Foobar 4" in diff.raw
284 assert "+Foobar 4" in diff.raw
285
285
286 def test_prev(self):
286 def test_prev(self):
287 tip = self.repo.get_commit()
287 tip = self.repo.get_commit()
288 prev_commit = tip.prev()
288 prev_commit = tip.prev()
289 assert prev_commit.message == 'Commit 3'
289 assert prev_commit.message == 'Commit 3'
290
290
291 def test_prev_raises_on_first_commit(self):
291 def test_prev_raises_on_first_commit(self):
292 commit = self.repo.get_commit(commit_idx=0)
292 commit = self.repo.get_commit(commit_idx=0)
293 with pytest.raises(CommitDoesNotExistError):
293 with pytest.raises(CommitDoesNotExistError):
294 commit.prev()
294 commit.prev()
295
295
296 def test_prev_works_on_second_commit_issue_183(self):
296 def test_prev_works_on_second_commit_issue_183(self):
297 commit = self.repo.get_commit(commit_idx=1)
297 commit = self.repo.get_commit(commit_idx=1)
298 prev_commit = commit.prev()
298 prev_commit = commit.prev()
299 assert prev_commit.idx == 0
299 assert prev_commit.idx == 0
300
300
301 def test_next(self):
301 def test_next(self):
302 commit = self.repo.get_commit(commit_idx=2)
302 commit = self.repo.get_commit(commit_idx=2)
303 next_commit = commit.next()
303 next_commit = commit.next()
304 assert next_commit.message == 'Commit 3'
304 assert next_commit.message == 'Commit 3'
305
305
306 def test_next_raises_on_tip(self):
306 def test_next_raises_on_tip(self):
307 commit = self.repo.get_commit()
307 commit = self.repo.get_commit()
308 with pytest.raises(CommitDoesNotExistError):
308 with pytest.raises(CommitDoesNotExistError):
309 commit.next()
309 commit.next()
310
310
311 def test_get_file_commit(self):
311 def test_get_file_commit(self):
312 commit = self.repo.get_commit()
312 commit = self.repo.get_commit()
313 commit.get_file_commit('file_4.txt')
313 commit.get_file_commit('file_4.txt')
314 assert commit.message == 'Commit 4'
314 assert commit.message == 'Commit 4'
315
315
316 def test_get_filenodes_generator(self):
316 def test_get_filenodes_generator(self):
317 tip = self.repo.get_commit()
317 tip = self.repo.get_commit()
318 filepaths = [node.path for node in tip.get_filenodes_generator()]
318 filepaths = [node.path for node in tip.get_filenodes_generator()]
319 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
319 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
320
320
321 def test_get_file_annotate(self):
321 def test_get_file_annotate(self):
322 file_added_commit = self.repo.get_commit(commit_idx=3)
322 file_added_commit = self.repo.get_commit(commit_idx=3)
323 annotations = list(file_added_commit.get_file_annotate('file_3.txt'))
323 annotations = list(file_added_commit.get_file_annotate('file_3.txt'))
324
324
325 line_no, commit_id, commit_loader, line = annotations[0]
325 line_no, commit_id, commit_loader, line = annotations[0]
326
326
327 assert line_no == 1
327 assert line_no == 1
328 assert commit_id == file_added_commit.raw_id
328 assert commit_id == file_added_commit.raw_id
329 assert commit_loader() == file_added_commit
329 assert commit_loader() == file_added_commit
330 assert 'Foobar 3' in line
330 assert 'Foobar 3' in line
331
331
332 def test_get_file_annotate_does_not_exist(self):
332 def test_get_file_annotate_does_not_exist(self):
333 file_added_commit = self.repo.get_commit(commit_idx=2)
333 file_added_commit = self.repo.get_commit(commit_idx=2)
334 # TODO: Should use a specific exception class here?
334 # TODO: Should use a specific exception class here?
335 with pytest.raises(Exception):
335 with pytest.raises(Exception):
336 list(file_added_commit.get_file_annotate('file_3.txt'))
336 list(file_added_commit.get_file_annotate('file_3.txt'))
337
337
338 def test_get_file_annotate_tip(self):
338 def test_get_file_annotate_tip(self):
339 tip = self.repo.get_commit()
339 tip = self.repo.get_commit()
340 commit = self.repo.get_commit(commit_idx=3)
340 commit = self.repo.get_commit(commit_idx=3)
341 expected_values = list(commit.get_file_annotate('file_3.txt'))
341 expected_values = list(commit.get_file_annotate('file_3.txt'))
342 annotations = list(tip.get_file_annotate('file_3.txt'))
342 annotations = list(tip.get_file_annotate('file_3.txt'))
343
343
344 # Note: Skip index 2 because the loader function is not the same
344 # Note: Skip index 2 because the loader function is not the same
345 for idx in (0, 1, 3):
345 for idx in (0, 1, 3):
346 assert annotations[0][idx] == expected_values[0][idx]
346 assert annotations[0][idx] == expected_values[0][idx]
347
347
348 def test_get_commits_is_ordered_by_date(self):
348 def test_get_commits_is_ordered_by_date(self):
349 commits = self.repo.get_commits()
349 commits = self.repo.get_commits()
350 assert isinstance(commits, CollectionGenerator)
350 assert isinstance(commits, CollectionGenerator)
351 assert len(commits) == 0 or len(commits) != 0
351 assert len(commits) == 0 or len(commits) != 0
352 commits = list(commits)
352 commits = list(commits)
353 ordered_by_date = sorted(commits, key=lambda commit: commit.date)
353 ordered_by_date = sorted(commits, key=lambda commit: commit.date)
354 assert commits == ordered_by_date
354 assert commits == ordered_by_date
355
355
356 def test_get_commits_respects_start(self):
356 def test_get_commits_respects_start(self):
357 second_id = self.repo.commit_ids[1]
357 second_id = self.repo.commit_ids[1]
358 commits = self.repo.get_commits(start_id=second_id)
358 commits = self.repo.get_commits(start_id=second_id)
359 assert isinstance(commits, CollectionGenerator)
359 assert isinstance(commits, CollectionGenerator)
360 commits = list(commits)
360 commits = list(commits)
361 assert len(commits) == 4
361 assert len(commits) == 4
362
362
363 def test_get_commits_includes_start_commit(self):
363 def test_get_commits_includes_start_commit(self):
364 second_id = self.repo.commit_ids[1]
364 second_id = self.repo.commit_ids[1]
365 commits = self.repo.get_commits(start_id=second_id)
365 commits = self.repo.get_commits(start_id=second_id)
366 assert isinstance(commits, CollectionGenerator)
366 assert isinstance(commits, CollectionGenerator)
367 commits = list(commits)
367 commits = list(commits)
368 assert commits[0].raw_id == second_id
368 assert commits[0].raw_id == second_id
369
369
370 def test_get_commits_respects_end(self):
370 def test_get_commits_respects_end(self):
371 second_id = self.repo.commit_ids[1]
371 second_id = self.repo.commit_ids[1]
372 commits = self.repo.get_commits(end_id=second_id)
372 commits = self.repo.get_commits(end_id=second_id)
373 assert isinstance(commits, CollectionGenerator)
373 assert isinstance(commits, CollectionGenerator)
374 commits = list(commits)
374 commits = list(commits)
375 assert commits[-1].raw_id == second_id
375 assert commits[-1].raw_id == second_id
376 assert len(commits) == 2
376 assert len(commits) == 2
377
377
378 def test_get_commits_respects_both_start_and_end(self):
378 def test_get_commits_respects_both_start_and_end(self):
379 second_id = self.repo.commit_ids[1]
379 second_id = self.repo.commit_ids[1]
380 third_id = self.repo.commit_ids[2]
380 third_id = self.repo.commit_ids[2]
381 commits = self.repo.get_commits(start_id=second_id, end_id=third_id)
381 commits = self.repo.get_commits(start_id=second_id, end_id=third_id)
382 assert isinstance(commits, CollectionGenerator)
382 assert isinstance(commits, CollectionGenerator)
383 commits = list(commits)
383 commits = list(commits)
384 assert len(commits) == 2
384 assert len(commits) == 2
385
385
386 def test_get_commits_on_empty_repo_raises_EmptyRepository_error(self):
386 def test_get_commits_on_empty_repo_raises_EmptyRepository_error(self):
387 repo_path = get_new_dir(str(time.time()))
387 repo_path = get_new_dir(str(time.time()))
388 repo = self.Backend(repo_path, create=True)
388 repo = self.Backend(repo_path, create=True)
389
389
390 with pytest.raises(EmptyRepositoryError):
390 with pytest.raises(EmptyRepositoryError):
391 list(repo.get_commits(start_id='foobar'))
391 list(repo.get_commits(start_id='foobar'))
392
392
393 def test_get_commits_respects_hidden(self):
394 commits = self.repo.get_commits(show_hidden=True)
395 assert isinstance(commits, CollectionGenerator)
396 assert len(commits) == 5
397
393 def test_get_commits_includes_end_commit(self):
398 def test_get_commits_includes_end_commit(self):
394 second_id = self.repo.commit_ids[1]
399 second_id = self.repo.commit_ids[1]
395 commits = self.repo.get_commits(end_id=second_id)
400 commits = self.repo.get_commits(end_id=second_id)
396 assert isinstance(commits, CollectionGenerator)
401 assert isinstance(commits, CollectionGenerator)
397 assert len(commits) == 2
402 assert len(commits) == 2
398 commits = list(commits)
403 commits = list(commits)
399 assert commits[-1].raw_id == second_id
404 assert commits[-1].raw_id == second_id
400
405
401 def test_get_commits_respects_start_date(self):
406 def test_get_commits_respects_start_date(self):
402 start_date = datetime.datetime(2010, 1, 2)
407 start_date = datetime.datetime(2010, 1, 2)
403 commits = self.repo.get_commits(start_date=start_date)
408 commits = self.repo.get_commits(start_date=start_date)
404 assert isinstance(commits, CollectionGenerator)
409 assert isinstance(commits, CollectionGenerator)
405 # Should be 4 commits after 2010-01-02 00:00:00
410 # Should be 4 commits after 2010-01-02 00:00:00
406 assert len(commits) == 4
411 assert len(commits) == 4
407 for c in commits:
412 for c in commits:
408 assert c.date >= start_date
413 assert c.date >= start_date
409
414
415 def test_get_commits_respects_start_date_with_branch(self):
416 start_date = datetime.datetime(2010, 1, 2)
417 commits = self.repo.get_commits(
418 start_date=start_date, branch_name=self.repo.DEFAULT_BRANCH_NAME)
419 assert isinstance(commits, CollectionGenerator)
420 # Should be 4 commits after 2010-01-02 00:00:00
421 assert len(commits) == 4
422 for c in commits:
423 assert c.date >= start_date
424
410 def test_get_commits_respects_start_date_and_end_date(self):
425 def test_get_commits_respects_start_date_and_end_date(self):
411 start_date = datetime.datetime(2010, 1, 2)
426 start_date = datetime.datetime(2010, 1, 2)
412 end_date = datetime.datetime(2010, 1, 3)
427 end_date = datetime.datetime(2010, 1, 3)
413 commits = self.repo.get_commits(start_date=start_date,
428 commits = self.repo.get_commits(start_date=start_date,
414 end_date=end_date)
429 end_date=end_date)
415 assert isinstance(commits, CollectionGenerator)
430 assert isinstance(commits, CollectionGenerator)
416 assert len(commits) == 2
431 assert len(commits) == 2
417 for c in commits:
432 for c in commits:
418 assert c.date >= start_date
433 assert c.date >= start_date
419 assert c.date <= end_date
434 assert c.date <= end_date
420
435
421 def test_get_commits_respects_end_date(self):
436 def test_get_commits_respects_end_date(self):
422 end_date = datetime.datetime(2010, 1, 2)
437 end_date = datetime.datetime(2010, 1, 2)
423 commits = self.repo.get_commits(end_date=end_date)
438 commits = self.repo.get_commits(end_date=end_date)
424 assert isinstance(commits, CollectionGenerator)
439 assert isinstance(commits, CollectionGenerator)
425 assert len(commits) == 1
440 assert len(commits) == 1
426 for c in commits:
441 for c in commits:
427 assert c.date <= end_date
442 assert c.date <= end_date
428
443
429 def test_get_commits_respects_reverse(self):
444 def test_get_commits_respects_reverse(self):
430 commits = self.repo.get_commits() # no longer reverse support
445 commits = self.repo.get_commits() # no longer reverse support
431 assert isinstance(commits, CollectionGenerator)
446 assert isinstance(commits, CollectionGenerator)
432 assert len(commits) == 5
447 assert len(commits) == 5
433 commit_ids = reversed([c.raw_id for c in commits])
448 commit_ids = reversed([c.raw_id for c in commits])
434 assert list(commit_ids) == list(reversed(self.repo.commit_ids))
449 assert list(commit_ids) == list(reversed(self.repo.commit_ids))
435
450
436 def test_get_commits_slice_generator(self):
451 def test_get_commits_slice_generator(self):
437 commits = self.repo.get_commits(
452 commits = self.repo.get_commits(
438 branch_name=self.repo.DEFAULT_BRANCH_NAME)
453 branch_name=self.repo.DEFAULT_BRANCH_NAME)
439 assert isinstance(commits, CollectionGenerator)
454 assert isinstance(commits, CollectionGenerator)
440 commit_slice = list(commits[1:3])
455 commit_slice = list(commits[1:3])
441 assert len(commit_slice) == 2
456 assert len(commit_slice) == 2
442
457
443 def test_get_commits_raise_commitdoesnotexist_for_wrong_start(self):
458 def test_get_commits_raise_commitdoesnotexist_for_wrong_start(self):
444 with pytest.raises(CommitDoesNotExistError):
459 with pytest.raises(CommitDoesNotExistError):
445 list(self.repo.get_commits(start_id='foobar'))
460 list(self.repo.get_commits(start_id='foobar'))
446
461
447 def test_get_commits_raise_commitdoesnotexist_for_wrong_end(self):
462 def test_get_commits_raise_commitdoesnotexist_for_wrong_end(self):
448 with pytest.raises(CommitDoesNotExistError):
463 with pytest.raises(CommitDoesNotExistError):
449 list(self.repo.get_commits(end_id='foobar'))
464 list(self.repo.get_commits(end_id='foobar'))
450
465
451 def test_get_commits_raise_branchdoesnotexist_for_wrong_branch_name(self):
466 def test_get_commits_raise_branchdoesnotexist_for_wrong_branch_name(self):
452 with pytest.raises(BranchDoesNotExistError):
467 with pytest.raises(BranchDoesNotExistError):
453 list(self.repo.get_commits(branch_name='foobar'))
468 list(self.repo.get_commits(branch_name='foobar'))
454
469
455 def test_get_commits_raise_repositoryerror_for_wrong_start_end(self):
470 def test_get_commits_raise_repositoryerror_for_wrong_start_end(self):
456 start_id = self.repo.commit_ids[-1]
471 start_id = self.repo.commit_ids[-1]
457 end_id = self.repo.commit_ids[0]
472 end_id = self.repo.commit_ids[0]
458 with pytest.raises(RepositoryError):
473 with pytest.raises(RepositoryError):
459 list(self.repo.get_commits(start_id=start_id, end_id=end_id))
474 list(self.repo.get_commits(start_id=start_id, end_id=end_id))
460
475
461 def test_get_commits_raises_for_numerical_ids(self):
476 def test_get_commits_raises_for_numerical_ids(self):
462 with pytest.raises(TypeError):
477 with pytest.raises(TypeError):
463 self.repo.get_commits(start_id=1, end_id=2)
478 self.repo.get_commits(start_id=1, end_id=2)
464
479
465 def test_commit_equality(self):
480 def test_commit_equality(self):
466 commit1 = self.repo.get_commit(self.repo.commit_ids[0])
481 commit1 = self.repo.get_commit(self.repo.commit_ids[0])
467 commit2 = self.repo.get_commit(self.repo.commit_ids[1])
482 commit2 = self.repo.get_commit(self.repo.commit_ids[1])
468
483
469 assert commit1 == commit1
484 assert commit1 == commit1
470 assert commit2 == commit2
485 assert commit2 == commit2
471 assert commit1 != commit2
486 assert commit1 != commit2
472 assert commit2 != commit1
487 assert commit2 != commit1
473 assert commit1 != None
488 assert commit1 != None
474 assert None != commit1
489 assert None != commit1
475 assert 1 != commit1
490 assert 1 != commit1
476 assert 'string' != commit1
491 assert 'string' != commit1
477
492
478
493
479 @pytest.mark.parametrize("filename, expected", [
494 @pytest.mark.parametrize("filename, expected", [
480 ("README.rst", False),
495 ("README.rst", False),
481 ("README", True),
496 ("README", True),
482 ])
497 ])
483 def test_commit_is_link(vcsbackend, filename, expected):
498 def test_commit_is_link(vcsbackend, filename, expected):
484 commit = vcsbackend.repo.get_commit()
499 commit = vcsbackend.repo.get_commit()
485 link_status = commit.is_link(filename)
500 link_status = commit.is_link(filename)
486 assert link_status is expected
501 assert link_status is expected
487
502
488
503
489 class TestCommitsChanges(BackendTestMixin):
504 class TestCommitsChanges(BackendTestMixin):
490 recreate_repo_per_test = False
505 recreate_repo_per_test = False
491
506
492 @classmethod
507 @classmethod
493 def _get_commits(cls):
508 def _get_commits(cls):
494 return [
509 return [
495 {
510 {
496 'message': u'Initial',
511 'message': u'Initial',
497 'author': u'Joe Doe <joe.doe@example.com>',
512 'author': u'Joe Doe <joe.doe@example.com>',
498 'date': datetime.datetime(2010, 1, 1, 20),
513 'date': datetime.datetime(2010, 1, 1, 20),
499 'added': [
514 'added': [
500 FileNode('foo/bar', content='foo'),
515 FileNode('foo/bar', content='foo'),
501 FileNode('foo/bał', content='foo'),
516 FileNode('foo/bał', content='foo'),
502 FileNode('foobar', content='foo'),
517 FileNode('foobar', content='foo'),
503 FileNode('qwe', content='foo'),
518 FileNode('qwe', content='foo'),
504 ],
519 ],
505 },
520 },
506 {
521 {
507 'message': u'Massive changes',
522 'message': u'Massive changes',
508 'author': u'Joe Doe <joe.doe@example.com>',
523 'author': u'Joe Doe <joe.doe@example.com>',
509 'date': datetime.datetime(2010, 1, 1, 22),
524 'date': datetime.datetime(2010, 1, 1, 22),
510 'added': [FileNode('fallout', content='War never changes')],
525 'added': [FileNode('fallout', content='War never changes')],
511 'changed': [
526 'changed': [
512 FileNode('foo/bar', content='baz'),
527 FileNode('foo/bar', content='baz'),
513 FileNode('foobar', content='baz'),
528 FileNode('foobar', content='baz'),
514 ],
529 ],
515 'removed': [FileNode('qwe')],
530 'removed': [FileNode('qwe')],
516 },
531 },
517 ]
532 ]
518
533
519 def test_initial_commit(self, local_dt_to_utc):
534 def test_initial_commit(self, local_dt_to_utc):
520 commit = self.repo.get_commit(commit_idx=0)
535 commit = self.repo.get_commit(commit_idx=0)
521 assert set(commit.added) == set([
536 assert set(commit.added) == set([
522 commit.get_node('foo/bar'),
537 commit.get_node('foo/bar'),
523 commit.get_node('foo/bał'),
538 commit.get_node('foo/bał'),
524 commit.get_node('foobar'),
539 commit.get_node('foobar'),
525 commit.get_node('qwe'),
540 commit.get_node('qwe'),
526 ])
541 ])
527 assert set(commit.changed) == set()
542 assert set(commit.changed) == set()
528 assert set(commit.removed) == set()
543 assert set(commit.removed) == set()
529 assert set(commit.affected_files) == set(
544 assert set(commit.affected_files) == set(
530 ['foo/bar', 'foo/bał', 'foobar', 'qwe'])
545 ['foo/bar', 'foo/bał', 'foobar', 'qwe'])
531 assert commit.date == local_dt_to_utc(
546 assert commit.date == local_dt_to_utc(
532 datetime.datetime(2010, 1, 1, 20, 0))
547 datetime.datetime(2010, 1, 1, 20, 0))
533
548
534 def test_head_added(self):
549 def test_head_added(self):
535 commit = self.repo.get_commit()
550 commit = self.repo.get_commit()
536 assert isinstance(commit.added, AddedFileNodesGenerator)
551 assert isinstance(commit.added, AddedFileNodesGenerator)
537 assert set(commit.added) == set([commit.get_node('fallout')])
552 assert set(commit.added) == set([commit.get_node('fallout')])
538 assert isinstance(commit.changed, ChangedFileNodesGenerator)
553 assert isinstance(commit.changed, ChangedFileNodesGenerator)
539 assert set(commit.changed) == set([
554 assert set(commit.changed) == set([
540 commit.get_node('foo/bar'),
555 commit.get_node('foo/bar'),
541 commit.get_node('foobar'),
556 commit.get_node('foobar'),
542 ])
557 ])
543 assert isinstance(commit.removed, RemovedFileNodesGenerator)
558 assert isinstance(commit.removed, RemovedFileNodesGenerator)
544 assert len(commit.removed) == 1
559 assert len(commit.removed) == 1
545 assert list(commit.removed)[0].path == 'qwe'
560 assert list(commit.removed)[0].path == 'qwe'
546
561
547 def test_get_filemode(self):
562 def test_get_filemode(self):
548 commit = self.repo.get_commit()
563 commit = self.repo.get_commit()
549 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bar')
564 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bar')
550
565
551 def test_get_filemode_non_ascii(self):
566 def test_get_filemode_non_ascii(self):
552 commit = self.repo.get_commit()
567 commit = self.repo.get_commit()
553 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bał')
568 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bał')
554 assert FILEMODE_DEFAULT == commit.get_file_mode(u'foo/bał')
569 assert FILEMODE_DEFAULT == commit.get_file_mode(u'foo/bał')
555
570
556 def test_get_file_history(self):
571 def test_get_file_history(self):
557 commit = self.repo.get_commit()
572 commit = self.repo.get_commit()
558 history = commit.get_file_history('foo/bar')
573 history = commit.get_file_history('foo/bar')
559 assert len(history) == 2
574 assert len(history) == 2
560
575
561 def test_get_file_history_with_limit(self):
576 def test_get_file_history_with_limit(self):
562 commit = self.repo.get_commit()
577 commit = self.repo.get_commit()
563 history = commit.get_file_history('foo/bar', limit=1)
578 history = commit.get_file_history('foo/bar', limit=1)
564 assert len(history) == 1
579 assert len(history) == 1
565
580
566 def test_get_file_history_first_commit(self):
581 def test_get_file_history_first_commit(self):
567 commit = self.repo[0]
582 commit = self.repo[0]
568 history = commit.get_file_history('foo/bar')
583 history = commit.get_file_history('foo/bar')
569 assert len(history) == 1
584 assert len(history) == 1
570
585
571
586
572 def assert_text_equal(expected, given):
587 def assert_text_equal(expected, given):
573 assert expected == given
588 assert expected == given
574 assert isinstance(expected, unicode)
589 assert isinstance(expected, unicode)
575 assert isinstance(given, unicode)
590 assert isinstance(given, unicode)
General Comments 0
You need to be logged in to leave comments. Login now