##// 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 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
25 25 from pyramid.view import view_config
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30 import rhodecode.lib.helpers as h
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator)
33 33
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.graphmod import _colored, _dagwalker
36 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 38 from rhodecode.lib.vcs.exceptions import (
39 39 RepositoryError, CommitDoesNotExistError,
40 40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44 DEFAULT_CHANGELOG_SIZE = 20
45 45
46 46
47 47 class RepoChangelogView(RepoAppView):
48 48
49 49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
50 50 """
51 51 This is a safe way to get commit. If an error occurs it redirects to
52 52 tip with proper message
53 53
54 54 :param commit_id: id of commit to fetch
55 55 :param redirect_after: toggle redirection
56 56 """
57 57 _ = self.request.translate
58 58
59 59 try:
60 60 return self.rhodecode_vcs_repo.get_commit(commit_id)
61 61 except EmptyRepositoryError:
62 62 if not redirect_after:
63 63 return None
64 64
65 65 h.flash(h.literal(
66 66 _('There are no commits yet')), category='warning')
67 67 raise HTTPFound(
68 68 h.route_path('repo_summary', repo_name=self.db_repo_name))
69 69
70 70 except (CommitDoesNotExistError, LookupError):
71 71 msg = _('No such commit exists for this repository')
72 72 h.flash(msg, category='error')
73 73 raise HTTPNotFound()
74 74 except RepositoryError as e:
75 75 h.flash(safe_str(h.escape(e)), category='error')
76 76 raise HTTPNotFound()
77 77
78 78 def _graph(self, repo, commits, prev_data=None, next_data=None):
79 79 """
80 80 Generates a DAG graph for repo
81 81
82 82 :param repo: repo instance
83 83 :param commits: list of commits
84 84 """
85 85 if not commits:
86 86 return json.dumps([]), json.dumps([])
87 87
88 88 def serialize(commit, parents=True):
89 89 data = dict(
90 90 raw_id=commit.raw_id,
91 91 idx=commit.idx,
92 92 branch=commit.branch,
93 93 )
94 94 if parents:
95 95 data['parents'] = [
96 96 serialize(x, parents=False) for x in commit.parents]
97 97 return data
98 98
99 99 prev_data = prev_data or []
100 100 next_data = next_data or []
101 101
102 102 current = [serialize(x) for x in commits]
103 103 commits = prev_data + current + next_data
104 104
105 105 dag = _dagwalker(repo, commits)
106 106
107 107 data = [[commit_id, vtx, edges, branch]
108 108 for commit_id, vtx, edges, branch in _colored(dag)]
109 109 return json.dumps(data), json.dumps(current)
110 110
111 111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
112 112 if branch_name not in self.rhodecode_vcs_repo.branches_all:
113 113 h.flash('Branch {} is not found.'.format(h.escape(branch_name)),
114 114 category='warning')
115 115 redirect_url = h.route_path(
116 116 'repo_changelog_file', repo_name=repo_name,
117 117 commit_id=branch_name, f_path=f_path or '')
118 118 raise HTTPFound(redirect_url)
119 119
120 120 def _load_changelog_data(
121 121 self, c, collection, page, chunk_size, branch_name=None,
122 122 dynamic=False, f_path=None, commit_id=None):
123 123
124 124 def url_generator(**kw):
125 125 query_params = {}
126 126 query_params.update(kw)
127 127 if f_path:
128 128 # changelog for file
129 129 return h.route_path(
130 130 'repo_changelog_file',
131 131 repo_name=c.rhodecode_db_repo.repo_name,
132 132 commit_id=commit_id, f_path=f_path,
133 133 _query=query_params)
134 134 else:
135 135 return h.route_path(
136 136 'repo_changelog',
137 137 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
138 138
139 139 c.total_cs = len(collection)
140 140 c.showing_commits = min(chunk_size, c.total_cs)
141 141 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
142 142 items_per_page=chunk_size, branch=branch_name,
143 143 url=url_generator)
144 144
145 145 c.next_page = c.pagination.next_page
146 146 c.prev_page = c.pagination.previous_page
147 147
148 148 if dynamic:
149 149 if self.request.GET.get('chunk') != 'next':
150 150 c.next_page = None
151 151 if self.request.GET.get('chunk') != 'prev':
152 152 c.prev_page = None
153 153
154 154 page_commit_ids = [x.raw_id for x in c.pagination]
155 155 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
156 156 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
157 157
158 158 def load_default_context(self):
159 159 c = self._get_local_tmpl_context(include_app_defaults=True)
160 160
161 161 c.rhodecode_repo = self.rhodecode_vcs_repo
162 162 self._register_global_c(c)
163 163 return c
164 164
165 165 def _get_preload_attrs(self):
166 166 pre_load = ['author', 'branch', 'date', 'message', 'parents',
167 167 'obsolete', 'phase', 'hidden']
168 168 return pre_load
169 169
170 170 @LoginRequired()
171 171 @HasRepoPermissionAnyDecorator(
172 172 'repository.read', 'repository.write', 'repository.admin')
173 173 @view_config(
174 174 route_name='repo_changelog', request_method='GET',
175 175 renderer='rhodecode:templates/changelog/changelog.mako')
176 176 @view_config(
177 177 route_name='repo_changelog_file', request_method='GET',
178 178 renderer='rhodecode:templates/changelog/changelog.mako')
179 179 def repo_changelog(self):
180 180 c = self.load_default_context()
181 181
182 182 commit_id = self.request.matchdict.get('commit_id')
183 183 f_path = self._get_f_path(self.request.matchdict)
184 show_hidden = str2bool(self.request.GET.get('evolve'))
184 185
185 186 chunk_size = 20
186 187
187 188 c.branch_name = branch_name = self.request.GET.get('branch') or ''
188 189 c.book_name = book_name = self.request.GET.get('bookmark') or ''
189 190 c.f_path = f_path
190 191 c.commit_id = commit_id
192 c.show_hidden = show_hidden
193
191 194 hist_limit = safe_int(self.request.GET.get('limit')) or None
192 195
193 196 p = safe_int(self.request.GET.get('page', 1), 1)
194 197
195 198 c.selected_name = branch_name or book_name
196 199 if not commit_id and branch_name:
197 200 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
198 201
199 202 c.changelog_for_path = f_path
200 203 pre_load = self._get_preload_attrs()
201 204
202 205 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
203 206
204 207 try:
205 208 if f_path:
206 209 log.debug('generating changelog for path %s', f_path)
207 210 # get the history for the file !
208 211 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
209 212
210 213 try:
211 214 collection = base_commit.get_file_history(
212 215 f_path, limit=hist_limit, pre_load=pre_load)
213 216 if collection and partial_xhr:
214 217 # for ajax call we remove first one since we're looking
215 218 # at it right now in the context of a file commit
216 219 collection.pop(0)
217 220 except (NodeDoesNotExistError, CommitError):
218 221 # this node is not present at tip!
219 222 try:
220 223 commit = self._get_commit_or_redirect(commit_id)
221 224 collection = commit.get_file_history(f_path)
222 225 except RepositoryError as e:
223 226 h.flash(safe_str(e), category='warning')
224 227 redirect_url = h.route_path(
225 228 'repo_changelog', repo_name=self.db_repo_name)
226 229 raise HTTPFound(redirect_url)
227 230 collection = list(reversed(collection))
228 231 else:
229 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 236 self._load_changelog_data(
233 237 c, collection, p, chunk_size, c.branch_name,
234 238 f_path=f_path, commit_id=commit_id)
235 239
236 240 except EmptyRepositoryError as e:
237 241 h.flash(safe_str(h.escape(e)), category='warning')
238 242 raise HTTPFound(
239 243 h.route_path('repo_summary', repo_name=self.db_repo_name))
240 244 except HTTPFound:
241 245 raise
242 246 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
243 247 log.exception(safe_str(e))
244 248 h.flash(safe_str(h.escape(e)), category='error')
245 249 raise HTTPFound(
246 250 h.route_path('repo_changelog', repo_name=self.db_repo_name))
247 251
248 252 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
249 253 # case when loading dynamic file history in file view
250 254 # loading from ajax, we don't want the first result, it's popped
251 255 # in the code above
252 256 html = render(
253 257 'rhodecode:templates/changelog/changelog_file_history.mako',
254 258 self._get_template_context(c), self.request)
255 259 return Response(html)
256 260
257 261 commit_ids = []
258 262 if not f_path:
259 263 # only load graph data when not in file history mode
260 264 commit_ids = c.pagination
261 265
262 266 c.graph_data, c.graph_commits = self._graph(
263 267 self.rhodecode_vcs_repo, commit_ids)
264 268
265 269 return self._get_template_context(c)
266 270
267 271 @LoginRequired()
268 272 @HasRepoPermissionAnyDecorator(
269 273 'repository.read', 'repository.write', 'repository.admin')
270 274 @view_config(
271 275 route_name='repo_changelog_elements', request_method=('GET', 'POST'),
272 276 renderer='rhodecode:templates/changelog/changelog_elements.mako',
273 277 xhr=True)
274 278 @view_config(
275 279 route_name='repo_changelog_elements_file', request_method=('GET', 'POST'),
276 280 renderer='rhodecode:templates/changelog/changelog_elements.mako',
277 281 xhr=True)
278 282 def repo_changelog_elements(self):
279 283 c = self.load_default_context()
280 284 commit_id = self.request.matchdict.get('commit_id')
281 285 f_path = self._get_f_path(self.request.matchdict)
286 show_hidden = str2bool(self.request.GET.get('evolve'))
287
282 288 chunk_size = 20
283 289 hist_limit = safe_int(self.request.GET.get('limit')) or None
284 290
285 291 def wrap_for_error(err):
286 292 html = '<tr>' \
287 293 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
288 294 '</tr>'.format(err)
289 295 return Response(html)
290 296
291 297 c.branch_name = branch_name = self.request.GET.get('branch') or ''
292 298 c.book_name = book_name = self.request.GET.get('bookmark') or ''
293 299 c.f_path = f_path
294 300 c.commit_id = commit_id
301 c.show_hidden = show_hidden
295 302
296 303 c.selected_name = branch_name or book_name
297 304 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
298 305 return wrap_for_error(
299 306 safe_str('Branch: {} is not valid'.format(branch_name)))
300 307
301 308 pre_load = self._get_preload_attrs()
302 309
303 310 if f_path:
304 311 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
305 312 collection = base_commit.get_file_history(
306 313 f_path, limit=hist_limit, pre_load=pre_load)
307 314 collection = list(reversed(collection))
308 315 else:
309 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 319 p = safe_int(self.request.GET.get('page', 1), 1)
313 320 try:
314 321 self._load_changelog_data(
315 322 c, collection, p, chunk_size, dynamic=True,
316 323 f_path=f_path, commit_id=commit_id)
317 324 except EmptyRepositoryError as e:
318 325 return wrap_for_error(safe_str(e))
319 326 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
320 327 log.exception('Failed to fetch commits')
321 328 return wrap_for_error(safe_str(e))
322 329
323 330 prev_data = None
324 331 next_data = None
325 332
326 333 prev_graph = json.loads(self.request.POST.get('graph') or '{}')
327 334
328 335 if self.request.GET.get('chunk') == 'prev':
329 336 next_data = prev_graph
330 337 elif self.request.GET.get('chunk') == 'next':
331 338 prev_data = prev_graph
332 339
333 340 commit_ids = []
334 341 if not f_path:
335 342 # only load graph data when not in file history mode
336 343 commit_ids = c.pagination
337 344
338 345 c.graph_data, c.graph_commits = self._graph(
339 346 self.rhodecode_vcs_repo, commit_ids,
340 347 prev_data=prev_data, next_data=next_data)
341 348
342 349 return self._get_template_context(c)
@@ -1,1590 +1,1591 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Base module for all VCS systems
23 23 """
24 24
25 25 import collections
26 26 import datetime
27 27 import itertools
28 28 import logging
29 29 import os
30 30 import time
31 31 import warnings
32 32
33 33 from zope.cachedescriptors.property import Lazy as LazyProperty
34 34
35 35 from rhodecode.lib.utils2 import safe_str, safe_unicode
36 36 from rhodecode.lib.vcs import connection
37 37 from rhodecode.lib.vcs.utils import author_name, author_email
38 38 from rhodecode.lib.vcs.conf import settings
39 39 from rhodecode.lib.vcs.exceptions import (
40 40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
41 41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
42 42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
43 43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
44 44 RepositoryError)
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 FILEMODE_DEFAULT = 0100644
51 51 FILEMODE_EXECUTABLE = 0100755
52 52
53 53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
54 54 MergeResponse = collections.namedtuple(
55 55 'MergeResponse',
56 56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
57 57
58 58
59 59 class MergeFailureReason(object):
60 60 """
61 61 Enumeration with all the reasons why the server side merge could fail.
62 62
63 63 DO NOT change the number of the reasons, as they may be stored in the
64 64 database.
65 65
66 66 Changing the name of a reason is acceptable and encouraged to deprecate old
67 67 reasons.
68 68 """
69 69
70 70 # Everything went well.
71 71 NONE = 0
72 72
73 73 # An unexpected exception was raised. Check the logs for more details.
74 74 UNKNOWN = 1
75 75
76 76 # The merge was not successful, there are conflicts.
77 77 MERGE_FAILED = 2
78 78
79 79 # The merge succeeded but we could not push it to the target repository.
80 80 PUSH_FAILED = 3
81 81
82 82 # The specified target is not a head in the target repository.
83 83 TARGET_IS_NOT_HEAD = 4
84 84
85 85 # The source repository contains more branches than the target. Pushing
86 86 # the merge will create additional branches in the target.
87 87 HG_SOURCE_HAS_MORE_BRANCHES = 5
88 88
89 89 # The target reference has multiple heads. That does not allow to correctly
90 90 # identify the target location. This could only happen for mercurial
91 91 # branches.
92 92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93 93
94 94 # The target repository is locked
95 95 TARGET_IS_LOCKED = 7
96 96
97 97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
98 98 # A involved commit could not be found.
99 99 _DEPRECATED_MISSING_COMMIT = 8
100 100
101 101 # The target repo reference is missing.
102 102 MISSING_TARGET_REF = 9
103 103
104 104 # The source repo reference is missing.
105 105 MISSING_SOURCE_REF = 10
106 106
107 107 # The merge was not successful, there are conflicts related to sub
108 108 # repositories.
109 109 SUBREPO_MERGE_FAILED = 11
110 110
111 111
112 112 class UpdateFailureReason(object):
113 113 """
114 114 Enumeration with all the reasons why the pull request update could fail.
115 115
116 116 DO NOT change the number of the reasons, as they may be stored in the
117 117 database.
118 118
119 119 Changing the name of a reason is acceptable and encouraged to deprecate old
120 120 reasons.
121 121 """
122 122
123 123 # Everything went well.
124 124 NONE = 0
125 125
126 126 # An unexpected exception was raised. Check the logs for more details.
127 127 UNKNOWN = 1
128 128
129 129 # The pull request is up to date.
130 130 NO_CHANGE = 2
131 131
132 132 # The pull request has a reference type that is not supported for update.
133 133 WRONG_REF_TYPE = 3
134 134
135 135 # Update failed because the target reference is missing.
136 136 MISSING_TARGET_REF = 4
137 137
138 138 # Update failed because the source reference is missing.
139 139 MISSING_SOURCE_REF = 5
140 140
141 141
142 142 class BaseRepository(object):
143 143 """
144 144 Base Repository for final backends
145 145
146 146 .. attribute:: DEFAULT_BRANCH_NAME
147 147
148 148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
149 149
150 150 .. attribute:: commit_ids
151 151
152 152 list of all available commit ids, in ascending order
153 153
154 154 .. attribute:: path
155 155
156 156 absolute path to the repository
157 157
158 158 .. attribute:: bookmarks
159 159
160 160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
161 161 there are no bookmarks or the backend implementation does not support
162 162 bookmarks.
163 163
164 164 .. attribute:: tags
165 165
166 166 Mapping from name to :term:`Commit ID` of the tag.
167 167
168 168 """
169 169
170 170 DEFAULT_BRANCH_NAME = None
171 171 DEFAULT_CONTACT = u"Unknown"
172 172 DEFAULT_DESCRIPTION = u"unknown"
173 173 EMPTY_COMMIT_ID = '0' * 40
174 174
175 175 path = None
176 176
177 177 def __init__(self, repo_path, config=None, create=False, **kwargs):
178 178 """
179 179 Initializes repository. Raises RepositoryError if repository could
180 180 not be find at the given ``repo_path`` or directory at ``repo_path``
181 181 exists and ``create`` is set to True.
182 182
183 183 :param repo_path: local path of the repository
184 184 :param config: repository configuration
185 185 :param create=False: if set to True, would try to create repository.
186 186 :param src_url=None: if set, should be proper url from which repository
187 187 would be cloned; requires ``create`` parameter to be set to True -
188 188 raises RepositoryError if src_url is set and create evaluates to
189 189 False
190 190 """
191 191 raise NotImplementedError
192 192
193 193 def __repr__(self):
194 194 return '<%s at %s>' % (self.__class__.__name__, self.path)
195 195
196 196 def __len__(self):
197 197 return self.count()
198 198
199 199 def __eq__(self, other):
200 200 same_instance = isinstance(other, self.__class__)
201 201 return same_instance and other.path == self.path
202 202
203 203 def __ne__(self, other):
204 204 return not self.__eq__(other)
205 205
206 206 @LazyProperty
207 207 def EMPTY_COMMIT(self):
208 208 return EmptyCommit(self.EMPTY_COMMIT_ID)
209 209
210 210 @LazyProperty
211 211 def alias(self):
212 212 for k, v in settings.BACKENDS.items():
213 213 if v.split('.')[-1] == str(self.__class__.__name__):
214 214 return k
215 215
216 216 @LazyProperty
217 217 def name(self):
218 218 return safe_unicode(os.path.basename(self.path))
219 219
220 220 @LazyProperty
221 221 def description(self):
222 222 raise NotImplementedError
223 223
224 224 def refs(self):
225 225 """
226 226 returns a `dict` with branches, bookmarks, tags, and closed_branches
227 227 for this repository
228 228 """
229 229 return dict(
230 230 branches=self.branches,
231 231 branches_closed=self.branches_closed,
232 232 tags=self.tags,
233 233 bookmarks=self.bookmarks
234 234 )
235 235
236 236 @LazyProperty
237 237 def branches(self):
238 238 """
239 239 A `dict` which maps branch names to commit ids.
240 240 """
241 241 raise NotImplementedError
242 242
243 243 @LazyProperty
244 244 def tags(self):
245 245 """
246 246 A `dict` which maps tags names to commit ids.
247 247 """
248 248 raise NotImplementedError
249 249
250 250 @LazyProperty
251 251 def size(self):
252 252 """
253 253 Returns combined size in bytes for all repository files
254 254 """
255 255 tip = self.get_commit()
256 256 return tip.size
257 257
258 258 def size_at_commit(self, commit_id):
259 259 commit = self.get_commit(commit_id)
260 260 return commit.size
261 261
262 262 def is_empty(self):
263 263 return not bool(self.commit_ids)
264 264
265 265 @staticmethod
266 266 def check_url(url, config):
267 267 """
268 268 Function will check given url and try to verify if it's a valid
269 269 link.
270 270 """
271 271 raise NotImplementedError
272 272
273 273 @staticmethod
274 274 def is_valid_repository(path):
275 275 """
276 276 Check if given `path` contains a valid repository of this backend
277 277 """
278 278 raise NotImplementedError
279 279
280 280 # ==========================================================================
281 281 # COMMITS
282 282 # ==========================================================================
283 283
284 284 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
285 285 """
286 286 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
287 287 are both None, most recent commit is returned.
288 288
289 289 :param pre_load: Optional. List of commit attributes to load.
290 290
291 291 :raises ``EmptyRepositoryError``: if there are no commits
292 292 """
293 293 raise NotImplementedError
294 294
295 295 def __iter__(self):
296 296 for commit_id in self.commit_ids:
297 297 yield self.get_commit(commit_id=commit_id)
298 298
299 299 def get_commits(
300 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 303 Returns iterator of `BaseCommit` objects from start to end
304 304 not inclusive. This should behave just like a list, ie. end is not
305 305 inclusive.
306 306
307 307 :param start_id: None or str, must be a valid commit id
308 308 :param end_id: None or str, must be a valid commit id
309 309 :param start_date:
310 310 :param end_date:
311 311 :param branch_name:
312 :param show_hidden:
312 313 :param pre_load:
313 314 """
314 315 raise NotImplementedError
315 316
316 317 def __getitem__(self, key):
317 318 """
318 319 Allows index based access to the commit objects of this repository.
319 320 """
320 321 pre_load = ["author", "branch", "date", "message", "parents"]
321 322 if isinstance(key, slice):
322 323 return self._get_range(key, pre_load)
323 324 return self.get_commit(commit_idx=key, pre_load=pre_load)
324 325
325 326 def _get_range(self, slice_obj, pre_load):
326 327 for commit_id in self.commit_ids.__getitem__(slice_obj):
327 328 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
328 329
329 330 def count(self):
330 331 return len(self.commit_ids)
331 332
332 333 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
333 334 """
334 335 Creates and returns a tag for the given ``commit_id``.
335 336
336 337 :param name: name for new tag
337 338 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
338 339 :param commit_id: commit id for which new tag would be created
339 340 :param message: message of the tag's commit
340 341 :param date: date of tag's commit
341 342
342 343 :raises TagAlreadyExistError: if tag with same name already exists
343 344 """
344 345 raise NotImplementedError
345 346
346 347 def remove_tag(self, name, user, message=None, date=None):
347 348 """
348 349 Removes tag with the given ``name``.
349 350
350 351 :param name: name of the tag to be removed
351 352 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
352 353 :param message: message of the tag's removal commit
353 354 :param date: date of tag's removal commit
354 355
355 356 :raises TagDoesNotExistError: if tag with given name does not exists
356 357 """
357 358 raise NotImplementedError
358 359
359 360 def get_diff(
360 361 self, commit1, commit2, path=None, ignore_whitespace=False,
361 362 context=3, path1=None):
362 363 """
363 364 Returns (git like) *diff*, as plain text. Shows changes introduced by
364 365 `commit2` since `commit1`.
365 366
366 367 :param commit1: Entry point from which diff is shown. Can be
367 368 ``self.EMPTY_COMMIT`` - in this case, patch showing all
368 369 the changes since empty state of the repository until `commit2`
369 370 :param commit2: Until which commit changes should be shown.
370 371 :param path: Can be set to a path of a file to create a diff of that
371 372 file. If `path1` is also set, this value is only associated to
372 373 `commit2`.
373 374 :param ignore_whitespace: If set to ``True``, would not show whitespace
374 375 changes. Defaults to ``False``.
375 376 :param context: How many lines before/after changed lines should be
376 377 shown. Defaults to ``3``.
377 378 :param path1: Can be set to a path to associate with `commit1`. This
378 379 parameter works only for backends which support diff generation for
379 380 different paths. Other backends will raise a `ValueError` if `path1`
380 381 is set and has a different value than `path`.
381 382 :param file_path: filter this diff by given path pattern
382 383 """
383 384 raise NotImplementedError
384 385
385 386 def strip(self, commit_id, branch=None):
386 387 """
387 388 Strip given commit_id from the repository
388 389 """
389 390 raise NotImplementedError
390 391
391 392 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
392 393 """
393 394 Return a latest common ancestor commit if one exists for this repo
394 395 `commit_id1` vs `commit_id2` from `repo2`.
395 396
396 397 :param commit_id1: Commit it from this repository to use as a
397 398 target for the comparison.
398 399 :param commit_id2: Source commit id to use for comparison.
399 400 :param repo2: Source repository to use for comparison.
400 401 """
401 402 raise NotImplementedError
402 403
403 404 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
404 405 """
405 406 Compare this repository's revision `commit_id1` with `commit_id2`.
406 407
407 408 Returns a tuple(commits, ancestor) that would be merged from
408 409 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
409 410 will be returned as ancestor.
410 411
411 412 :param commit_id1: Commit it from this repository to use as a
412 413 target for the comparison.
413 414 :param commit_id2: Source commit id to use for comparison.
414 415 :param repo2: Source repository to use for comparison.
415 416 :param merge: If set to ``True`` will do a merge compare which also
416 417 returns the common ancestor.
417 418 :param pre_load: Optional. List of commit attributes to load.
418 419 """
419 420 raise NotImplementedError
420 421
421 422 def merge(self, target_ref, source_repo, source_ref, workspace_id,
422 423 user_name='', user_email='', message='', dry_run=False,
423 424 use_rebase=False, close_branch=False):
424 425 """
425 426 Merge the revisions specified in `source_ref` from `source_repo`
426 427 onto the `target_ref` of this repository.
427 428
428 429 `source_ref` and `target_ref` are named tupls with the following
429 430 fields `type`, `name` and `commit_id`.
430 431
431 432 Returns a MergeResponse named tuple with the following fields
432 433 'possible', 'executed', 'source_commit', 'target_commit',
433 434 'merge_commit'.
434 435
435 436 :param target_ref: `target_ref` points to the commit on top of which
436 437 the `source_ref` should be merged.
437 438 :param source_repo: The repository that contains the commits to be
438 439 merged.
439 440 :param source_ref: `source_ref` points to the topmost commit from
440 441 the `source_repo` which should be merged.
441 442 :param workspace_id: `workspace_id` unique identifier.
442 443 :param user_name: Merge commit `user_name`.
443 444 :param user_email: Merge commit `user_email`.
444 445 :param message: Merge commit `message`.
445 446 :param dry_run: If `True` the merge will not take place.
446 447 :param use_rebase: If `True` commits from the source will be rebased
447 448 on top of the target instead of being merged.
448 449 :param close_branch: If `True` branch will be close before merging it
449 450 """
450 451 if dry_run:
451 452 message = message or 'dry_run_merge_message'
452 453 user_email = user_email or 'dry-run-merge@rhodecode.com'
453 454 user_name = user_name or 'Dry-Run User'
454 455 else:
455 456 if not user_name:
456 457 raise ValueError('user_name cannot be empty')
457 458 if not user_email:
458 459 raise ValueError('user_email cannot be empty')
459 460 if not message:
460 461 raise ValueError('message cannot be empty')
461 462
462 463 shadow_repository_path = self._maybe_prepare_merge_workspace(
463 464 workspace_id, target_ref)
464 465
465 466 try:
466 467 return self._merge_repo(
467 468 shadow_repository_path, target_ref, source_repo,
468 469 source_ref, message, user_name, user_email, dry_run=dry_run,
469 470 use_rebase=use_rebase, close_branch=close_branch)
470 471 except RepositoryError:
471 472 log.exception(
472 473 'Unexpected failure when running merge, dry-run=%s',
473 474 dry_run)
474 475 return MergeResponse(
475 476 False, False, None, MergeFailureReason.UNKNOWN)
476 477
477 478 def _merge_repo(self, shadow_repository_path, target_ref,
478 479 source_repo, source_ref, merge_message,
479 480 merger_name, merger_email, dry_run=False,
480 481 use_rebase=False, close_branch=False):
481 482 """Internal implementation of merge."""
482 483 raise NotImplementedError
483 484
484 485 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
485 486 """
486 487 Create the merge workspace.
487 488
488 489 :param workspace_id: `workspace_id` unique identifier.
489 490 """
490 491 raise NotImplementedError
491 492
492 493 def cleanup_merge_workspace(self, workspace_id):
493 494 """
494 495 Remove merge workspace.
495 496
496 497 This function MUST not fail in case there is no workspace associated to
497 498 the given `workspace_id`.
498 499
499 500 :param workspace_id: `workspace_id` unique identifier.
500 501 """
501 502 raise NotImplementedError
502 503
503 504 # ========== #
504 505 # COMMIT API #
505 506 # ========== #
506 507
507 508 @LazyProperty
508 509 def in_memory_commit(self):
509 510 """
510 511 Returns :class:`InMemoryCommit` object for this repository.
511 512 """
512 513 raise NotImplementedError
513 514
514 515 # ======================== #
515 516 # UTILITIES FOR SUBCLASSES #
516 517 # ======================== #
517 518
518 519 def _validate_diff_commits(self, commit1, commit2):
519 520 """
520 521 Validates that the given commits are related to this repository.
521 522
522 523 Intended as a utility for sub classes to have a consistent validation
523 524 of input parameters in methods like :meth:`get_diff`.
524 525 """
525 526 self._validate_commit(commit1)
526 527 self._validate_commit(commit2)
527 528 if (isinstance(commit1, EmptyCommit) and
528 529 isinstance(commit2, EmptyCommit)):
529 530 raise ValueError("Cannot compare two empty commits")
530 531
531 532 def _validate_commit(self, commit):
532 533 if not isinstance(commit, BaseCommit):
533 534 raise TypeError(
534 535 "%s is not of type BaseCommit" % repr(commit))
535 536 if commit.repository != self and not isinstance(commit, EmptyCommit):
536 537 raise ValueError(
537 538 "Commit %s must be a valid commit from this repository %s, "
538 539 "related to this repository instead %s." %
539 540 (commit, self, commit.repository))
540 541
541 542 def _validate_commit_id(self, commit_id):
542 543 if not isinstance(commit_id, basestring):
543 544 raise TypeError("commit_id must be a string value")
544 545
545 546 def _validate_commit_idx(self, commit_idx):
546 547 if not isinstance(commit_idx, (int, long)):
547 548 raise TypeError("commit_idx must be a numeric value")
548 549
549 550 def _validate_branch_name(self, branch_name):
550 551 if branch_name and branch_name not in self.branches_all:
551 552 msg = ("Branch %s not found in %s" % (branch_name, self))
552 553 raise BranchDoesNotExistError(msg)
553 554
554 555 #
555 556 # Supporting deprecated API parts
556 557 # TODO: johbo: consider to move this into a mixin
557 558 #
558 559
559 560 @property
560 561 def EMPTY_CHANGESET(self):
561 562 warnings.warn(
562 563 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
563 564 return self.EMPTY_COMMIT_ID
564 565
565 566 @property
566 567 def revisions(self):
567 568 warnings.warn("Use commits attribute instead", DeprecationWarning)
568 569 return self.commit_ids
569 570
570 571 @revisions.setter
571 572 def revisions(self, value):
572 573 warnings.warn("Use commits attribute instead", DeprecationWarning)
573 574 self.commit_ids = value
574 575
575 576 def get_changeset(self, revision=None, pre_load=None):
576 577 warnings.warn("Use get_commit instead", DeprecationWarning)
577 578 commit_id = None
578 579 commit_idx = None
579 580 if isinstance(revision, basestring):
580 581 commit_id = revision
581 582 else:
582 583 commit_idx = revision
583 584 return self.get_commit(
584 585 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
585 586
586 587 def get_changesets(
587 588 self, start=None, end=None, start_date=None, end_date=None,
588 589 branch_name=None, pre_load=None):
589 590 warnings.warn("Use get_commits instead", DeprecationWarning)
590 591 start_id = self._revision_to_commit(start)
591 592 end_id = self._revision_to_commit(end)
592 593 return self.get_commits(
593 594 start_id=start_id, end_id=end_id, start_date=start_date,
594 595 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
595 596
596 597 def _revision_to_commit(self, revision):
597 598 """
598 599 Translates a revision to a commit_id
599 600
600 601 Helps to support the old changeset based API which allows to use
601 602 commit ids and commit indices interchangeable.
602 603 """
603 604 if revision is None:
604 605 return revision
605 606
606 607 if isinstance(revision, basestring):
607 608 commit_id = revision
608 609 else:
609 610 commit_id = self.commit_ids[revision]
610 611 return commit_id
611 612
612 613 @property
613 614 def in_memory_changeset(self):
614 615 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
615 616 return self.in_memory_commit
616 617
617 618
618 619 class BaseCommit(object):
619 620 """
620 621 Each backend should implement it's commit representation.
621 622
622 623 **Attributes**
623 624
624 625 ``repository``
625 626 repository object within which commit exists
626 627
627 628 ``id``
628 629 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
629 630 just ``tip``.
630 631
631 632 ``raw_id``
632 633 raw commit representation (i.e. full 40 length sha for git
633 634 backend)
634 635
635 636 ``short_id``
636 637 shortened (if apply) version of ``raw_id``; it would be simple
637 638 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
638 639 as ``raw_id`` for subversion
639 640
640 641 ``idx``
641 642 commit index
642 643
643 644 ``files``
644 645 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
645 646
646 647 ``dirs``
647 648 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
648 649
649 650 ``nodes``
650 651 combined list of ``Node`` objects
651 652
652 653 ``author``
653 654 author of the commit, as unicode
654 655
655 656 ``message``
656 657 message of the commit, as unicode
657 658
658 659 ``parents``
659 660 list of parent commits
660 661
661 662 """
662 663
663 664 branch = None
664 665 """
665 666 Depending on the backend this should be set to the branch name of the
666 667 commit. Backends not supporting branches on commits should leave this
667 668 value as ``None``.
668 669 """
669 670
670 671 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
671 672 """
672 673 This template is used to generate a default prefix for repository archives
673 674 if no prefix has been specified.
674 675 """
675 676
676 677 def __str__(self):
677 678 return '<%s at %s:%s>' % (
678 679 self.__class__.__name__, self.idx, self.short_id)
679 680
680 681 def __repr__(self):
681 682 return self.__str__()
682 683
683 684 def __unicode__(self):
684 685 return u'%s:%s' % (self.idx, self.short_id)
685 686
686 687 def __eq__(self, other):
687 688 same_instance = isinstance(other, self.__class__)
688 689 return same_instance and self.raw_id == other.raw_id
689 690
690 691 def __json__(self):
691 692 parents = []
692 693 try:
693 694 for parent in self.parents:
694 695 parents.append({'raw_id': parent.raw_id})
695 696 except NotImplementedError:
696 697 # empty commit doesn't have parents implemented
697 698 pass
698 699
699 700 return {
700 701 'short_id': self.short_id,
701 702 'raw_id': self.raw_id,
702 703 'revision': self.idx,
703 704 'message': self.message,
704 705 'date': self.date,
705 706 'author': self.author,
706 707 'parents': parents,
707 708 'branch': self.branch
708 709 }
709 710
710 711 @LazyProperty
711 712 def last(self):
712 713 """
713 714 ``True`` if this is last commit in repository, ``False``
714 715 otherwise; trying to access this attribute while there is no
715 716 commits would raise `EmptyRepositoryError`
716 717 """
717 718 if self.repository is None:
718 719 raise CommitError("Cannot check if it's most recent commit")
719 720 return self.raw_id == self.repository.commit_ids[-1]
720 721
721 722 @LazyProperty
722 723 def parents(self):
723 724 """
724 725 Returns list of parent commits.
725 726 """
726 727 raise NotImplementedError
727 728
728 729 @property
729 730 def merge(self):
730 731 """
731 732 Returns boolean if commit is a merge.
732 733 """
733 734 return len(self.parents) > 1
734 735
735 736 @LazyProperty
736 737 def children(self):
737 738 """
738 739 Returns list of child commits.
739 740 """
740 741 raise NotImplementedError
741 742
742 743 @LazyProperty
743 744 def id(self):
744 745 """
745 746 Returns string identifying this commit.
746 747 """
747 748 raise NotImplementedError
748 749
749 750 @LazyProperty
750 751 def raw_id(self):
751 752 """
752 753 Returns raw string identifying this commit.
753 754 """
754 755 raise NotImplementedError
755 756
756 757 @LazyProperty
757 758 def short_id(self):
758 759 """
759 760 Returns shortened version of ``raw_id`` attribute, as string,
760 761 identifying this commit, useful for presentation to users.
761 762 """
762 763 raise NotImplementedError
763 764
764 765 @LazyProperty
765 766 def idx(self):
766 767 """
767 768 Returns integer identifying this commit.
768 769 """
769 770 raise NotImplementedError
770 771
771 772 @LazyProperty
772 773 def committer(self):
773 774 """
774 775 Returns committer for this commit
775 776 """
776 777 raise NotImplementedError
777 778
778 779 @LazyProperty
779 780 def committer_name(self):
780 781 """
781 782 Returns committer name for this commit
782 783 """
783 784
784 785 return author_name(self.committer)
785 786
786 787 @LazyProperty
787 788 def committer_email(self):
788 789 """
789 790 Returns committer email address for this commit
790 791 """
791 792
792 793 return author_email(self.committer)
793 794
794 795 @LazyProperty
795 796 def author(self):
796 797 """
797 798 Returns author for this commit
798 799 """
799 800
800 801 raise NotImplementedError
801 802
802 803 @LazyProperty
803 804 def author_name(self):
804 805 """
805 806 Returns author name for this commit
806 807 """
807 808
808 809 return author_name(self.author)
809 810
810 811 @LazyProperty
811 812 def author_email(self):
812 813 """
813 814 Returns author email address for this commit
814 815 """
815 816
816 817 return author_email(self.author)
817 818
818 819 def get_file_mode(self, path):
819 820 """
820 821 Returns stat mode of the file at `path`.
821 822 """
822 823 raise NotImplementedError
823 824
824 825 def is_link(self, path):
825 826 """
826 827 Returns ``True`` if given `path` is a symlink
827 828 """
828 829 raise NotImplementedError
829 830
830 831 def get_file_content(self, path):
831 832 """
832 833 Returns content of the file at the given `path`.
833 834 """
834 835 raise NotImplementedError
835 836
836 837 def get_file_size(self, path):
837 838 """
838 839 Returns size of the file at the given `path`.
839 840 """
840 841 raise NotImplementedError
841 842
842 843 def get_file_commit(self, path, pre_load=None):
843 844 """
844 845 Returns last commit of the file at the given `path`.
845 846
846 847 :param pre_load: Optional. List of commit attributes to load.
847 848 """
848 849 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
849 850 if not commits:
850 851 raise RepositoryError(
851 852 'Failed to fetch history for path {}. '
852 853 'Please check if such path exists in your repository'.format(
853 854 path))
854 855 return commits[0]
855 856
856 857 def get_file_history(self, path, limit=None, pre_load=None):
857 858 """
858 859 Returns history of file as reversed list of :class:`BaseCommit`
859 860 objects for which file at given `path` has been modified.
860 861
861 862 :param limit: Optional. Allows to limit the size of the returned
862 863 history. This is intended as a hint to the underlying backend, so
863 864 that it can apply optimizations depending on the limit.
864 865 :param pre_load: Optional. List of commit attributes to load.
865 866 """
866 867 raise NotImplementedError
867 868
868 869 def get_file_annotate(self, path, pre_load=None):
869 870 """
870 871 Returns a generator of four element tuples with
871 872 lineno, sha, commit lazy loader and line
872 873
873 874 :param pre_load: Optional. List of commit attributes to load.
874 875 """
875 876 raise NotImplementedError
876 877
877 878 def get_nodes(self, path):
878 879 """
879 880 Returns combined ``DirNode`` and ``FileNode`` objects list representing
880 881 state of commit at the given ``path``.
881 882
882 883 :raises ``CommitError``: if node at the given ``path`` is not
883 884 instance of ``DirNode``
884 885 """
885 886 raise NotImplementedError
886 887
887 888 def get_node(self, path):
888 889 """
889 890 Returns ``Node`` object from the given ``path``.
890 891
891 892 :raises ``NodeDoesNotExistError``: if there is no node at the given
892 893 ``path``
893 894 """
894 895 raise NotImplementedError
895 896
896 897 def get_largefile_node(self, path):
897 898 """
898 899 Returns the path to largefile from Mercurial/Git-lfs storage.
899 900 or None if it's not a largefile node
900 901 """
901 902 return None
902 903
903 904 def archive_repo(self, file_path, kind='tgz', subrepos=None,
904 905 prefix=None, write_metadata=False, mtime=None):
905 906 """
906 907 Creates an archive containing the contents of the repository.
907 908
908 909 :param file_path: path to the file which to create the archive.
909 910 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
910 911 :param prefix: name of root directory in archive.
911 912 Default is repository name and commit's short_id joined with dash:
912 913 ``"{repo_name}-{short_id}"``.
913 914 :param write_metadata: write a metadata file into archive.
914 915 :param mtime: custom modification time for archive creation, defaults
915 916 to time.time() if not given.
916 917
917 918 :raise VCSError: If prefix has a problem.
918 919 """
919 920 allowed_kinds = settings.ARCHIVE_SPECS.keys()
920 921 if kind not in allowed_kinds:
921 922 raise ImproperArchiveTypeError(
922 923 'Archive kind (%s) not supported use one of %s' %
923 924 (kind, allowed_kinds))
924 925
925 926 prefix = self._validate_archive_prefix(prefix)
926 927
927 928 mtime = mtime or time.mktime(self.date.timetuple())
928 929
929 930 file_info = []
930 931 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
931 932 for _r, _d, files in cur_rev.walk('/'):
932 933 for f in files:
933 934 f_path = os.path.join(prefix, f.path)
934 935 file_info.append(
935 936 (f_path, f.mode, f.is_link(), f.raw_bytes))
936 937
937 938 if write_metadata:
938 939 metadata = [
939 940 ('repo_name', self.repository.name),
940 941 ('rev', self.raw_id),
941 942 ('create_time', mtime),
942 943 ('branch', self.branch),
943 944 ('tags', ','.join(self.tags)),
944 945 ]
945 946 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
946 947 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
947 948
948 949 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
949 950
950 951 def _validate_archive_prefix(self, prefix):
951 952 if prefix is None:
952 953 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
953 954 repo_name=safe_str(self.repository.name),
954 955 short_id=self.short_id)
955 956 elif not isinstance(prefix, str):
956 957 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
957 958 elif prefix.startswith('/'):
958 959 raise VCSError("Prefix cannot start with leading slash")
959 960 elif prefix.strip() == '':
960 961 raise VCSError("Prefix cannot be empty")
961 962 return prefix
962 963
963 964 @LazyProperty
964 965 def root(self):
965 966 """
966 967 Returns ``RootNode`` object for this commit.
967 968 """
968 969 return self.get_node('')
969 970
970 971 def next(self, branch=None):
971 972 """
972 973 Returns next commit from current, if branch is gives it will return
973 974 next commit belonging to this branch
974 975
975 976 :param branch: show commits within the given named branch
976 977 """
977 978 indexes = xrange(self.idx + 1, self.repository.count())
978 979 return self._find_next(indexes, branch)
979 980
980 981 def prev(self, branch=None):
981 982 """
982 983 Returns previous commit from current, if branch is gives it will
983 984 return previous commit belonging to this branch
984 985
985 986 :param branch: show commit within the given named branch
986 987 """
987 988 indexes = xrange(self.idx - 1, -1, -1)
988 989 return self._find_next(indexes, branch)
989 990
990 991 def _find_next(self, indexes, branch=None):
991 992 if branch and self.branch != branch:
992 993 raise VCSError('Branch option used on commit not belonging '
993 994 'to that branch')
994 995
995 996 for next_idx in indexes:
996 997 commit = self.repository.get_commit(commit_idx=next_idx)
997 998 if branch and branch != commit.branch:
998 999 continue
999 1000 return commit
1000 1001 raise CommitDoesNotExistError
1001 1002
1002 1003 def diff(self, ignore_whitespace=True, context=3):
1003 1004 """
1004 1005 Returns a `Diff` object representing the change made by this commit.
1005 1006 """
1006 1007 parent = (
1007 1008 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
1008 1009 diff = self.repository.get_diff(
1009 1010 parent, self,
1010 1011 ignore_whitespace=ignore_whitespace,
1011 1012 context=context)
1012 1013 return diff
1013 1014
1014 1015 @LazyProperty
1015 1016 def added(self):
1016 1017 """
1017 1018 Returns list of added ``FileNode`` objects.
1018 1019 """
1019 1020 raise NotImplementedError
1020 1021
1021 1022 @LazyProperty
1022 1023 def changed(self):
1023 1024 """
1024 1025 Returns list of modified ``FileNode`` objects.
1025 1026 """
1026 1027 raise NotImplementedError
1027 1028
1028 1029 @LazyProperty
1029 1030 def removed(self):
1030 1031 """
1031 1032 Returns list of removed ``FileNode`` objects.
1032 1033 """
1033 1034 raise NotImplementedError
1034 1035
1035 1036 @LazyProperty
1036 1037 def size(self):
1037 1038 """
1038 1039 Returns total number of bytes from contents of all filenodes.
1039 1040 """
1040 1041 return sum((node.size for node in self.get_filenodes_generator()))
1041 1042
1042 1043 def walk(self, topurl=''):
1043 1044 """
1044 1045 Similar to os.walk method. Insted of filesystem it walks through
1045 1046 commit starting at given ``topurl``. Returns generator of tuples
1046 1047 (topnode, dirnodes, filenodes).
1047 1048 """
1048 1049 topnode = self.get_node(topurl)
1049 1050 if not topnode.is_dir():
1050 1051 return
1051 1052 yield (topnode, topnode.dirs, topnode.files)
1052 1053 for dirnode in topnode.dirs:
1053 1054 for tup in self.walk(dirnode.path):
1054 1055 yield tup
1055 1056
1056 1057 def get_filenodes_generator(self):
1057 1058 """
1058 1059 Returns generator that yields *all* file nodes.
1059 1060 """
1060 1061 for topnode, dirs, files in self.walk():
1061 1062 for node in files:
1062 1063 yield node
1063 1064
1064 1065 #
1065 1066 # Utilities for sub classes to support consistent behavior
1066 1067 #
1067 1068
1068 1069 def no_node_at_path(self, path):
1069 1070 return NodeDoesNotExistError(
1070 1071 u"There is no file nor directory at the given path: "
1071 1072 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1072 1073
1073 1074 def _fix_path(self, path):
1074 1075 """
1075 1076 Paths are stored without trailing slash so we need to get rid off it if
1076 1077 needed.
1077 1078 """
1078 1079 return path.rstrip('/')
1079 1080
1080 1081 #
1081 1082 # Deprecated API based on changesets
1082 1083 #
1083 1084
1084 1085 @property
1085 1086 def revision(self):
1086 1087 warnings.warn("Use idx instead", DeprecationWarning)
1087 1088 return self.idx
1088 1089
1089 1090 @revision.setter
1090 1091 def revision(self, value):
1091 1092 warnings.warn("Use idx instead", DeprecationWarning)
1092 1093 self.idx = value
1093 1094
1094 1095 def get_file_changeset(self, path):
1095 1096 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1096 1097 return self.get_file_commit(path)
1097 1098
1098 1099
1099 1100 class BaseChangesetClass(type):
1100 1101
1101 1102 def __instancecheck__(self, instance):
1102 1103 return isinstance(instance, BaseCommit)
1103 1104
1104 1105
1105 1106 class BaseChangeset(BaseCommit):
1106 1107
1107 1108 __metaclass__ = BaseChangesetClass
1108 1109
1109 1110 def __new__(cls, *args, **kwargs):
1110 1111 warnings.warn(
1111 1112 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1112 1113 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1113 1114
1114 1115
1115 1116 class BaseInMemoryCommit(object):
1116 1117 """
1117 1118 Represents differences between repository's state (most recent head) and
1118 1119 changes made *in place*.
1119 1120
1120 1121 **Attributes**
1121 1122
1122 1123 ``repository``
1123 1124 repository object for this in-memory-commit
1124 1125
1125 1126 ``added``
1126 1127 list of ``FileNode`` objects marked as *added*
1127 1128
1128 1129 ``changed``
1129 1130 list of ``FileNode`` objects marked as *changed*
1130 1131
1131 1132 ``removed``
1132 1133 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1133 1134 *removed*
1134 1135
1135 1136 ``parents``
1136 1137 list of :class:`BaseCommit` instances representing parents of
1137 1138 in-memory commit. Should always be 2-element sequence.
1138 1139
1139 1140 """
1140 1141
1141 1142 def __init__(self, repository):
1142 1143 self.repository = repository
1143 1144 self.added = []
1144 1145 self.changed = []
1145 1146 self.removed = []
1146 1147 self.parents = []
1147 1148
1148 1149 def add(self, *filenodes):
1149 1150 """
1150 1151 Marks given ``FileNode`` objects as *to be committed*.
1151 1152
1152 1153 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1153 1154 latest commit
1154 1155 :raises ``NodeAlreadyAddedError``: if node with same path is already
1155 1156 marked as *added*
1156 1157 """
1157 1158 # Check if not already marked as *added* first
1158 1159 for node in filenodes:
1159 1160 if node.path in (n.path for n in self.added):
1160 1161 raise NodeAlreadyAddedError(
1161 1162 "Such FileNode %s is already marked for addition"
1162 1163 % node.path)
1163 1164 for node in filenodes:
1164 1165 self.added.append(node)
1165 1166
1166 1167 def change(self, *filenodes):
1167 1168 """
1168 1169 Marks given ``FileNode`` objects to be *changed* in next commit.
1169 1170
1170 1171 :raises ``EmptyRepositoryError``: if there are no commits yet
1171 1172 :raises ``NodeAlreadyExistsError``: if node with same path is already
1172 1173 marked to be *changed*
1173 1174 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1174 1175 marked to be *removed*
1175 1176 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1176 1177 commit
1177 1178 :raises ``NodeNotChangedError``: if node hasn't really be changed
1178 1179 """
1179 1180 for node in filenodes:
1180 1181 if node.path in (n.path for n in self.removed):
1181 1182 raise NodeAlreadyRemovedError(
1182 1183 "Node at %s is already marked as removed" % node.path)
1183 1184 try:
1184 1185 self.repository.get_commit()
1185 1186 except EmptyRepositoryError:
1186 1187 raise EmptyRepositoryError(
1187 1188 "Nothing to change - try to *add* new nodes rather than "
1188 1189 "changing them")
1189 1190 for node in filenodes:
1190 1191 if node.path in (n.path for n in self.changed):
1191 1192 raise NodeAlreadyChangedError(
1192 1193 "Node at '%s' is already marked as changed" % node.path)
1193 1194 self.changed.append(node)
1194 1195
1195 1196 def remove(self, *filenodes):
1196 1197 """
1197 1198 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1198 1199 *removed* in next commit.
1199 1200
1200 1201 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1201 1202 be *removed*
1202 1203 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1203 1204 be *changed*
1204 1205 """
1205 1206 for node in filenodes:
1206 1207 if node.path in (n.path for n in self.removed):
1207 1208 raise NodeAlreadyRemovedError(
1208 1209 "Node is already marked to for removal at %s" % node.path)
1209 1210 if node.path in (n.path for n in self.changed):
1210 1211 raise NodeAlreadyChangedError(
1211 1212 "Node is already marked to be changed at %s" % node.path)
1212 1213 # We only mark node as *removed* - real removal is done by
1213 1214 # commit method
1214 1215 self.removed.append(node)
1215 1216
1216 1217 def reset(self):
1217 1218 """
1218 1219 Resets this instance to initial state (cleans ``added``, ``changed``
1219 1220 and ``removed`` lists).
1220 1221 """
1221 1222 self.added = []
1222 1223 self.changed = []
1223 1224 self.removed = []
1224 1225 self.parents = []
1225 1226
1226 1227 def get_ipaths(self):
1227 1228 """
1228 1229 Returns generator of paths from nodes marked as added, changed or
1229 1230 removed.
1230 1231 """
1231 1232 for node in itertools.chain(self.added, self.changed, self.removed):
1232 1233 yield node.path
1233 1234
1234 1235 def get_paths(self):
1235 1236 """
1236 1237 Returns list of paths from nodes marked as added, changed or removed.
1237 1238 """
1238 1239 return list(self.get_ipaths())
1239 1240
1240 1241 def check_integrity(self, parents=None):
1241 1242 """
1242 1243 Checks in-memory commit's integrity. Also, sets parents if not
1243 1244 already set.
1244 1245
1245 1246 :raises CommitError: if any error occurs (i.e.
1246 1247 ``NodeDoesNotExistError``).
1247 1248 """
1248 1249 if not self.parents:
1249 1250 parents = parents or []
1250 1251 if len(parents) == 0:
1251 1252 try:
1252 1253 parents = [self.repository.get_commit(), None]
1253 1254 except EmptyRepositoryError:
1254 1255 parents = [None, None]
1255 1256 elif len(parents) == 1:
1256 1257 parents += [None]
1257 1258 self.parents = parents
1258 1259
1259 1260 # Local parents, only if not None
1260 1261 parents = [p for p in self.parents if p]
1261 1262
1262 1263 # Check nodes marked as added
1263 1264 for p in parents:
1264 1265 for node in self.added:
1265 1266 try:
1266 1267 p.get_node(node.path)
1267 1268 except NodeDoesNotExistError:
1268 1269 pass
1269 1270 else:
1270 1271 raise NodeAlreadyExistsError(
1271 1272 "Node `%s` already exists at %s" % (node.path, p))
1272 1273
1273 1274 # Check nodes marked as changed
1274 1275 missing = set(self.changed)
1275 1276 not_changed = set(self.changed)
1276 1277 if self.changed and not parents:
1277 1278 raise NodeDoesNotExistError(str(self.changed[0].path))
1278 1279 for p in parents:
1279 1280 for node in self.changed:
1280 1281 try:
1281 1282 old = p.get_node(node.path)
1282 1283 missing.remove(node)
1283 1284 # if content actually changed, remove node from not_changed
1284 1285 if old.content != node.content:
1285 1286 not_changed.remove(node)
1286 1287 except NodeDoesNotExistError:
1287 1288 pass
1288 1289 if self.changed and missing:
1289 1290 raise NodeDoesNotExistError(
1290 1291 "Node `%s` marked as modified but missing in parents: %s"
1291 1292 % (node.path, parents))
1292 1293
1293 1294 if self.changed and not_changed:
1294 1295 raise NodeNotChangedError(
1295 1296 "Node `%s` wasn't actually changed (parents: %s)"
1296 1297 % (not_changed.pop().path, parents))
1297 1298
1298 1299 # Check nodes marked as removed
1299 1300 if self.removed and not parents:
1300 1301 raise NodeDoesNotExistError(
1301 1302 "Cannot remove node at %s as there "
1302 1303 "were no parents specified" % self.removed[0].path)
1303 1304 really_removed = set()
1304 1305 for p in parents:
1305 1306 for node in self.removed:
1306 1307 try:
1307 1308 p.get_node(node.path)
1308 1309 really_removed.add(node)
1309 1310 except CommitError:
1310 1311 pass
1311 1312 not_removed = set(self.removed) - really_removed
1312 1313 if not_removed:
1313 1314 # TODO: johbo: This code branch does not seem to be covered
1314 1315 raise NodeDoesNotExistError(
1315 1316 "Cannot remove node at %s from "
1316 1317 "following parents: %s" % (not_removed, parents))
1317 1318
1318 1319 def commit(
1319 1320 self, message, author, parents=None, branch=None, date=None,
1320 1321 **kwargs):
1321 1322 """
1322 1323 Performs in-memory commit (doesn't check workdir in any way) and
1323 1324 returns newly created :class:`BaseCommit`. Updates repository's
1324 1325 attribute `commits`.
1325 1326
1326 1327 .. note::
1327 1328
1328 1329 While overriding this method each backend's should call
1329 1330 ``self.check_integrity(parents)`` in the first place.
1330 1331
1331 1332 :param message: message of the commit
1332 1333 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1333 1334 :param parents: single parent or sequence of parents from which commit
1334 1335 would be derived
1335 1336 :param date: ``datetime.datetime`` instance. Defaults to
1336 1337 ``datetime.datetime.now()``.
1337 1338 :param branch: branch name, as string. If none given, default backend's
1338 1339 branch would be used.
1339 1340
1340 1341 :raises ``CommitError``: if any error occurs while committing
1341 1342 """
1342 1343 raise NotImplementedError
1343 1344
1344 1345
1345 1346 class BaseInMemoryChangesetClass(type):
1346 1347
1347 1348 def __instancecheck__(self, instance):
1348 1349 return isinstance(instance, BaseInMemoryCommit)
1349 1350
1350 1351
1351 1352 class BaseInMemoryChangeset(BaseInMemoryCommit):
1352 1353
1353 1354 __metaclass__ = BaseInMemoryChangesetClass
1354 1355
1355 1356 def __new__(cls, *args, **kwargs):
1356 1357 warnings.warn(
1357 1358 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1358 1359 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1359 1360
1360 1361
1361 1362 class EmptyCommit(BaseCommit):
1362 1363 """
1363 1364 An dummy empty commit. It's possible to pass hash when creating
1364 1365 an EmptyCommit
1365 1366 """
1366 1367
1367 1368 def __init__(
1368 1369 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1369 1370 message='', author='', date=None):
1370 1371 self._empty_commit_id = commit_id
1371 1372 # TODO: johbo: Solve idx parameter, default value does not make
1372 1373 # too much sense
1373 1374 self.idx = idx
1374 1375 self.message = message
1375 1376 self.author = author
1376 1377 self.date = date or datetime.datetime.fromtimestamp(0)
1377 1378 self.repository = repo
1378 1379 self.alias = alias
1379 1380
1380 1381 @LazyProperty
1381 1382 def raw_id(self):
1382 1383 """
1383 1384 Returns raw string identifying this commit, useful for web
1384 1385 representation.
1385 1386 """
1386 1387
1387 1388 return self._empty_commit_id
1388 1389
1389 1390 @LazyProperty
1390 1391 def branch(self):
1391 1392 if self.alias:
1392 1393 from rhodecode.lib.vcs.backends import get_backend
1393 1394 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1394 1395
1395 1396 @LazyProperty
1396 1397 def short_id(self):
1397 1398 return self.raw_id[:12]
1398 1399
1399 1400 @LazyProperty
1400 1401 def id(self):
1401 1402 return self.raw_id
1402 1403
1403 1404 def get_file_commit(self, path):
1404 1405 return self
1405 1406
1406 1407 def get_file_content(self, path):
1407 1408 return u''
1408 1409
1409 1410 def get_file_size(self, path):
1410 1411 return 0
1411 1412
1412 1413
1413 1414 class EmptyChangesetClass(type):
1414 1415
1415 1416 def __instancecheck__(self, instance):
1416 1417 return isinstance(instance, EmptyCommit)
1417 1418
1418 1419
1419 1420 class EmptyChangeset(EmptyCommit):
1420 1421
1421 1422 __metaclass__ = EmptyChangesetClass
1422 1423
1423 1424 def __new__(cls, *args, **kwargs):
1424 1425 warnings.warn(
1425 1426 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1426 1427 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1427 1428
1428 1429 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1429 1430 alias=None, revision=-1, message='', author='', date=None):
1430 1431 if requested_revision is not None:
1431 1432 warnings.warn(
1432 1433 "Parameter requested_revision not supported anymore",
1433 1434 DeprecationWarning)
1434 1435 super(EmptyChangeset, self).__init__(
1435 1436 commit_id=cs, repo=repo, alias=alias, idx=revision,
1436 1437 message=message, author=author, date=date)
1437 1438
1438 1439 @property
1439 1440 def revision(self):
1440 1441 warnings.warn("Use idx instead", DeprecationWarning)
1441 1442 return self.idx
1442 1443
1443 1444 @revision.setter
1444 1445 def revision(self, value):
1445 1446 warnings.warn("Use idx instead", DeprecationWarning)
1446 1447 self.idx = value
1447 1448
1448 1449
1449 1450 class EmptyRepository(BaseRepository):
1450 1451 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1451 1452 pass
1452 1453
1453 1454 def get_diff(self, *args, **kwargs):
1454 1455 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1455 1456 return GitDiff('')
1456 1457
1457 1458
1458 1459 class CollectionGenerator(object):
1459 1460
1460 1461 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1461 1462 self.repo = repo
1462 1463 self.commit_ids = commit_ids
1463 1464 # TODO: (oliver) this isn't currently hooked up
1464 1465 self.collection_size = None
1465 1466 self.pre_load = pre_load
1466 1467
1467 1468 def __len__(self):
1468 1469 if self.collection_size is not None:
1469 1470 return self.collection_size
1470 1471 return self.commit_ids.__len__()
1471 1472
1472 1473 def __iter__(self):
1473 1474 for commit_id in self.commit_ids:
1474 1475 # TODO: johbo: Mercurial passes in commit indices or commit ids
1475 1476 yield self._commit_factory(commit_id)
1476 1477
1477 1478 def _commit_factory(self, commit_id):
1478 1479 """
1479 1480 Allows backends to override the way commits are generated.
1480 1481 """
1481 1482 return self.repo.get_commit(commit_id=commit_id,
1482 1483 pre_load=self.pre_load)
1483 1484
1484 1485 def __getslice__(self, i, j):
1485 1486 """
1486 1487 Returns an iterator of sliced repository
1487 1488 """
1488 1489 commit_ids = self.commit_ids[i:j]
1489 1490 return self.__class__(
1490 1491 self.repo, commit_ids, pre_load=self.pre_load)
1491 1492
1492 1493 def __repr__(self):
1493 1494 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1494 1495
1495 1496
1496 1497 class Config(object):
1497 1498 """
1498 1499 Represents the configuration for a repository.
1499 1500
1500 1501 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1501 1502 standard library. It implements only the needed subset.
1502 1503 """
1503 1504
1504 1505 def __init__(self):
1505 1506 self._values = {}
1506 1507
1507 1508 def copy(self):
1508 1509 clone = Config()
1509 1510 for section, values in self._values.items():
1510 1511 clone._values[section] = values.copy()
1511 1512 return clone
1512 1513
1513 1514 def __repr__(self):
1514 1515 return '<Config(%s sections) at %s>' % (
1515 1516 len(self._values), hex(id(self)))
1516 1517
1517 1518 def items(self, section):
1518 1519 return self._values.get(section, {}).iteritems()
1519 1520
1520 1521 def get(self, section, option):
1521 1522 return self._values.get(section, {}).get(option)
1522 1523
1523 1524 def set(self, section, option, value):
1524 1525 section_values = self._values.setdefault(section, {})
1525 1526 section_values[option] = value
1526 1527
1527 1528 def clear_section(self, section):
1528 1529 self._values[section] = {}
1529 1530
1530 1531 def serialize(self):
1531 1532 """
1532 1533 Creates a list of three tuples (section, key, value) representing
1533 1534 this config object.
1534 1535 """
1535 1536 items = []
1536 1537 for section in self._values:
1537 1538 for option, value in self._values[section].items():
1538 1539 items.append(
1539 1540 (safe_str(section), safe_str(option), safe_str(value)))
1540 1541 return items
1541 1542
1542 1543
1543 1544 class Diff(object):
1544 1545 """
1545 1546 Represents a diff result from a repository backend.
1546 1547
1547 1548 Subclasses have to provide a backend specific value for
1548 1549 :attr:`_header_re` and :attr:`_meta_re`.
1549 1550 """
1550 1551 _meta_re = None
1551 1552 _header_re = None
1552 1553
1553 1554 def __init__(self, raw_diff):
1554 1555 self.raw = raw_diff
1555 1556
1556 1557 def chunks(self):
1557 1558 """
1558 1559 split the diff in chunks of separate --git a/file b/file chunks
1559 1560 to make diffs consistent we must prepend with \n, and make sure
1560 1561 we can detect last chunk as this was also has special rule
1561 1562 """
1562 1563
1563 1564 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1564 1565 header = diff_parts[0]
1565 1566
1566 1567 if self._meta_re:
1567 1568 match = self._meta_re.match(header)
1568 1569
1569 1570 chunks = diff_parts[1:]
1570 1571 total_chunks = len(chunks)
1571 1572
1572 1573 return (
1573 1574 DiffChunk(chunk, self, cur_chunk == total_chunks)
1574 1575 for cur_chunk, chunk in enumerate(chunks, start=1))
1575 1576
1576 1577
1577 1578 class DiffChunk(object):
1578 1579
1579 1580 def __init__(self, chunk, diff, last_chunk):
1580 1581 self._diff = diff
1581 1582
1582 1583 # since we split by \ndiff --git that part is lost from original diff
1583 1584 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1584 1585 if not last_chunk:
1585 1586 chunk += '\n'
1586 1587
1587 1588 match = self._diff._header_re.match(chunk)
1588 1589 self.header = match.groupdict()
1589 1590 self.diff = chunk[match.end():]
1590 1591 self.raw = chunk
@@ -1,946 +1,947 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 GIT repository module
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28 import shutil
29 29
30 30 from zope.cachedescriptors.property import Lazy as LazyProperty
31 31
32 32 from rhodecode.lib.compat import OrderedDict
33 33 from rhodecode.lib.datelib import (
34 34 utcdate_fromtimestamp, makedate, date_astimestamp)
35 35 from rhodecode.lib.utils import safe_unicode, safe_str
36 36 from rhodecode.lib.vcs import connection, path as vcspath
37 37 from rhodecode.lib.vcs.backends.base import (
38 38 BaseRepository, CollectionGenerator, Config, MergeResponse,
39 39 MergeFailureReason, Reference)
40 40 from rhodecode.lib.vcs.backends.git.commit import GitCommit
41 41 from rhodecode.lib.vcs.backends.git.diff import GitDiff
42 42 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
43 43 from rhodecode.lib.vcs.exceptions import (
44 44 CommitDoesNotExistError, EmptyRepositoryError,
45 45 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
46 46
47 47
48 48 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class GitRepository(BaseRepository):
54 54 """
55 55 Git repository backend.
56 56 """
57 57 DEFAULT_BRANCH_NAME = 'master'
58 58
59 59 contact = BaseRepository.DEFAULT_CONTACT
60 60
61 61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 62 update_after_clone=False, with_wire=None, bare=False):
63 63
64 64 self.path = safe_str(os.path.abspath(repo_path))
65 65 self.config = config if config else Config()
66 66 self._remote = connection.Git(
67 67 self.path, self.config, with_wire=with_wire)
68 68
69 69 self._init_repo(create, src_url, update_after_clone, bare)
70 70
71 71 # caches
72 72 self._commit_ids = {}
73 73
74 74 self.bookmarks = {}
75 75
76 76 @LazyProperty
77 77 def bare(self):
78 78 return self._remote.bare()
79 79
80 80 @LazyProperty
81 81 def head(self):
82 82 return self._remote.head()
83 83
84 84 @LazyProperty
85 85 def commit_ids(self):
86 86 """
87 87 Returns list of commit ids, in ascending order. Being lazy
88 88 attribute allows external tools to inject commit ids from cache.
89 89 """
90 90 commit_ids = self._get_all_commit_ids()
91 91 self._rebuild_cache(commit_ids)
92 92 return commit_ids
93 93
94 94 def _rebuild_cache(self, commit_ids):
95 95 self._commit_ids = dict((commit_id, index)
96 96 for index, commit_id in enumerate(commit_ids))
97 97
98 98 def run_git_command(self, cmd, **opts):
99 99 """
100 100 Runs given ``cmd`` as git command and returns tuple
101 101 (stdout, stderr).
102 102
103 103 :param cmd: git command to be executed
104 104 :param opts: env options to pass into Subprocess command
105 105 """
106 106 if not isinstance(cmd, list):
107 107 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
108 108
109 109 out, err = self._remote.run_git_command(cmd, **opts)
110 110 if err:
111 111 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
112 112 return out, err
113 113
114 114 @staticmethod
115 115 def check_url(url, config):
116 116 """
117 117 Function will check given url and try to verify if it's a valid
118 118 link. Sometimes it may happened that git will issue basic
119 119 auth request that can cause whole API to hang when used from python
120 120 or other external calls.
121 121
122 122 On failures it'll raise urllib2.HTTPError, exception is also thrown
123 123 when the return code is non 200
124 124 """
125 125 # check first if it's not an url
126 126 if os.path.isdir(url) or url.startswith('file:'):
127 127 return True
128 128
129 129 if '+' in url.split('://', 1)[0]:
130 130 url = url.split('+', 1)[1]
131 131
132 132 # Request the _remote to verify the url
133 133 return connection.Git.check_url(url, config.serialize())
134 134
135 135 @staticmethod
136 136 def is_valid_repository(path):
137 137 if os.path.isdir(os.path.join(path, '.git')):
138 138 return True
139 139 # check case of bare repository
140 140 try:
141 141 GitRepository(path)
142 142 return True
143 143 except VCSError:
144 144 pass
145 145 return False
146 146
147 147 def _init_repo(self, create, src_url=None, update_after_clone=False,
148 148 bare=False):
149 149 if create and os.path.exists(self.path):
150 150 raise RepositoryError(
151 151 "Cannot create repository at %s, location already exist"
152 152 % self.path)
153 153
154 154 try:
155 155 if create and src_url:
156 156 GitRepository.check_url(src_url, self.config)
157 157 self.clone(src_url, update_after_clone, bare)
158 158 elif create:
159 159 os.makedirs(self.path, mode=0755)
160 160
161 161 if bare:
162 162 self._remote.init_bare()
163 163 else:
164 164 self._remote.init()
165 165 else:
166 166 self._remote.assert_correct_path()
167 167 # TODO: johbo: check if we have to translate the OSError here
168 168 except OSError as err:
169 169 raise RepositoryError(err)
170 170
171 171 def _get_all_commit_ids(self, filters=None):
172 172 # we must check if this repo is not empty, since later command
173 173 # fails if it is. And it's cheaper to ask than throw the subprocess
174 174 # errors
175 175 try:
176 176 self._remote.head()
177 177 except KeyError:
178 178 return []
179 179
180 180 rev_filter = ['--branches', '--tags']
181 181 extra_filter = []
182 182
183 183 if filters:
184 184 if filters.get('since'):
185 185 extra_filter.append('--since=%s' % (filters['since']))
186 186 if filters.get('until'):
187 187 extra_filter.append('--until=%s' % (filters['until']))
188 188 if filters.get('branch_name'):
189 189 rev_filter = ['--tags']
190 190 extra_filter.append(filters['branch_name'])
191 191 rev_filter.extend(extra_filter)
192 192
193 193 # if filters.get('start') or filters.get('end'):
194 194 # # skip is offset, max-count is limit
195 195 # if filters.get('start'):
196 196 # extra_filter += ' --skip=%s' % filters['start']
197 197 # if filters.get('end'):
198 198 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
199 199
200 200 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
201 201 try:
202 202 output, __ = self.run_git_command(cmd)
203 203 except RepositoryError:
204 204 # Can be raised for empty repositories
205 205 return []
206 206 return output.splitlines()
207 207
208 208 def _get_commit_id(self, commit_id_or_idx):
209 209 def is_null(value):
210 210 return len(value) == commit_id_or_idx.count('0')
211 211
212 212 if self.is_empty():
213 213 raise EmptyRepositoryError("There are no commits yet")
214 214
215 215 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
216 216 return self.commit_ids[-1]
217 217
218 218 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
219 219 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
220 220 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
221 221 try:
222 222 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
223 223 except Exception:
224 224 msg = "Commit %s does not exist for %s" % (
225 225 commit_id_or_idx, self)
226 226 raise CommitDoesNotExistError(msg)
227 227
228 228 elif is_bstr:
229 229 # check full path ref, eg. refs/heads/master
230 230 ref_id = self._refs.get(commit_id_or_idx)
231 231 if ref_id:
232 232 return ref_id
233 233
234 234 # check branch name
235 235 branch_ids = self.branches.values()
236 236 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
237 237 if ref_id:
238 238 return ref_id
239 239
240 240 # check tag name
241 241 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
242 242 if ref_id:
243 243 return ref_id
244 244
245 245 if (not SHA_PATTERN.match(commit_id_or_idx) or
246 246 commit_id_or_idx not in self.commit_ids):
247 247 msg = "Commit %s does not exist for %s" % (
248 248 commit_id_or_idx, self)
249 249 raise CommitDoesNotExistError(msg)
250 250
251 251 # Ensure we return full id
252 252 if not SHA_PATTERN.match(str(commit_id_or_idx)):
253 253 raise CommitDoesNotExistError(
254 254 "Given commit id %s not recognized" % commit_id_or_idx)
255 255 return commit_id_or_idx
256 256
257 257 def get_hook_location(self):
258 258 """
259 259 returns absolute path to location where hooks are stored
260 260 """
261 261 loc = os.path.join(self.path, 'hooks')
262 262 if not self.bare:
263 263 loc = os.path.join(self.path, '.git', 'hooks')
264 264 return loc
265 265
266 266 @LazyProperty
267 267 def last_change(self):
268 268 """
269 269 Returns last change made on this repository as
270 270 `datetime.datetime` object.
271 271 """
272 272 try:
273 273 return self.get_commit().date
274 274 except RepositoryError:
275 275 tzoffset = makedate()[1]
276 276 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
277 277
278 278 def _get_fs_mtime(self):
279 279 idx_loc = '' if self.bare else '.git'
280 280 # fallback to filesystem
281 281 in_path = os.path.join(self.path, idx_loc, "index")
282 282 he_path = os.path.join(self.path, idx_loc, "HEAD")
283 283 if os.path.exists(in_path):
284 284 return os.stat(in_path).st_mtime
285 285 else:
286 286 return os.stat(he_path).st_mtime
287 287
288 288 @LazyProperty
289 289 def description(self):
290 290 description = self._remote.get_description()
291 291 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
292 292
293 293 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
294 294 if self.is_empty():
295 295 return OrderedDict()
296 296
297 297 result = []
298 298 for ref, sha in self._refs.iteritems():
299 299 if ref.startswith(prefix):
300 300 ref_name = ref
301 301 if strip_prefix:
302 302 ref_name = ref[len(prefix):]
303 303 result.append((safe_unicode(ref_name), sha))
304 304
305 305 def get_name(entry):
306 306 return entry[0]
307 307
308 308 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
309 309
310 310 def _get_branches(self):
311 311 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
312 312
313 313 @LazyProperty
314 314 def branches(self):
315 315 return self._get_branches()
316 316
317 317 @LazyProperty
318 318 def branches_closed(self):
319 319 return {}
320 320
321 321 @LazyProperty
322 322 def branches_all(self):
323 323 all_branches = {}
324 324 all_branches.update(self.branches)
325 325 all_branches.update(self.branches_closed)
326 326 return all_branches
327 327
328 328 @LazyProperty
329 329 def tags(self):
330 330 return self._get_tags()
331 331
332 332 def _get_tags(self):
333 333 return self._get_refs_entries(
334 334 prefix='refs/tags/', strip_prefix=True, reverse=True)
335 335
336 336 def tag(self, name, user, commit_id=None, message=None, date=None,
337 337 **kwargs):
338 338 # TODO: fix this method to apply annotated tags correct with message
339 339 """
340 340 Creates and returns a tag for the given ``commit_id``.
341 341
342 342 :param name: name for new tag
343 343 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
344 344 :param commit_id: commit id for which new tag would be created
345 345 :param message: message of the tag's commit
346 346 :param date: date of tag's commit
347 347
348 348 :raises TagAlreadyExistError: if tag with same name already exists
349 349 """
350 350 if name in self.tags:
351 351 raise TagAlreadyExistError("Tag %s already exists" % name)
352 352 commit = self.get_commit(commit_id=commit_id)
353 353 message = message or "Added tag %s for commit %s" % (
354 354 name, commit.raw_id)
355 355 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
356 356
357 357 self._refs = self._get_refs()
358 358 self.tags = self._get_tags()
359 359 return commit
360 360
361 361 def remove_tag(self, name, user, message=None, date=None):
362 362 """
363 363 Removes tag with the given ``name``.
364 364
365 365 :param name: name of the tag to be removed
366 366 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
367 367 :param message: message of the tag's removal commit
368 368 :param date: date of tag's removal commit
369 369
370 370 :raises TagDoesNotExistError: if tag with given name does not exists
371 371 """
372 372 if name not in self.tags:
373 373 raise TagDoesNotExistError("Tag %s does not exist" % name)
374 374 tagpath = vcspath.join(
375 375 self._remote.get_refs_path(), 'refs', 'tags', name)
376 376 try:
377 377 os.remove(tagpath)
378 378 self._refs = self._get_refs()
379 379 self.tags = self._get_tags()
380 380 except OSError as e:
381 381 raise RepositoryError(e.strerror)
382 382
383 383 def _get_refs(self):
384 384 return self._remote.get_refs()
385 385
386 386 @LazyProperty
387 387 def _refs(self):
388 388 return self._get_refs()
389 389
390 390 @property
391 391 def _ref_tree(self):
392 392 node = tree = {}
393 393 for ref, sha in self._refs.iteritems():
394 394 path = ref.split('/')
395 395 for bit in path[:-1]:
396 396 node = node.setdefault(bit, {})
397 397 node[path[-1]] = sha
398 398 node = tree
399 399 return tree
400 400
401 401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
402 402 """
403 403 Returns `GitCommit` object representing commit from git repository
404 404 at the given `commit_id` or head (most recent commit) if None given.
405 405 """
406 406 if commit_id is not None:
407 407 self._validate_commit_id(commit_id)
408 408 elif commit_idx is not None:
409 409 self._validate_commit_idx(commit_idx)
410 410 commit_id = commit_idx
411 411 commit_id = self._get_commit_id(commit_id)
412 412 try:
413 413 # Need to call remote to translate id for tagging scenario
414 414 commit_id = self._remote.get_object(commit_id)["commit_id"]
415 415 idx = self._commit_ids[commit_id]
416 416 except KeyError:
417 417 raise RepositoryError("Cannot get object with id %s" % commit_id)
418 418
419 419 return GitCommit(self, commit_id, idx, pre_load=pre_load)
420 420
421 421 def get_commits(
422 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 425 Returns generator of `GitCommit` objects from start to end (both
426 426 are inclusive), in ascending date order.
427 427
428 428 :param start_id: None, str(commit_id)
429 429 :param end_id: None, str(commit_id)
430 430 :param start_date: if specified, commits with commit date less than
431 431 ``start_date`` would be filtered out from returned set
432 432 :param end_date: if specified, commits with commit date greater than
433 433 ``end_date`` would be filtered out from returned set
434 434 :param branch_name: if specified, commits not reachable from given
435 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 438 :raise BranchDoesNotExistError: If given `branch_name` does not
438 439 exist.
439 440 :raise CommitDoesNotExistError: If commits for given `start` or
440 441 `end` could not be found.
441 442
442 443 """
443 444 if self.is_empty():
444 445 raise EmptyRepositoryError("There are no commits yet")
445 446 self._validate_branch_name(branch_name)
446 447
447 448 if start_id is not None:
448 449 self._validate_commit_id(start_id)
449 450 if end_id is not None:
450 451 self._validate_commit_id(end_id)
451 452
452 453 start_raw_id = self._get_commit_id(start_id)
453 454 start_pos = self._commit_ids[start_raw_id] if start_id else None
454 455 end_raw_id = self._get_commit_id(end_id)
455 456 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
456 457
457 458 if None not in [start_id, end_id] and start_pos > end_pos:
458 459 raise RepositoryError(
459 460 "Start commit '%s' cannot be after end commit '%s'" %
460 461 (start_id, end_id))
461 462
462 463 if end_pos is not None:
463 464 end_pos += 1
464 465
465 466 filter_ = []
466 467 if branch_name:
467 468 filter_.append({'branch_name': branch_name})
468 469 if start_date and not end_date:
469 470 filter_.append({'since': start_date})
470 471 if end_date and not start_date:
471 472 filter_.append({'until': end_date})
472 473 if start_date and end_date:
473 474 filter_.append({'since': start_date})
474 475 filter_.append({'until': end_date})
475 476
476 477 # if start_pos or end_pos:
477 478 # filter_.append({'start': start_pos})
478 479 # filter_.append({'end': end_pos})
479 480
480 481 if filter_:
481 482 revfilters = {
482 483 'branch_name': branch_name,
483 484 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
484 485 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
485 486 'start': start_pos,
486 487 'end': end_pos,
487 488 }
488 489 commit_ids = self._get_all_commit_ids(filters=revfilters)
489 490
490 491 # pure python stuff, it's slow due to walker walking whole repo
491 492 # def get_revs(walker):
492 493 # for walker_entry in walker:
493 494 # yield walker_entry.commit.id
494 495 # revfilters = {}
495 496 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
496 497 else:
497 498 commit_ids = self.commit_ids
498 499
499 500 if start_pos or end_pos:
500 501 commit_ids = commit_ids[start_pos: end_pos]
501 502
502 503 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
503 504
504 505 def get_diff(
505 506 self, commit1, commit2, path='', ignore_whitespace=False,
506 507 context=3, path1=None):
507 508 """
508 509 Returns (git like) *diff*, as plain text. Shows changes introduced by
509 510 ``commit2`` since ``commit1``.
510 511
511 512 :param commit1: Entry point from which diff is shown. Can be
512 513 ``self.EMPTY_COMMIT`` - in this case, patch showing all
513 514 the changes since empty state of the repository until ``commit2``
514 515 :param commit2: Until which commits changes should be shown.
515 516 :param ignore_whitespace: If set to ``True``, would not show whitespace
516 517 changes. Defaults to ``False``.
517 518 :param context: How many lines before/after changed lines should be
518 519 shown. Defaults to ``3``.
519 520 """
520 521 self._validate_diff_commits(commit1, commit2)
521 522 if path1 is not None and path1 != path:
522 523 raise ValueError("Diff of two different paths not supported.")
523 524
524 525 flags = [
525 526 '-U%s' % context, '--full-index', '--binary', '-p',
526 527 '-M', '--abbrev=40']
527 528 if ignore_whitespace:
528 529 flags.append('-w')
529 530
530 531 if commit1 == self.EMPTY_COMMIT:
531 532 cmd = ['show'] + flags + [commit2.raw_id]
532 533 else:
533 534 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
534 535
535 536 if path:
536 537 cmd.extend(['--', path])
537 538
538 539 stdout, __ = self.run_git_command(cmd)
539 540 # If we used 'show' command, strip first few lines (until actual diff
540 541 # starts)
541 542 if commit1 == self.EMPTY_COMMIT:
542 543 lines = stdout.splitlines()
543 544 x = 0
544 545 for line in lines:
545 546 if line.startswith('diff'):
546 547 break
547 548 x += 1
548 549 # Append new line just like 'diff' command do
549 550 stdout = '\n'.join(lines[x:]) + '\n'
550 551 return GitDiff(stdout)
551 552
552 553 def strip(self, commit_id, branch_name):
553 554 commit = self.get_commit(commit_id=commit_id)
554 555 if commit.merge:
555 556 raise Exception('Cannot reset to merge commit')
556 557
557 558 # parent is going to be the new head now
558 559 commit = commit.parents[0]
559 560 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
560 561
561 562 self.commit_ids = self._get_all_commit_ids()
562 563 self._rebuild_cache(self.commit_ids)
563 564
564 565 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
565 566 if commit_id1 == commit_id2:
566 567 return commit_id1
567 568
568 569 if self != repo2:
569 570 commits = self._remote.get_missing_revs(
570 571 commit_id1, commit_id2, repo2.path)
571 572 if commits:
572 573 commit = repo2.get_commit(commits[-1])
573 574 if commit.parents:
574 575 ancestor_id = commit.parents[0].raw_id
575 576 else:
576 577 ancestor_id = None
577 578 else:
578 579 # no commits from other repo, ancestor_id is the commit_id2
579 580 ancestor_id = commit_id2
580 581 else:
581 582 output, __ = self.run_git_command(
582 583 ['merge-base', commit_id1, commit_id2])
583 584 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
584 585
585 586 return ancestor_id
586 587
587 588 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
588 589 repo1 = self
589 590 ancestor_id = None
590 591
591 592 if commit_id1 == commit_id2:
592 593 commits = []
593 594 elif repo1 != repo2:
594 595 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
595 596 repo2.path)
596 597 commits = [
597 598 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
598 599 for commit_id in reversed(missing_ids)]
599 600 else:
600 601 output, __ = repo1.run_git_command(
601 602 ['log', '--reverse', '--pretty=format: %H', '-s',
602 603 '%s..%s' % (commit_id1, commit_id2)])
603 604 commits = [
604 605 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
605 606 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
606 607
607 608 return commits
608 609
609 610 @LazyProperty
610 611 def in_memory_commit(self):
611 612 """
612 613 Returns ``GitInMemoryCommit`` object for this repository.
613 614 """
614 615 return GitInMemoryCommit(self)
615 616
616 617 def clone(self, url, update_after_clone=True, bare=False):
617 618 """
618 619 Tries to clone commits from external location.
619 620
620 621 :param update_after_clone: If set to ``False``, git won't checkout
621 622 working directory
622 623 :param bare: If set to ``True``, repository would be cloned into
623 624 *bare* git repository (no working directory at all).
624 625 """
625 626 # init_bare and init expect empty dir created to proceed
626 627 if not os.path.exists(self.path):
627 628 os.mkdir(self.path)
628 629
629 630 if bare:
630 631 self._remote.init_bare()
631 632 else:
632 633 self._remote.init()
633 634
634 635 deferred = '^{}'
635 636 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
636 637
637 638 return self._remote.clone(
638 639 url, deferred, valid_refs, update_after_clone)
639 640
640 641 def pull(self, url, commit_ids=None):
641 642 """
642 643 Tries to pull changes from external location. We use fetch here since
643 644 pull in get does merges and we want to be compatible with hg backend so
644 645 pull == fetch in this case
645 646 """
646 647 self.fetch(url, commit_ids=commit_ids)
647 648
648 649 def fetch(self, url, commit_ids=None):
649 650 """
650 651 Tries to fetch changes from external location.
651 652 """
652 653 refs = None
653 654
654 655 if commit_ids is not None:
655 656 remote_refs = self._remote.get_remote_refs(url)
656 657 refs = [
657 658 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
658 659 self._remote.fetch(url, refs=refs)
659 660
660 661 def set_refs(self, ref_name, commit_id):
661 662 self._remote.set_refs(ref_name, commit_id)
662 663
663 664 def remove_ref(self, ref_name):
664 665 self._remote.remove_ref(ref_name)
665 666
666 667 def _update_server_info(self):
667 668 """
668 669 runs gits update-server-info command in this repo instance
669 670 """
670 671 self._remote.update_server_info()
671 672
672 673 def _current_branch(self):
673 674 """
674 675 Return the name of the current branch.
675 676
676 677 It only works for non bare repositories (i.e. repositories with a
677 678 working copy)
678 679 """
679 680 if self.bare:
680 681 raise RepositoryError('Bare git repos do not have active branches')
681 682
682 683 if self.is_empty():
683 684 return None
684 685
685 686 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
686 687 return stdout.strip()
687 688
688 689 def _checkout(self, branch_name, create=False):
689 690 """
690 691 Checkout a branch in the working directory.
691 692
692 693 It tries to create the branch if create is True, failing if the branch
693 694 already exists.
694 695
695 696 It only works for non bare repositories (i.e. repositories with a
696 697 working copy)
697 698 """
698 699 if self.bare:
699 700 raise RepositoryError('Cannot checkout branches in a bare git repo')
700 701
701 702 cmd = ['checkout']
702 703 if create:
703 704 cmd.append('-b')
704 705 cmd.append(branch_name)
705 706 self.run_git_command(cmd, fail_on_stderr=False)
706 707
707 708 def _identify(self):
708 709 """
709 710 Return the current state of the working directory.
710 711 """
711 712 if self.bare:
712 713 raise RepositoryError('Bare git repos do not have active branches')
713 714
714 715 if self.is_empty():
715 716 return None
716 717
717 718 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
718 719 return stdout.strip()
719 720
720 721 def _local_clone(self, clone_path, branch_name):
721 722 """
722 723 Create a local clone of the current repo.
723 724 """
724 725 # N.B.(skreft): the --branch option is required as otherwise the shallow
725 726 # clone will only fetch the active branch.
726 727 cmd = ['clone', '--branch', branch_name, '--single-branch',
727 728 self.path, os.path.abspath(clone_path)]
728 729 self.run_git_command(cmd, fail_on_stderr=False)
729 730
730 731 def _local_fetch(self, repository_path, branch_name):
731 732 """
732 733 Fetch a branch from a local repository.
733 734 """
734 735 repository_path = os.path.abspath(repository_path)
735 736 if repository_path == self.path:
736 737 raise ValueError('Cannot fetch from the same repository')
737 738
738 739 cmd = ['fetch', '--no-tags', repository_path, branch_name]
739 740 self.run_git_command(cmd, fail_on_stderr=False)
740 741
741 742 def _last_fetch_heads(self):
742 743 """
743 744 Return the last fetched heads that need merging.
744 745
745 746 The algorithm is defined at
746 747 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
747 748 """
748 749 if not self.bare:
749 750 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
750 751 else:
751 752 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
752 753
753 754 heads = []
754 755 with open(fetch_heads_path) as f:
755 756 for line in f:
756 757 if ' not-for-merge ' in line:
757 758 continue
758 759 line = re.sub('\t.*', '', line, flags=re.DOTALL)
759 760 heads.append(line)
760 761
761 762 return heads
762 763
763 764 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
764 765 return GitRepository(shadow_repository_path)
765 766
766 767 def _local_pull(self, repository_path, branch_name):
767 768 """
768 769 Pull a branch from a local repository.
769 770 """
770 771 if self.bare:
771 772 raise RepositoryError('Cannot pull into a bare git repository')
772 773 # N.B.(skreft): The --ff-only option is to make sure this is a
773 774 # fast-forward (i.e., we are only pulling new changes and there are no
774 775 # conflicts with our current branch)
775 776 # Additionally, that option needs to go before --no-tags, otherwise git
776 777 # pull complains about it being an unknown flag.
777 778 cmd = ['pull', '--ff-only', '--no-tags', repository_path, branch_name]
778 779 self.run_git_command(cmd, fail_on_stderr=False)
779 780
780 781 def _local_merge(self, merge_message, user_name, user_email, heads):
781 782 """
782 783 Merge the given head into the checked out branch.
783 784
784 785 It will force a merge commit.
785 786
786 787 Currently it raises an error if the repo is empty, as it is not possible
787 788 to create a merge commit in an empty repo.
788 789
789 790 :param merge_message: The message to use for the merge commit.
790 791 :param heads: the heads to merge.
791 792 """
792 793 if self.bare:
793 794 raise RepositoryError('Cannot merge into a bare git repository')
794 795
795 796 if not heads:
796 797 return
797 798
798 799 if self.is_empty():
799 800 # TODO(skreft): do somehting more robust in this case.
800 801 raise RepositoryError(
801 802 'Do not know how to merge into empty repositories yet')
802 803
803 804 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
804 805 # commit message. We also specify the user who is doing the merge.
805 806 cmd = ['-c', 'user.name=%s' % safe_str(user_name),
806 807 '-c', 'user.email=%s' % safe_str(user_email),
807 808 'merge', '--no-ff', '-m', safe_str(merge_message)]
808 809 cmd.extend(heads)
809 810 try:
810 811 self.run_git_command(cmd, fail_on_stderr=False)
811 812 except RepositoryError:
812 813 # Cleanup any merge leftovers
813 814 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
814 815 raise
815 816
816 817 def _local_push(
817 818 self, source_branch, repository_path, target_branch,
818 819 enable_hooks=False, rc_scm_data=None):
819 820 """
820 821 Push the source_branch to the given repository and target_branch.
821 822
822 823 Currently it if the target_branch is not master and the target repo is
823 824 empty, the push will work, but then GitRepository won't be able to find
824 825 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
825 826 pointing to master, which does not exist).
826 827
827 828 It does not run the hooks in the target repo.
828 829 """
829 830 # TODO(skreft): deal with the case in which the target repo is empty,
830 831 # and the target_branch is not master.
831 832 target_repo = GitRepository(repository_path)
832 833 if (not target_repo.bare and
833 834 target_repo._current_branch() == target_branch):
834 835 # Git prevents pushing to the checked out branch, so simulate it by
835 836 # pulling into the target repository.
836 837 target_repo._local_pull(self.path, source_branch)
837 838 else:
838 839 cmd = ['push', os.path.abspath(repository_path),
839 840 '%s:%s' % (source_branch, target_branch)]
840 841 gitenv = {}
841 842 if rc_scm_data:
842 843 gitenv.update({'RC_SCM_DATA': rc_scm_data})
843 844
844 845 if not enable_hooks:
845 846 gitenv['RC_SKIP_HOOKS'] = '1'
846 847 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
847 848
848 849 def _get_new_pr_branch(self, source_branch, target_branch):
849 850 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
850 851 pr_branches = []
851 852 for branch in self.branches:
852 853 if branch.startswith(prefix):
853 854 pr_branches.append(int(branch[len(prefix):]))
854 855
855 856 if not pr_branches:
856 857 branch_id = 0
857 858 else:
858 859 branch_id = max(pr_branches) + 1
859 860
860 861 return '%s%d' % (prefix, branch_id)
861 862
862 863 def _merge_repo(self, shadow_repository_path, target_ref,
863 864 source_repo, source_ref, merge_message,
864 865 merger_name, merger_email, dry_run=False,
865 866 use_rebase=False, close_branch=False):
866 867 if target_ref.commit_id != self.branches[target_ref.name]:
867 868 return MergeResponse(
868 869 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
869 870
870 871 shadow_repo = GitRepository(shadow_repository_path)
871 872 shadow_repo._checkout(target_ref.name)
872 873 shadow_repo._local_pull(self.path, target_ref.name)
873 874 # Need to reload repo to invalidate the cache, or otherwise we cannot
874 875 # retrieve the last target commit.
875 876 shadow_repo = GitRepository(shadow_repository_path)
876 877 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
877 878 return MergeResponse(
878 879 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
879 880
880 881 pr_branch = shadow_repo._get_new_pr_branch(
881 882 source_ref.name, target_ref.name)
882 883 shadow_repo._checkout(pr_branch, create=True)
883 884 try:
884 885 shadow_repo._local_fetch(source_repo.path, source_ref.name)
885 886 except RepositoryError:
886 887 log.exception('Failure when doing local fetch on git shadow repo')
887 888 return MergeResponse(
888 889 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
889 890
890 891 merge_ref = None
891 892 merge_failure_reason = MergeFailureReason.NONE
892 893 try:
893 894 shadow_repo._local_merge(merge_message, merger_name, merger_email,
894 895 [source_ref.commit_id])
895 896 merge_possible = True
896 897
897 898 # Need to reload repo to invalidate the cache, or otherwise we
898 899 # cannot retrieve the merge commit.
899 900 shadow_repo = GitRepository(shadow_repository_path)
900 901 merge_commit_id = shadow_repo.branches[pr_branch]
901 902
902 903 # Set a reference pointing to the merge commit. This reference may
903 904 # be used to easily identify the last successful merge commit in
904 905 # the shadow repository.
905 906 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
906 907 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
907 908 except RepositoryError:
908 909 log.exception('Failure when doing local merge on git shadow repo')
909 910 merge_possible = False
910 911 merge_failure_reason = MergeFailureReason.MERGE_FAILED
911 912
912 913 if merge_possible and not dry_run:
913 914 try:
914 915 shadow_repo._local_push(
915 916 pr_branch, self.path, target_ref.name, enable_hooks=True,
916 917 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
917 918 merge_succeeded = True
918 919 except RepositoryError:
919 920 log.exception(
920 921 'Failure when doing local push on git shadow repo')
921 922 merge_succeeded = False
922 923 merge_failure_reason = MergeFailureReason.PUSH_FAILED
923 924 else:
924 925 merge_succeeded = False
925 926
926 927 return MergeResponse(
927 928 merge_possible, merge_succeeded, merge_ref,
928 929 merge_failure_reason)
929 930
930 931 def _get_shadow_repository_path(self, workspace_id):
931 932 # The name of the shadow repository must start with '.', so it is
932 933 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
933 934 return os.path.join(
934 935 os.path.dirname(self.path),
935 936 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
936 937
937 938 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
938 939 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
939 940 if not os.path.exists(shadow_repository_path):
940 941 self._local_clone(shadow_repository_path, target_ref.name)
941 942
942 943 return shadow_repository_path
943 944
944 945 def cleanup_merge_workspace(self, workspace_id):
945 946 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
946 947 shutil.rmtree(shadow_repository_path, ignore_errors=True)
@@ -1,882 +1,889 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 HG repository module
23 23 """
24 24
25 25 import logging
26 26 import binascii
27 27 import os
28 28 import shutil
29 29 import urllib
30 30
31 31 from zope.cachedescriptors.property import Lazy as LazyProperty
32 32
33 33 from rhodecode.lib.compat import OrderedDict
34 34 from rhodecode.lib.datelib import (
35 35 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate,
36 36 date_astimestamp)
37 37 from rhodecode.lib.utils import safe_unicode, safe_str
38 38 from rhodecode.lib.vcs import connection
39 39 from rhodecode.lib.vcs.backends.base import (
40 40 BaseRepository, CollectionGenerator, Config, MergeResponse,
41 41 MergeFailureReason, Reference)
42 42 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
43 43 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
44 44 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
47 47 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError)
48 48
49 49 hexlify = binascii.hexlify
50 50 nullid = "\0" * 20
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class MercurialRepository(BaseRepository):
56 56 """
57 57 Mercurial repository backend
58 58 """
59 59 DEFAULT_BRANCH_NAME = 'default'
60 60
61 61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 62 update_after_clone=False, with_wire=None):
63 63 """
64 64 Raises RepositoryError if repository could not be find at the given
65 65 ``repo_path``.
66 66
67 67 :param repo_path: local path of the repository
68 68 :param config: config object containing the repo configuration
69 69 :param create=False: if set to True, would try to create repository if
70 70 it does not exist rather than raising exception
71 71 :param src_url=None: would try to clone repository from given location
72 72 :param update_after_clone=False: sets update of working copy after
73 73 making a clone
74 74 """
75 75 self.path = safe_str(os.path.abspath(repo_path))
76 76 self.config = config if config else Config()
77 77 self._remote = connection.Hg(
78 78 self.path, self.config, with_wire=with_wire)
79 79
80 80 self._init_repo(create, src_url, update_after_clone)
81 81
82 82 # caches
83 83 self._commit_ids = {}
84 84
85 85 @LazyProperty
86 86 def commit_ids(self):
87 87 """
88 88 Returns list of commit ids, in ascending order. Being lazy
89 89 attribute allows external tools to inject shas from cache.
90 90 """
91 91 commit_ids = self._get_all_commit_ids()
92 92 self._rebuild_cache(commit_ids)
93 93 return commit_ids
94 94
95 95 def _rebuild_cache(self, commit_ids):
96 96 self._commit_ids = dict((commit_id, index)
97 97 for index, commit_id in enumerate(commit_ids))
98 98
99 99 @LazyProperty
100 100 def branches(self):
101 101 return self._get_branches()
102 102
103 103 @LazyProperty
104 104 def branches_closed(self):
105 105 return self._get_branches(active=False, closed=True)
106 106
107 107 @LazyProperty
108 108 def branches_all(self):
109 109 all_branches = {}
110 110 all_branches.update(self.branches)
111 111 all_branches.update(self.branches_closed)
112 112 return all_branches
113 113
114 114 def _get_branches(self, active=True, closed=False):
115 115 """
116 116 Gets branches for this repository
117 117 Returns only not closed active branches by default
118 118
119 119 :param active: return also active branches
120 120 :param closed: return also closed branches
121 121
122 122 """
123 123 if self.is_empty():
124 124 return {}
125 125
126 126 def get_name(ctx):
127 127 return ctx[0]
128 128
129 129 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
130 130 self._remote.branches(active, closed).items()]
131 131
132 132 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
133 133
134 134 @LazyProperty
135 135 def tags(self):
136 136 """
137 137 Gets tags for this repository
138 138 """
139 139 return self._get_tags()
140 140
141 141 def _get_tags(self):
142 142 if self.is_empty():
143 143 return {}
144 144
145 145 def get_name(ctx):
146 146 return ctx[0]
147 147
148 148 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
149 149 self._remote.tags().items()]
150 150
151 151 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
152 152
153 153 def tag(self, name, user, commit_id=None, message=None, date=None,
154 154 **kwargs):
155 155 """
156 156 Creates and returns a tag for the given ``commit_id``.
157 157
158 158 :param name: name for new tag
159 159 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
160 160 :param commit_id: commit id for which new tag would be created
161 161 :param message: message of the tag's commit
162 162 :param date: date of tag's commit
163 163
164 164 :raises TagAlreadyExistError: if tag with same name already exists
165 165 """
166 166 if name in self.tags:
167 167 raise TagAlreadyExistError("Tag %s already exists" % name)
168 168 commit = self.get_commit(commit_id=commit_id)
169 169 local = kwargs.setdefault('local', False)
170 170
171 171 if message is None:
172 172 message = "Added tag %s for commit %s" % (name, commit.short_id)
173 173
174 174 date, tz = date_to_timestamp_plus_offset(date)
175 175
176 176 self._remote.tag(
177 177 name, commit.raw_id, message, local, user, date, tz)
178 178 self._remote.invalidate_vcs_cache()
179 179
180 180 # Reinitialize tags
181 181 self.tags = self._get_tags()
182 182 tag_id = self.tags[name]
183 183
184 184 return self.get_commit(commit_id=tag_id)
185 185
186 186 def remove_tag(self, name, user, message=None, date=None):
187 187 """
188 188 Removes tag with the given `name`.
189 189
190 190 :param name: name of the tag to be removed
191 191 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
192 192 :param message: message of the tag's removal commit
193 193 :param date: date of tag's removal commit
194 194
195 195 :raises TagDoesNotExistError: if tag with given name does not exists
196 196 """
197 197 if name not in self.tags:
198 198 raise TagDoesNotExistError("Tag %s does not exist" % name)
199 199 if message is None:
200 200 message = "Removed tag %s" % name
201 201 local = False
202 202
203 203 date, tz = date_to_timestamp_plus_offset(date)
204 204
205 205 self._remote.tag(name, nullid, message, local, user, date, tz)
206 206 self._remote.invalidate_vcs_cache()
207 207 self.tags = self._get_tags()
208 208
209 209 @LazyProperty
210 210 def bookmarks(self):
211 211 """
212 212 Gets bookmarks for this repository
213 213 """
214 214 return self._get_bookmarks()
215 215
216 216 def _get_bookmarks(self):
217 217 if self.is_empty():
218 218 return {}
219 219
220 220 def get_name(ctx):
221 221 return ctx[0]
222 222
223 223 _bookmarks = [
224 224 (safe_unicode(n), hexlify(h)) for n, h in
225 225 self._remote.bookmarks().items()]
226 226
227 227 return OrderedDict(sorted(_bookmarks, key=get_name))
228 228
229 229 def _get_all_commit_ids(self):
230 230 return self._remote.get_all_commit_ids('visible')
231 231
232 232 def get_diff(
233 233 self, commit1, commit2, path='', ignore_whitespace=False,
234 234 context=3, path1=None):
235 235 """
236 236 Returns (git like) *diff*, as plain text. Shows changes introduced by
237 237 `commit2` since `commit1`.
238 238
239 239 :param commit1: Entry point from which diff is shown. Can be
240 240 ``self.EMPTY_COMMIT`` - in this case, patch showing all
241 241 the changes since empty state of the repository until `commit2`
242 242 :param commit2: Until which commit changes should be shown.
243 243 :param ignore_whitespace: If set to ``True``, would not show whitespace
244 244 changes. Defaults to ``False``.
245 245 :param context: How many lines before/after changed lines should be
246 246 shown. Defaults to ``3``.
247 247 """
248 248 self._validate_diff_commits(commit1, commit2)
249 249 if path1 is not None and path1 != path:
250 250 raise ValueError("Diff of two different paths not supported.")
251 251
252 252 if path:
253 253 file_filter = [self.path, path]
254 254 else:
255 255 file_filter = None
256 256
257 257 diff = self._remote.diff(
258 258 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
259 259 opt_git=True, opt_ignorews=ignore_whitespace,
260 260 context=context)
261 261 return MercurialDiff(diff)
262 262
263 263 def strip(self, commit_id, branch=None):
264 264 self._remote.strip(commit_id, update=False, backup="none")
265 265
266 266 self._remote.invalidate_vcs_cache()
267 267 self.commit_ids = self._get_all_commit_ids()
268 268 self._rebuild_cache(self.commit_ids)
269 269
270 270 def verify(self):
271 271 verify = self._remote.verify()
272 272
273 273 self._remote.invalidate_vcs_cache()
274 274 return verify
275 275
276 276 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
277 277 if commit_id1 == commit_id2:
278 278 return commit_id1
279 279
280 280 ancestors = self._remote.revs_from_revspec(
281 281 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
282 282 other_path=repo2.path)
283 283 return repo2[ancestors[0]].raw_id if ancestors else None
284 284
285 285 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
286 286 if commit_id1 == commit_id2:
287 287 commits = []
288 288 else:
289 289 if merge:
290 290 indexes = self._remote.revs_from_revspec(
291 291 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
292 292 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
293 293 else:
294 294 indexes = self._remote.revs_from_revspec(
295 295 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
296 296 commit_id1, other_path=repo2.path)
297 297
298 298 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
299 299 for idx in indexes]
300 300
301 301 return commits
302 302
303 303 @staticmethod
304 304 def check_url(url, config):
305 305 """
306 306 Function will check given url and try to verify if it's a valid
307 307 link. Sometimes it may happened that mercurial will issue basic
308 308 auth request that can cause whole API to hang when used from python
309 309 or other external calls.
310 310
311 311 On failures it'll raise urllib2.HTTPError, exception is also thrown
312 312 when the return code is non 200
313 313 """
314 314 # check first if it's not an local url
315 315 if os.path.isdir(url) or url.startswith('file:'):
316 316 return True
317 317
318 318 # Request the _remote to verify the url
319 319 return connection.Hg.check_url(url, config.serialize())
320 320
321 321 @staticmethod
322 322 def is_valid_repository(path):
323 323 return os.path.isdir(os.path.join(path, '.hg'))
324 324
325 325 def _init_repo(self, create, src_url=None, update_after_clone=False):
326 326 """
327 327 Function will check for mercurial repository in given path. If there
328 328 is no repository in that path it will raise an exception unless
329 329 `create` parameter is set to True - in that case repository would
330 330 be created.
331 331
332 332 If `src_url` is given, would try to clone repository from the
333 333 location at given clone_point. Additionally it'll make update to
334 334 working copy accordingly to `update_after_clone` flag.
335 335 """
336 336 if create and os.path.exists(self.path):
337 337 raise RepositoryError(
338 338 "Cannot create repository at %s, location already exist"
339 339 % self.path)
340 340
341 341 if src_url:
342 342 url = str(self._get_url(src_url))
343 343 MercurialRepository.check_url(url, self.config)
344 344
345 345 self._remote.clone(url, self.path, update_after_clone)
346 346
347 347 # Don't try to create if we've already cloned repo
348 348 create = False
349 349
350 350 if create:
351 351 os.makedirs(self.path, mode=0755)
352 352
353 353 self._remote.localrepository(create)
354 354
355 355 @LazyProperty
356 356 def in_memory_commit(self):
357 357 return MercurialInMemoryCommit(self)
358 358
359 359 @LazyProperty
360 360 def description(self):
361 361 description = self._remote.get_config_value(
362 362 'web', 'description', untrusted=True)
363 363 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
364 364
365 365 @LazyProperty
366 366 def contact(self):
367 367 contact = (
368 368 self._remote.get_config_value("web", "contact") or
369 369 self._remote.get_config_value("ui", "username"))
370 370 return safe_unicode(contact or self.DEFAULT_CONTACT)
371 371
372 372 @LazyProperty
373 373 def last_change(self):
374 374 """
375 375 Returns last change made on this repository as
376 376 `datetime.datetime` object.
377 377 """
378 378 try:
379 379 return self.get_commit().date
380 380 except RepositoryError:
381 381 tzoffset = makedate()[1]
382 382 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
383 383
384 384 def _get_fs_mtime(self):
385 385 # fallback to filesystem
386 386 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
387 387 st_path = os.path.join(self.path, '.hg', "store")
388 388 if os.path.exists(cl_path):
389 389 return os.stat(cl_path).st_mtime
390 390 else:
391 391 return os.stat(st_path).st_mtime
392 392
393 393 def _sanitize_commit_idx(self, idx):
394 394 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
395 395 # number. A `long` is treated in the correct way though. So we convert
396 396 # `int` to `long` here to make sure it is handled correctly.
397 397 if isinstance(idx, int):
398 398 return long(idx)
399 399 return idx
400 400
401 401 def _get_url(self, url):
402 402 """
403 403 Returns normalized url. If schema is not given, would fall
404 404 to filesystem
405 405 (``file:///``) schema.
406 406 """
407 407 url = url.encode('utf8')
408 408 if url != 'default' and '://' not in url:
409 409 url = "file:" + urllib.pathname2url(url)
410 410 return url
411 411
412 412 def get_hook_location(self):
413 413 """
414 414 returns absolute path to location where hooks are stored
415 415 """
416 416 return os.path.join(self.path, '.hg', '.hgrc')
417 417
418 418 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
419 419 """
420 420 Returns ``MercurialCommit`` object representing repository's
421 421 commit at the given `commit_id` or `commit_idx`.
422 422 """
423 423 if self.is_empty():
424 424 raise EmptyRepositoryError("There are no commits yet")
425 425
426 426 if commit_id is not None:
427 427 self._validate_commit_id(commit_id)
428 428 try:
429 429 idx = self._commit_ids[commit_id]
430 430 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
431 431 except KeyError:
432 432 pass
433 433 elif commit_idx is not None:
434 434 self._validate_commit_idx(commit_idx)
435 435 commit_idx = self._sanitize_commit_idx(commit_idx)
436 436 try:
437 437 id_ = self.commit_ids[commit_idx]
438 438 if commit_idx < 0:
439 439 commit_idx += len(self.commit_ids)
440 440 return MercurialCommit(
441 441 self, id_, commit_idx, pre_load=pre_load)
442 442 except IndexError:
443 443 commit_id = commit_idx
444 444 else:
445 445 commit_id = "tip"
446 446
447 447 # TODO Paris: Ugly hack to "serialize" long for msgpack
448 448 if isinstance(commit_id, long):
449 449 commit_id = float(commit_id)
450 450
451 451 if isinstance(commit_id, unicode):
452 452 commit_id = safe_str(commit_id)
453 453
454 454 try:
455 455 raw_id, idx = self._remote.lookup(commit_id, both=True)
456 456 except CommitDoesNotExistError:
457 457 msg = "Commit %s does not exist for %s" % (
458 458 commit_id, self)
459 459 raise CommitDoesNotExistError(msg)
460 460
461 461 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
462 462
463 463 def get_commits(
464 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 467 Returns generator of ``MercurialCommit`` objects from start to end
468 468 (both are inclusive)
469 469
470 470 :param start_id: None, str(commit_id)
471 471 :param end_id: None, str(commit_id)
472 472 :param start_date: if specified, commits with commit date less than
473 473 ``start_date`` would be filtered out from returned set
474 474 :param end_date: if specified, commits with commit date greater than
475 475 ``end_date`` would be filtered out from returned set
476 476 :param branch_name: if specified, commits not reachable from given
477 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 480 :raise BranchDoesNotExistError: If given ``branch_name`` does not
480 481 exist.
481 482 :raise CommitDoesNotExistError: If commit for given ``start`` or
482 483 ``end`` could not be found.
483 484 """
484 485 # actually we should check now if it's not an empty repo
485 486 branch_ancestors = False
486 487 if self.is_empty():
487 488 raise EmptyRepositoryError("There are no commits yet")
488 489 self._validate_branch_name(branch_name)
489 490
490 491 if start_id is not None:
491 492 self._validate_commit_id(start_id)
492 493 c_start = self.get_commit(commit_id=start_id)
493 494 start_pos = self._commit_ids[c_start.raw_id]
494 495 else:
495 496 start_pos = None
496 497
497 498 if end_id is not None:
498 499 self._validate_commit_id(end_id)
499 500 c_end = self.get_commit(commit_id=end_id)
500 501 end_pos = max(0, self._commit_ids[c_end.raw_id])
501 502 else:
502 503 end_pos = None
503 504
504 505 if None not in [start_id, end_id] and start_pos > end_pos:
505 506 raise RepositoryError(
506 507 "Start commit '%s' cannot be after end commit '%s'" %
507 508 (start_id, end_id))
508 509
509 510 if end_pos is not None:
510 511 end_pos += 1
511 512
512 513 commit_filter = []
514
513 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 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 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 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 524 if start_date and end_date:
522 525 commit_filter.append(
523 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 532 # TODO: johbo: Figure out a simpler way for this solution
526 533 collection_generator = CollectionGenerator
527 534 if commit_filter:
528 commit_filter = map(safe_str, commit_filter)
529 revisions = self._remote.rev_range(commit_filter)
535 commit_filter = ' and '.join(map(safe_str, commit_filter))
536 revisions = self._remote.rev_range([commit_filter])
530 537 collection_generator = MercurialIndexBasedCollectionGenerator
531 538 else:
532 539 revisions = self.commit_ids
533 540
534 541 if start_pos or end_pos:
535 542 revisions = revisions[start_pos:end_pos]
536 543
537 544 return collection_generator(self, revisions, pre_load=pre_load)
538 545
539 546 def pull(self, url, commit_ids=None):
540 547 """
541 548 Tries to pull changes from external location.
542 549
543 550 :param commit_ids: Optional. Can be set to a list of commit ids
544 551 which shall be pulled from the other repository.
545 552 """
546 553 url = self._get_url(url)
547 554 self._remote.pull(url, commit_ids=commit_ids)
548 555 self._remote.invalidate_vcs_cache()
549 556
550 557 def _local_clone(self, clone_path):
551 558 """
552 559 Create a local clone of the current repo.
553 560 """
554 561 self._remote.clone(self.path, clone_path, update_after_clone=True,
555 562 hooks=False)
556 563
557 564 def _update(self, revision, clean=False):
558 565 """
559 566 Update the working copy to the specified revision.
560 567 """
561 568 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
562 569 self._remote.update(revision, clean=clean)
563 570
564 571 def _identify(self):
565 572 """
566 573 Return the current state of the working directory.
567 574 """
568 575 return self._remote.identify().strip().rstrip('+')
569 576
570 577 def _heads(self, branch=None):
571 578 """
572 579 Return the commit ids of the repository heads.
573 580 """
574 581 return self._remote.heads(branch=branch).strip().split(' ')
575 582
576 583 def _ancestor(self, revision1, revision2):
577 584 """
578 585 Return the common ancestor of the two revisions.
579 586 """
580 587 return self._remote.ancestor(revision1, revision2)
581 588
582 589 def _local_push(
583 590 self, revision, repository_path, push_branches=False,
584 591 enable_hooks=False):
585 592 """
586 593 Push the given revision to the specified repository.
587 594
588 595 :param push_branches: allow to create branches in the target repo.
589 596 """
590 597 self._remote.push(
591 598 [revision], repository_path, hooks=enable_hooks,
592 599 push_branches=push_branches)
593 600
594 601 def _local_merge(self, target_ref, merge_message, user_name, user_email,
595 602 source_ref, use_rebase=False, dry_run=False):
596 603 """
597 604 Merge the given source_revision into the checked out revision.
598 605
599 606 Returns the commit id of the merge and a boolean indicating if the
600 607 commit needs to be pushed.
601 608 """
602 609 self._update(target_ref.commit_id)
603 610
604 611 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
605 612 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
606 613
607 614 if ancestor == source_ref.commit_id:
608 615 # Nothing to do, the changes were already integrated
609 616 return target_ref.commit_id, False
610 617
611 618 elif ancestor == target_ref.commit_id and is_the_same_branch:
612 619 # In this case we should force a commit message
613 620 return source_ref.commit_id, True
614 621
615 622 if use_rebase:
616 623 try:
617 624 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
618 625 target_ref.commit_id)
619 626 self.bookmark(bookmark_name, revision=source_ref.commit_id)
620 627 self._remote.rebase(
621 628 source=source_ref.commit_id, dest=target_ref.commit_id)
622 629 self._remote.invalidate_vcs_cache()
623 630 self._update(bookmark_name)
624 631 return self._identify(), True
625 632 except RepositoryError:
626 633 # The rebase-abort may raise another exception which 'hides'
627 634 # the original one, therefore we log it here.
628 635 log.exception('Error while rebasing shadow repo during merge.')
629 636
630 637 # Cleanup any rebase leftovers
631 638 self._remote.invalidate_vcs_cache()
632 639 self._remote.rebase(abort=True)
633 640 self._remote.invalidate_vcs_cache()
634 641 self._remote.update(clean=True)
635 642 raise
636 643 else:
637 644 try:
638 645 self._remote.merge(source_ref.commit_id)
639 646 self._remote.invalidate_vcs_cache()
640 647 self._remote.commit(
641 648 message=safe_str(merge_message),
642 649 username=safe_str('%s <%s>' % (user_name, user_email)))
643 650 self._remote.invalidate_vcs_cache()
644 651 return self._identify(), True
645 652 except RepositoryError:
646 653 # Cleanup any merge leftovers
647 654 self._remote.update(clean=True)
648 655 raise
649 656
650 657 def _local_close(self, target_ref, user_name, user_email,
651 658 source_ref, close_message=''):
652 659 """
653 660 Close the branch of the given source_revision
654 661
655 662 Returns the commit id of the close and a boolean indicating if the
656 663 commit needs to be pushed.
657 664 """
658 665 self._update(source_ref.commit_id)
659 666 message = close_message or "Closing branch: `{}`".format(source_ref.name)
660 667 try:
661 668 self._remote.commit(
662 669 message=safe_str(message),
663 670 username=safe_str('%s <%s>' % (user_name, user_email)),
664 671 close_branch=True)
665 672 self._remote.invalidate_vcs_cache()
666 673 return self._identify(), True
667 674 except RepositoryError:
668 675 # Cleanup any commit leftovers
669 676 self._remote.update(clean=True)
670 677 raise
671 678
672 679 def _is_the_same_branch(self, target_ref, source_ref):
673 680 return (
674 681 self._get_branch_name(target_ref) ==
675 682 self._get_branch_name(source_ref))
676 683
677 684 def _get_branch_name(self, ref):
678 685 if ref.type == 'branch':
679 686 return ref.name
680 687 return self._remote.ctx_branch(ref.commit_id)
681 688
682 689 def _get_shadow_repository_path(self, workspace_id):
683 690 # The name of the shadow repository must start with '.', so it is
684 691 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
685 692 return os.path.join(
686 693 os.path.dirname(self.path),
687 694 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
688 695
689 696 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
690 697 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
691 698 if not os.path.exists(shadow_repository_path):
692 699 self._local_clone(shadow_repository_path)
693 700 log.debug(
694 701 'Prepared shadow repository in %s', shadow_repository_path)
695 702
696 703 return shadow_repository_path
697 704
698 705 def cleanup_merge_workspace(self, workspace_id):
699 706 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
700 707 shutil.rmtree(shadow_repository_path, ignore_errors=True)
701 708
702 709 def _merge_repo(self, shadow_repository_path, target_ref,
703 710 source_repo, source_ref, merge_message,
704 711 merger_name, merger_email, dry_run=False,
705 712 use_rebase=False, close_branch=False):
706 713
707 714 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
708 715 'rebase' if use_rebase else 'merge', dry_run)
709 716 if target_ref.commit_id not in self._heads():
710 717 return MergeResponse(
711 718 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
712 719
713 720 try:
714 721 if (target_ref.type == 'branch' and
715 722 len(self._heads(target_ref.name)) != 1):
716 723 return MergeResponse(
717 724 False, False, None,
718 725 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
719 726 except CommitDoesNotExistError:
720 727 log.exception('Failure when looking up branch heads on hg target')
721 728 return MergeResponse(
722 729 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
723 730
724 731 shadow_repo = self._get_shadow_instance(shadow_repository_path)
725 732
726 733 log.debug('Pulling in target reference %s', target_ref)
727 734 self._validate_pull_reference(target_ref)
728 735 shadow_repo._local_pull(self.path, target_ref)
729 736 try:
730 737 log.debug('Pulling in source reference %s', source_ref)
731 738 source_repo._validate_pull_reference(source_ref)
732 739 shadow_repo._local_pull(source_repo.path, source_ref)
733 740 except CommitDoesNotExistError:
734 741 log.exception('Failure when doing local pull on hg shadow repo')
735 742 return MergeResponse(
736 743 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
737 744
738 745 merge_ref = None
739 746 merge_commit_id = None
740 747 close_commit_id = None
741 748 merge_failure_reason = MergeFailureReason.NONE
742 749
743 750 # enforce that close branch should be used only in case we source from
744 751 # an actual Branch
745 752 close_branch = close_branch and source_ref.type == 'branch'
746 753
747 754 # don't allow to close branch if source and target are the same
748 755 close_branch = close_branch and source_ref.name != target_ref.name
749 756
750 757 needs_push_on_close = False
751 758 if close_branch and not use_rebase and not dry_run:
752 759 try:
753 760 close_commit_id, needs_push_on_close = shadow_repo._local_close(
754 761 target_ref, merger_name, merger_email, source_ref)
755 762 merge_possible = True
756 763 except RepositoryError:
757 764 log.exception(
758 765 'Failure when doing close branch on hg shadow repo')
759 766 merge_possible = False
760 767 merge_failure_reason = MergeFailureReason.MERGE_FAILED
761 768 else:
762 769 merge_possible = True
763 770
764 771 if merge_possible:
765 772 try:
766 773 merge_commit_id, needs_push = shadow_repo._local_merge(
767 774 target_ref, merge_message, merger_name, merger_email,
768 775 source_ref, use_rebase=use_rebase, dry_run=dry_run)
769 776 merge_possible = True
770 777
771 778 # read the state of the close action, if it
772 779 # maybe required a push
773 780 needs_push = needs_push or needs_push_on_close
774 781
775 782 # Set a bookmark pointing to the merge commit. This bookmark
776 783 # may be used to easily identify the last successful merge
777 784 # commit in the shadow repository.
778 785 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
779 786 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
780 787 except SubrepoMergeError:
781 788 log.exception(
782 789 'Subrepo merge error during local merge on hg shadow repo.')
783 790 merge_possible = False
784 791 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
785 792 needs_push = False
786 793 except RepositoryError:
787 794 log.exception('Failure when doing local merge on hg shadow repo')
788 795 merge_possible = False
789 796 merge_failure_reason = MergeFailureReason.MERGE_FAILED
790 797 needs_push = False
791 798
792 799 if merge_possible and not dry_run:
793 800 if needs_push:
794 801 # In case the target is a bookmark, update it, so after pushing
795 802 # the bookmarks is also updated in the target.
796 803 if target_ref.type == 'book':
797 804 shadow_repo.bookmark(
798 805 target_ref.name, revision=merge_commit_id)
799 806 try:
800 807 shadow_repo_with_hooks = self._get_shadow_instance(
801 808 shadow_repository_path,
802 809 enable_hooks=True)
803 810 # This is the actual merge action, we push from shadow
804 811 # into origin.
805 812 # Note: the push_branches option will push any new branch
806 813 # defined in the source repository to the target. This may
807 814 # be dangerous as branches are permanent in Mercurial.
808 815 # This feature was requested in issue #441.
809 816 shadow_repo_with_hooks._local_push(
810 817 merge_commit_id, self.path, push_branches=True,
811 818 enable_hooks=True)
812 819
813 820 # maybe we also need to push the close_commit_id
814 821 if close_commit_id:
815 822 shadow_repo_with_hooks._local_push(
816 823 close_commit_id, self.path, push_branches=True,
817 824 enable_hooks=True)
818 825 merge_succeeded = True
819 826 except RepositoryError:
820 827 log.exception(
821 828 'Failure when doing local push from the shadow '
822 829 'repository to the target repository.')
823 830 merge_succeeded = False
824 831 merge_failure_reason = MergeFailureReason.PUSH_FAILED
825 832 else:
826 833 merge_succeeded = True
827 834 else:
828 835 merge_succeeded = False
829 836
830 837 return MergeResponse(
831 838 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
832 839
833 840 def _get_shadow_instance(
834 841 self, shadow_repository_path, enable_hooks=False):
835 842 config = self.config.copy()
836 843 if not enable_hooks:
837 844 config.clear_section('hooks')
838 845 return MercurialRepository(shadow_repository_path, config)
839 846
840 847 def _validate_pull_reference(self, reference):
841 848 if not (reference.name in self.bookmarks or
842 849 reference.name in self.branches or
843 850 self.get_commit(reference.commit_id)):
844 851 raise CommitDoesNotExistError(
845 852 'Unknown branch, bookmark or commit id')
846 853
847 854 def _local_pull(self, repository_path, reference):
848 855 """
849 856 Fetch a branch, bookmark or commit from a local repository.
850 857 """
851 858 repository_path = os.path.abspath(repository_path)
852 859 if repository_path == self.path:
853 860 raise ValueError('Cannot pull from the same repository')
854 861
855 862 reference_type_to_option_name = {
856 863 'book': 'bookmark',
857 864 'branch': 'branch',
858 865 }
859 866 option_name = reference_type_to_option_name.get(
860 867 reference.type, 'revision')
861 868
862 869 if option_name == 'revision':
863 870 ref = reference.commit_id
864 871 else:
865 872 ref = reference.name
866 873
867 874 options = {option_name: [ref]}
868 875 self._remote.pull_cmd(repository_path, hooks=False, **options)
869 876 self._remote.invalidate_vcs_cache()
870 877
871 878 def bookmark(self, bookmark, revision=None):
872 879 if isinstance(bookmark, unicode):
873 880 bookmark = safe_str(bookmark)
874 881 self._remote.bookmark(bookmark, revision=revision)
875 882 self._remote.invalidate_vcs_cache()
876 883
877 884
878 885 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
879 886
880 887 def _commit_factory(self, commit_id):
881 888 return self.repo.get_commit(
882 889 commit_idx=commit_id, pre_load=self.pre_load)
@@ -1,339 +1,339 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SVN repository module
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import urllib
28 28
29 29 from zope.cachedescriptors.property import Lazy as LazyProperty
30 30
31 31 from rhodecode.lib.compat import OrderedDict
32 32 from rhodecode.lib.datelib import date_astimestamp
33 33 from rhodecode.lib.utils import safe_str, safe_unicode
34 34 from rhodecode.lib.vcs import connection, path as vcspath
35 35 from rhodecode.lib.vcs.backends import base
36 36 from rhodecode.lib.vcs.backends.svn.commit import (
37 37 SubversionCommit, _date_from_svn_properties)
38 38 from rhodecode.lib.vcs.backends.svn.diff import SubversionDiff
39 39 from rhodecode.lib.vcs.backends.svn.inmemory import SubversionInMemoryCommit
40 40 from rhodecode.lib.vcs.conf import settings
41 41 from rhodecode.lib.vcs.exceptions import (
42 42 CommitDoesNotExistError, EmptyRepositoryError, RepositoryError,
43 43 VCSError, NodeDoesNotExistError)
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class SubversionRepository(base.BaseRepository):
50 50 """
51 51 Subversion backend implementation
52 52
53 53 .. important::
54 54
55 55 It is very important to distinguish the commit index and the commit id
56 56 which is assigned by Subversion. The first one is always handled as an
57 57 `int` by this implementation. The commit id assigned by Subversion on
58 58 the other side will always be a `str`.
59 59
60 60 There is a specific trap since the first commit will have the index
61 61 ``0`` but the svn id will be ``"1"``.
62 62
63 63 """
64 64
65 65 # Note: Subversion does not really have a default branch name.
66 66 DEFAULT_BRANCH_NAME = None
67 67
68 68 contact = base.BaseRepository.DEFAULT_CONTACT
69 69 description = base.BaseRepository.DEFAULT_DESCRIPTION
70 70
71 71 def __init__(self, repo_path, config=None, create=False, src_url=None,
72 72 **kwargs):
73 73 self.path = safe_str(os.path.abspath(repo_path))
74 74 self.config = config if config else base.Config()
75 75 self._remote = connection.Svn(
76 76 self.path, self.config)
77 77
78 78 self._init_repo(create, src_url)
79 79
80 80 self.bookmarks = {}
81 81
82 82 def _init_repo(self, create, src_url):
83 83 if create and os.path.exists(self.path):
84 84 raise RepositoryError(
85 85 "Cannot create repository at %s, location already exist"
86 86 % self.path)
87 87
88 88 if create:
89 89 self._remote.create_repository(settings.SVN_COMPATIBLE_VERSION)
90 90 if src_url:
91 91 src_url = _sanitize_url(src_url)
92 92 self._remote.import_remote_repository(src_url)
93 93 else:
94 94 self._check_path()
95 95
96 96 @LazyProperty
97 97 def commit_ids(self):
98 98 head = self._remote.lookup(None)
99 99 return [str(r) for r in xrange(1, head + 1)]
100 100
101 101 @LazyProperty
102 102 def branches(self):
103 103 return self._tags_or_branches('vcs_svn_branch')
104 104
105 105 @LazyProperty
106 106 def branches_closed(self):
107 107 return {}
108 108
109 109 @LazyProperty
110 110 def branches_all(self):
111 111 # TODO: johbo: Implement proper branch support
112 112 all_branches = {}
113 113 all_branches.update(self.branches)
114 114 all_branches.update(self.branches_closed)
115 115 return all_branches
116 116
117 117 @LazyProperty
118 118 def tags(self):
119 119 return self._tags_or_branches('vcs_svn_tag')
120 120
121 121 def _tags_or_branches(self, config_section):
122 122 found_items = {}
123 123
124 124 if self.is_empty():
125 125 return {}
126 126
127 127 for pattern in self._patterns_from_section(config_section):
128 128 pattern = vcspath.sanitize(pattern)
129 129 tip = self.get_commit()
130 130 try:
131 131 if pattern.endswith('*'):
132 132 basedir = tip.get_node(vcspath.dirname(pattern))
133 133 directories = basedir.dirs
134 134 else:
135 135 directories = (tip.get_node(pattern), )
136 136 except NodeDoesNotExistError:
137 137 continue
138 138 found_items.update(
139 139 (safe_unicode(n.path),
140 140 self.commit_ids[-1])
141 141 for n in directories)
142 142
143 143 def get_name(item):
144 144 return item[0]
145 145
146 146 return OrderedDict(sorted(found_items.items(), key=get_name))
147 147
148 148 def _patterns_from_section(self, section):
149 149 return (pattern for key, pattern in self.config.items(section))
150 150
151 151 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
152 152 if self != repo2:
153 153 raise ValueError(
154 154 "Subversion does not support getting common ancestor of"
155 155 " different repositories.")
156 156
157 157 if int(commit_id1) < int(commit_id2):
158 158 return commit_id1
159 159 return commit_id2
160 160
161 161 def verify(self):
162 162 verify = self._remote.verify()
163 163
164 164 self._remote.invalidate_vcs_cache()
165 165 return verify
166 166
167 167 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
168 168 # TODO: johbo: Implement better comparison, this is a very naive
169 169 # version which does not allow to compare branches, tags or folders
170 170 # at all.
171 171 if repo2 != self:
172 172 raise ValueError(
173 173 "Subversion does not support comparison of of different "
174 174 "repositories.")
175 175
176 176 if commit_id1 == commit_id2:
177 177 return []
178 178
179 179 commit_idx1 = self._get_commit_idx(commit_id1)
180 180 commit_idx2 = self._get_commit_idx(commit_id2)
181 181
182 182 commits = [
183 183 self.get_commit(commit_idx=idx)
184 184 for idx in range(commit_idx1 + 1, commit_idx2 + 1)]
185 185
186 186 return commits
187 187
188 188 def _get_commit_idx(self, commit_id):
189 189 try:
190 190 svn_rev = int(commit_id)
191 191 except:
192 192 # TODO: johbo: this might be only one case, HEAD, check this
193 193 svn_rev = self._remote.lookup(commit_id)
194 194 commit_idx = svn_rev - 1
195 195 if commit_idx >= len(self.commit_ids):
196 196 raise CommitDoesNotExistError(
197 197 "Commit at index %s does not exist." % (commit_idx, ))
198 198 return commit_idx
199 199
200 200 @staticmethod
201 201 def check_url(url, config):
202 202 """
203 203 Check if `url` is a valid source to import a Subversion repository.
204 204 """
205 205 # convert to URL if it's a local directory
206 206 if os.path.isdir(url):
207 207 url = 'file://' + urllib.pathname2url(url)
208 208 return connection.Svn.check_url(url, config.serialize())
209 209
210 210 @staticmethod
211 211 def is_valid_repository(path):
212 212 try:
213 213 SubversionRepository(path)
214 214 return True
215 215 except VCSError:
216 216 pass
217 217 return False
218 218
219 219 def _check_path(self):
220 220 if not os.path.exists(self.path):
221 221 raise VCSError('Path "%s" does not exist!' % (self.path, ))
222 222 if not self._remote.is_path_valid_repository(self.path):
223 223 raise VCSError(
224 224 'Path "%s" does not contain a Subversion repository' %
225 225 (self.path, ))
226 226
227 227 @LazyProperty
228 228 def last_change(self):
229 229 """
230 230 Returns last change made on this repository as
231 231 `datetime.datetime` object.
232 232 """
233 233 # Subversion always has a first commit which has id "0" and contains
234 234 # what we are looking for.
235 235 last_id = len(self.commit_ids)
236 236 properties = self._remote.revision_properties(last_id)
237 237 return _date_from_svn_properties(properties)
238 238
239 239 @LazyProperty
240 240 def in_memory_commit(self):
241 241 return SubversionInMemoryCommit(self)
242 242
243 243 def get_hook_location(self):
244 244 """
245 245 returns absolute path to location where hooks are stored
246 246 """
247 247 return os.path.join(self.path, 'hooks')
248 248
249 249 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
250 250 if self.is_empty():
251 251 raise EmptyRepositoryError("There are no commits yet")
252 252 if commit_id is not None:
253 253 self._validate_commit_id(commit_id)
254 254 elif commit_idx is not None:
255 255 self._validate_commit_idx(commit_idx)
256 256 try:
257 257 commit_id = self.commit_ids[commit_idx]
258 258 except IndexError:
259 259 raise CommitDoesNotExistError
260 260
261 261 commit_id = self._sanitize_commit_id(commit_id)
262 262 commit = SubversionCommit(repository=self, commit_id=commit_id)
263 263 return commit
264 264
265 265 def get_commits(
266 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 268 if self.is_empty():
269 269 raise EmptyRepositoryError("There are no commit_ids yet")
270 270 self._validate_branch_name(branch_name)
271 271
272 272 if start_id is not None:
273 273 self._validate_commit_id(start_id)
274 274 if end_id is not None:
275 275 self._validate_commit_id(end_id)
276 276
277 277 start_raw_id = self._sanitize_commit_id(start_id)
278 278 start_pos = self.commit_ids.index(start_raw_id) if start_id else None
279 279 end_raw_id = self._sanitize_commit_id(end_id)
280 280 end_pos = max(0, self.commit_ids.index(end_raw_id)) if end_id else None
281 281
282 282 if None not in [start_id, end_id] and start_pos > end_pos:
283 283 raise RepositoryError(
284 284 "Start commit '%s' cannot be after end commit '%s'" %
285 285 (start_id, end_id))
286 286 if end_pos is not None:
287 287 end_pos += 1
288 288
289 289 # Date based filtering
290 290 if start_date or end_date:
291 291 start_raw_id, end_raw_id = self._remote.lookup_interval(
292 292 date_astimestamp(start_date) if start_date else None,
293 293 date_astimestamp(end_date) if end_date else None)
294 294 start_pos = start_raw_id - 1
295 295 end_pos = end_raw_id
296 296
297 297 commit_ids = self.commit_ids
298 298
299 299 # TODO: johbo: Reconsider impact of DEFAULT_BRANCH_NAME here
300 300 if branch_name not in [None, self.DEFAULT_BRANCH_NAME]:
301 301 svn_rev = long(self.commit_ids[-1])
302 302 commit_ids = self._remote.node_history(
303 303 path=branch_name, revision=svn_rev, limit=None)
304 304 commit_ids = [str(i) for i in reversed(commit_ids)]
305 305
306 306 if start_pos or end_pos:
307 307 commit_ids = commit_ids[start_pos:end_pos]
308 308 return base.CollectionGenerator(self, commit_ids, pre_load=pre_load)
309 309
310 310 def _sanitize_commit_id(self, commit_id):
311 311 if commit_id and commit_id.isdigit():
312 312 if int(commit_id) <= len(self.commit_ids):
313 313 return commit_id
314 314 else:
315 315 raise CommitDoesNotExistError(
316 316 "Commit %s does not exist." % (commit_id, ))
317 317 if commit_id not in [
318 318 None, 'HEAD', 'tip', self.DEFAULT_BRANCH_NAME]:
319 319 raise CommitDoesNotExistError(
320 320 "Commit id %s not understood." % (commit_id, ))
321 321 svn_rev = self._remote.lookup('HEAD')
322 322 return str(svn_rev)
323 323
324 324 def get_diff(
325 325 self, commit1, commit2, path=None, ignore_whitespace=False,
326 326 context=3, path1=None):
327 327 self._validate_diff_commits(commit1, commit2)
328 328 svn_rev1 = long(commit1.raw_id)
329 329 svn_rev2 = long(commit2.raw_id)
330 330 diff = self._remote.diff(
331 331 svn_rev1, svn_rev2, path1=path1, path2=path,
332 332 ignore_whitespace=ignore_whitespace, context=context)
333 333 return SubversionDiff(diff)
334 334
335 335
336 336 def _sanitize_url(url):
337 337 if '://' not in url:
338 338 url = 'file://' + urllib.pathname2url(url)
339 339 return url
@@ -1,2403 +1,2408 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'readme-box';
15 15 @import 'progress-bar';
16 16
17 17 @import 'type';
18 18 @import 'alerts';
19 19 @import 'buttons';
20 20 @import 'tags';
21 21 @import 'code-block';
22 22 @import 'examples';
23 23 @import 'login';
24 24 @import 'main-content';
25 25 @import 'select2';
26 26 @import 'comments';
27 27 @import 'panels-bootstrap';
28 28 @import 'panels';
29 29 @import 'deform';
30 30
31 31 //--- BASE ------------------//
32 32 .noscript-error {
33 33 top: 0;
34 34 left: 0;
35 35 width: 100%;
36 36 z-index: 101;
37 37 text-align: center;
38 38 font-family: @text-semibold;
39 39 font-size: 120%;
40 40 color: white;
41 41 background-color: @alert2;
42 42 padding: 5px 0 5px 0;
43 43 }
44 44
45 45 html {
46 46 display: table;
47 47 height: 100%;
48 48 width: 100%;
49 49 }
50 50
51 51 body {
52 52 display: table-cell;
53 53 width: 100%;
54 54 }
55 55
56 56 //--- LAYOUT ------------------//
57 57
58 58 .hidden{
59 59 display: none !important;
60 60 }
61 61
62 62 .box{
63 63 float: left;
64 64 width: 100%;
65 65 }
66 66
67 67 .browser-header {
68 68 clear: both;
69 69 }
70 70 .main {
71 71 clear: both;
72 72 padding:0 0 @pagepadding;
73 73 height: auto;
74 74
75 75 &:after { //clearfix
76 76 content:"";
77 77 clear:both;
78 78 width:100%;
79 79 display:block;
80 80 }
81 81 }
82 82
83 83 .action-link{
84 84 margin-left: @padding;
85 85 padding-left: @padding;
86 86 border-left: @border-thickness solid @border-default-color;
87 87 }
88 88
89 89 input + .action-link, .action-link.first{
90 90 border-left: none;
91 91 }
92 92
93 93 .action-link.last{
94 94 margin-right: @padding;
95 95 padding-right: @padding;
96 96 }
97 97
98 98 .action-link.active,
99 99 .action-link.active a{
100 100 color: @grey4;
101 101 }
102 102
103 .action-link.disabled {
104 color: @grey4;
105 cursor: inherit;
106 }
107
103 108 .clipboard-action {
104 109 cursor: pointer;
105 110 }
106 111
107 112 ul.simple-list{
108 113 list-style: none;
109 114 margin: 0;
110 115 padding: 0;
111 116 }
112 117
113 118 .main-content {
114 119 padding-bottom: @pagepadding;
115 120 }
116 121
117 122 .wide-mode-wrapper {
118 123 max-width:4000px !important;
119 124 }
120 125
121 126 .wrapper {
122 127 position: relative;
123 128 max-width: @wrapper-maxwidth;
124 129 margin: 0 auto;
125 130 }
126 131
127 132 #content {
128 133 clear: both;
129 134 padding: 0 @contentpadding;
130 135 }
131 136
132 137 .advanced-settings-fields{
133 138 input{
134 139 margin-left: @textmargin;
135 140 margin-right: @padding/2;
136 141 }
137 142 }
138 143
139 144 .cs_files_title {
140 145 margin: @pagepadding 0 0;
141 146 }
142 147
143 148 input.inline[type="file"] {
144 149 display: inline;
145 150 }
146 151
147 152 .error_page {
148 153 margin: 10% auto;
149 154
150 155 h1 {
151 156 color: @grey2;
152 157 }
153 158
154 159 .alert {
155 160 margin: @padding 0;
156 161 }
157 162
158 163 .error-branding {
159 164 font-family: @text-semibold;
160 165 color: @grey4;
161 166 }
162 167
163 168 .error_message {
164 169 font-family: @text-regular;
165 170 }
166 171
167 172 .sidebar {
168 173 min-height: 275px;
169 174 margin: 0;
170 175 padding: 0 0 @sidebarpadding @sidebarpadding;
171 176 border: none;
172 177 }
173 178
174 179 .main-content {
175 180 position: relative;
176 181 margin: 0 @sidebarpadding @sidebarpadding;
177 182 padding: 0 0 0 @sidebarpadding;
178 183 border-left: @border-thickness solid @grey5;
179 184
180 185 @media (max-width:767px) {
181 186 clear: both;
182 187 width: 100%;
183 188 margin: 0;
184 189 border: none;
185 190 }
186 191 }
187 192
188 193 .inner-column {
189 194 float: left;
190 195 width: 29.75%;
191 196 min-height: 150px;
192 197 margin: @sidebarpadding 2% 0 0;
193 198 padding: 0 2% 0 0;
194 199 border-right: @border-thickness solid @grey5;
195 200
196 201 @media (max-width:767px) {
197 202 clear: both;
198 203 width: 100%;
199 204 border: none;
200 205 }
201 206
202 207 ul {
203 208 padding-left: 1.25em;
204 209 }
205 210
206 211 &:last-child {
207 212 margin: @sidebarpadding 0 0;
208 213 border: none;
209 214 }
210 215
211 216 h4 {
212 217 margin: 0 0 @padding;
213 218 font-family: @text-semibold;
214 219 }
215 220 }
216 221 }
217 222 .error-page-logo {
218 223 width: 130px;
219 224 height: 160px;
220 225 }
221 226
222 227 // HEADER
223 228 .header {
224 229
225 230 // TODO: johbo: Fix login pages, so that they work without a min-height
226 231 // for the header and then remove the min-height. I chose a smaller value
227 232 // intentionally here to avoid rendering issues in the main navigation.
228 233 min-height: 49px;
229 234
230 235 position: relative;
231 236 vertical-align: bottom;
232 237 padding: 0 @header-padding;
233 238 background-color: @grey2;
234 239 color: @grey5;
235 240
236 241 .title {
237 242 overflow: visible;
238 243 }
239 244
240 245 &:before,
241 246 &:after {
242 247 content: "";
243 248 clear: both;
244 249 width: 100%;
245 250 }
246 251
247 252 // TODO: johbo: Avoids breaking "Repositories" chooser
248 253 .select2-container .select2-choice .select2-arrow {
249 254 display: none;
250 255 }
251 256 }
252 257
253 258 #header-inner {
254 259 &.title {
255 260 margin: 0;
256 261 }
257 262 &:before,
258 263 &:after {
259 264 content: "";
260 265 clear: both;
261 266 }
262 267 }
263 268
264 269 // Gists
265 270 #files_data {
266 271 clear: both; //for firefox
267 272 }
268 273 #gistid {
269 274 margin-right: @padding;
270 275 }
271 276
272 277 // Global Settings Editor
273 278 .textarea.editor {
274 279 float: left;
275 280 position: relative;
276 281 max-width: @texteditor-width;
277 282
278 283 select {
279 284 position: absolute;
280 285 top:10px;
281 286 right:0;
282 287 }
283 288
284 289 .CodeMirror {
285 290 margin: 0;
286 291 }
287 292
288 293 .help-block {
289 294 margin: 0 0 @padding;
290 295 padding:.5em;
291 296 background-color: @grey6;
292 297 &.pre-formatting {
293 298 white-space: pre;
294 299 }
295 300 }
296 301 }
297 302
298 303 ul.auth_plugins {
299 304 margin: @padding 0 @padding @legend-width;
300 305 padding: 0;
301 306
302 307 li {
303 308 margin-bottom: @padding;
304 309 line-height: 1em;
305 310 list-style-type: none;
306 311
307 312 .auth_buttons .btn {
308 313 margin-right: @padding;
309 314 }
310 315
311 316 &:before { content: none; }
312 317 }
313 318 }
314 319
315 320
316 321 // My Account PR list
317 322
318 323 #show_closed {
319 324 margin: 0 1em 0 0;
320 325 }
321 326
322 327 .pullrequestlist {
323 328 .closed {
324 329 background-color: @grey6;
325 330 }
326 331 .td-status {
327 332 padding-left: .5em;
328 333 }
329 334 .log-container .truncate {
330 335 height: 2.75em;
331 336 white-space: pre-line;
332 337 }
333 338 table.rctable .user {
334 339 padding-left: 0;
335 340 }
336 341 table.rctable {
337 342 td.td-description,
338 343 .rc-user {
339 344 min-width: auto;
340 345 }
341 346 }
342 347 }
343 348
344 349 // Pull Requests
345 350
346 351 .pullrequests_section_head {
347 352 display: block;
348 353 clear: both;
349 354 margin: @padding 0;
350 355 font-family: @text-bold;
351 356 }
352 357
353 358 .pr-origininfo, .pr-targetinfo {
354 359 position: relative;
355 360
356 361 .tag {
357 362 display: inline-block;
358 363 margin: 0 1em .5em 0;
359 364 }
360 365
361 366 .clone-url {
362 367 display: inline-block;
363 368 margin: 0 0 .5em 0;
364 369 padding: 0;
365 370 line-height: 1.2em;
366 371 }
367 372 }
368 373
369 374 .pr-mergeinfo {
370 375 min-width: 95% !important;
371 376 padding: 0 !important;
372 377 border: 0;
373 378 }
374 379 .pr-mergeinfo-copy {
375 380 padding: 0 0;
376 381 }
377 382
378 383 .pr-pullinfo {
379 384 min-width: 95% !important;
380 385 padding: 0 !important;
381 386 border: 0;
382 387 }
383 388 .pr-pullinfo-copy {
384 389 padding: 0 0;
385 390 }
386 391
387 392
388 393 #pr-title-input {
389 394 width: 72%;
390 395 font-size: 1em;
391 396 font-family: @text-bold;
392 397 margin: 0;
393 398 padding: 0 0 0 @padding/4;
394 399 line-height: 1.7em;
395 400 color: @text-color;
396 401 letter-spacing: .02em;
397 402 }
398 403
399 404 #pullrequest_title {
400 405 width: 100%;
401 406 box-sizing: border-box;
402 407 }
403 408
404 409 #pr_open_message {
405 410 border: @border-thickness solid #fff;
406 411 border-radius: @border-radius;
407 412 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
408 413 text-align: left;
409 414 overflow: hidden;
410 415 }
411 416
412 417 .pr-submit-button {
413 418 float: right;
414 419 margin: 0 0 0 5px;
415 420 }
416 421
417 422 .pr-spacing-container {
418 423 padding: 20px;
419 424 clear: both
420 425 }
421 426
422 427 #pr-description-input {
423 428 margin-bottom: 0;
424 429 }
425 430
426 431 .pr-description-label {
427 432 vertical-align: top;
428 433 }
429 434
430 435 .perms_section_head {
431 436 min-width: 625px;
432 437
433 438 h2 {
434 439 margin-bottom: 0;
435 440 }
436 441
437 442 .label-checkbox {
438 443 float: left;
439 444 }
440 445
441 446 &.field {
442 447 margin: @space 0 @padding;
443 448 }
444 449
445 450 &:first-child.field {
446 451 margin-top: 0;
447 452
448 453 .label {
449 454 margin-top: 0;
450 455 padding-top: 0;
451 456 }
452 457
453 458 .radios {
454 459 padding-top: 0;
455 460 }
456 461 }
457 462
458 463 .radios {
459 464 position: relative;
460 465 width: 405px;
461 466 }
462 467 }
463 468
464 469 //--- MODULES ------------------//
465 470
466 471
467 472 // Server Announcement
468 473 #server-announcement {
469 474 width: 95%;
470 475 margin: @padding auto;
471 476 padding: @padding;
472 477 border-width: 2px;
473 478 border-style: solid;
474 479 .border-radius(2px);
475 480 font-family: @text-bold;
476 481
477 482 &.info { border-color: @alert4; background-color: @alert4-inner; }
478 483 &.warning { border-color: @alert3; background-color: @alert3-inner; }
479 484 &.error { border-color: @alert2; background-color: @alert2-inner; }
480 485 &.success { border-color: @alert1; background-color: @alert1-inner; }
481 486 &.neutral { border-color: @grey3; background-color: @grey6; }
482 487 }
483 488
484 489 // Fixed Sidebar Column
485 490 .sidebar-col-wrapper {
486 491 padding-left: @sidebar-all-width;
487 492
488 493 .sidebar {
489 494 width: @sidebar-width;
490 495 margin-left: -@sidebar-all-width;
491 496 }
492 497 }
493 498
494 499 .sidebar-col-wrapper.scw-small {
495 500 padding-left: @sidebar-small-all-width;
496 501
497 502 .sidebar {
498 503 width: @sidebar-small-width;
499 504 margin-left: -@sidebar-small-all-width;
500 505 }
501 506 }
502 507
503 508
504 509 // FOOTER
505 510 #footer {
506 511 padding: 0;
507 512 text-align: center;
508 513 vertical-align: middle;
509 514 color: @grey2;
510 515 background-color: @grey6;
511 516
512 517 p {
513 518 margin: 0;
514 519 padding: 1em;
515 520 line-height: 1em;
516 521 }
517 522
518 523 .server-instance { //server instance
519 524 display: none;
520 525 }
521 526
522 527 .title {
523 528 float: none;
524 529 margin: 0 auto;
525 530 }
526 531 }
527 532
528 533 button.close {
529 534 padding: 0;
530 535 cursor: pointer;
531 536 background: transparent;
532 537 border: 0;
533 538 .box-shadow(none);
534 539 -webkit-appearance: none;
535 540 }
536 541
537 542 .close {
538 543 float: right;
539 544 font-size: 21px;
540 545 font-family: @text-bootstrap;
541 546 line-height: 1em;
542 547 font-weight: bold;
543 548 color: @grey2;
544 549
545 550 &:hover,
546 551 &:focus {
547 552 color: @grey1;
548 553 text-decoration: none;
549 554 cursor: pointer;
550 555 }
551 556 }
552 557
553 558 // GRID
554 559 .sorting,
555 560 .sorting_desc,
556 561 .sorting_asc {
557 562 cursor: pointer;
558 563 }
559 564 .sorting_desc:after {
560 565 content: "\00A0\25B2";
561 566 font-size: .75em;
562 567 }
563 568 .sorting_asc:after {
564 569 content: "\00A0\25BC";
565 570 font-size: .68em;
566 571 }
567 572
568 573
569 574 .user_auth_tokens {
570 575
571 576 &.truncate {
572 577 white-space: nowrap;
573 578 overflow: hidden;
574 579 text-overflow: ellipsis;
575 580 }
576 581
577 582 .fields .field .input {
578 583 margin: 0;
579 584 }
580 585
581 586 input#description {
582 587 width: 100px;
583 588 margin: 0;
584 589 }
585 590
586 591 .drop-menu {
587 592 // TODO: johbo: Remove this, should work out of the box when
588 593 // having multiple inputs inline
589 594 margin: 0 0 0 5px;
590 595 }
591 596 }
592 597 #user_list_table {
593 598 .closed {
594 599 background-color: @grey6;
595 600 }
596 601 }
597 602
598 603
599 604 input {
600 605 &.disabled {
601 606 opacity: .5;
602 607 }
603 608 }
604 609
605 610 // remove extra padding in firefox
606 611 input::-moz-focus-inner { border:0; padding:0 }
607 612
608 613 .adjacent input {
609 614 margin-bottom: @padding;
610 615 }
611 616
612 617 .permissions_boxes {
613 618 display: block;
614 619 }
615 620
616 621 //TODO: lisa: this should be in tables
617 622 .show_more_col {
618 623 width: 20px;
619 624 }
620 625
621 626 //FORMS
622 627
623 628 .medium-inline,
624 629 input#description.medium-inline {
625 630 display: inline;
626 631 width: @medium-inline-input-width;
627 632 min-width: 100px;
628 633 }
629 634
630 635 select {
631 636 //reset
632 637 -webkit-appearance: none;
633 638 -moz-appearance: none;
634 639
635 640 display: inline-block;
636 641 height: 28px;
637 642 width: auto;
638 643 margin: 0 @padding @padding 0;
639 644 padding: 0 18px 0 8px;
640 645 line-height:1em;
641 646 font-size: @basefontsize;
642 647 border: @border-thickness solid @rcblue;
643 648 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
644 649 color: @rcblue;
645 650
646 651 &:after {
647 652 content: "\00A0\25BE";
648 653 }
649 654
650 655 &:focus {
651 656 outline: none;
652 657 }
653 658 }
654 659
655 660 option {
656 661 &:focus {
657 662 outline: none;
658 663 }
659 664 }
660 665
661 666 input,
662 667 textarea {
663 668 padding: @input-padding;
664 669 border: @input-border-thickness solid @border-highlight-color;
665 670 .border-radius (@border-radius);
666 671 font-family: @text-light;
667 672 font-size: @basefontsize;
668 673
669 674 &.input-sm {
670 675 padding: 5px;
671 676 }
672 677
673 678 &#description {
674 679 min-width: @input-description-minwidth;
675 680 min-height: 1em;
676 681 padding: 10px;
677 682 }
678 683 }
679 684
680 685 .field-sm {
681 686 input,
682 687 textarea {
683 688 padding: 5px;
684 689 }
685 690 }
686 691
687 692 textarea {
688 693 display: block;
689 694 clear: both;
690 695 width: 100%;
691 696 min-height: 100px;
692 697 margin-bottom: @padding;
693 698 .box-sizing(border-box);
694 699 overflow: auto;
695 700 }
696 701
697 702 label {
698 703 font-family: @text-light;
699 704 }
700 705
701 706 // GRAVATARS
702 707 // centers gravatar on username to the right
703 708
704 709 .gravatar {
705 710 display: inline;
706 711 min-width: 16px;
707 712 min-height: 16px;
708 713 margin: -5px 0;
709 714 padding: 0;
710 715 line-height: 1em;
711 716 border: 1px solid @grey4;
712 717 box-sizing: content-box;
713 718
714 719 &.gravatar-large {
715 720 margin: -0.5em .25em -0.5em 0;
716 721 }
717 722
718 723 & + .user {
719 724 display: inline;
720 725 margin: 0;
721 726 padding: 0 0 0 .17em;
722 727 line-height: 1em;
723 728 }
724 729 }
725 730
726 731 .user-inline-data {
727 732 display: inline-block;
728 733 float: left;
729 734 padding-left: .5em;
730 735 line-height: 1.3em;
731 736 }
732 737
733 738 .rc-user { // gravatar + user wrapper
734 739 float: left;
735 740 position: relative;
736 741 min-width: 100px;
737 742 max-width: 200px;
738 743 min-height: (@gravatar-size + @border-thickness * 2); // account for border
739 744 display: block;
740 745 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
741 746
742 747
743 748 .gravatar {
744 749 display: block;
745 750 position: absolute;
746 751 top: 0;
747 752 left: 0;
748 753 min-width: @gravatar-size;
749 754 min-height: @gravatar-size;
750 755 margin: 0;
751 756 }
752 757
753 758 .user {
754 759 display: block;
755 760 max-width: 175px;
756 761 padding-top: 2px;
757 762 overflow: hidden;
758 763 text-overflow: ellipsis;
759 764 }
760 765 }
761 766
762 767 .gist-gravatar,
763 768 .journal_container {
764 769 .gravatar-large {
765 770 margin: 0 .5em -10px 0;
766 771 }
767 772 }
768 773
769 774
770 775 // ADMIN SETTINGS
771 776
772 777 // Tag Patterns
773 778 .tag_patterns {
774 779 .tag_input {
775 780 margin-bottom: @padding;
776 781 }
777 782 }
778 783
779 784 .locked_input {
780 785 position: relative;
781 786
782 787 input {
783 788 display: inline;
784 789 margin: 3px 5px 0px 0px;
785 790 }
786 791
787 792 br {
788 793 display: none;
789 794 }
790 795
791 796 .error-message {
792 797 float: left;
793 798 width: 100%;
794 799 }
795 800
796 801 .lock_input_button {
797 802 display: inline;
798 803 }
799 804
800 805 .help-block {
801 806 clear: both;
802 807 }
803 808 }
804 809
805 810 // Notifications
806 811
807 812 .notifications_buttons {
808 813 margin: 0 0 @space 0;
809 814 padding: 0;
810 815
811 816 .btn {
812 817 display: inline-block;
813 818 }
814 819 }
815 820
816 821 .notification-list {
817 822
818 823 div {
819 824 display: inline-block;
820 825 vertical-align: middle;
821 826 }
822 827
823 828 .container {
824 829 display: block;
825 830 margin: 0 0 @padding 0;
826 831 }
827 832
828 833 .delete-notifications {
829 834 margin-left: @padding;
830 835 text-align: right;
831 836 cursor: pointer;
832 837 }
833 838
834 839 .read-notifications {
835 840 margin-left: @padding/2;
836 841 text-align: right;
837 842 width: 35px;
838 843 cursor: pointer;
839 844 }
840 845
841 846 .icon-minus-sign {
842 847 color: @alert2;
843 848 }
844 849
845 850 .icon-ok-sign {
846 851 color: @alert1;
847 852 }
848 853 }
849 854
850 855 .user_settings {
851 856 float: left;
852 857 clear: both;
853 858 display: block;
854 859 width: 100%;
855 860
856 861 .gravatar_box {
857 862 margin-bottom: @padding;
858 863
859 864 &:after {
860 865 content: " ";
861 866 clear: both;
862 867 width: 100%;
863 868 }
864 869 }
865 870
866 871 .fields .field {
867 872 clear: both;
868 873 }
869 874 }
870 875
871 876 .advanced_settings {
872 877 margin-bottom: @space;
873 878
874 879 .help-block {
875 880 margin-left: 0;
876 881 }
877 882
878 883 button + .help-block {
879 884 margin-top: @padding;
880 885 }
881 886 }
882 887
883 888 // admin settings radio buttons and labels
884 889 .label-2 {
885 890 float: left;
886 891 width: @label2-width;
887 892
888 893 label {
889 894 color: @grey1;
890 895 }
891 896 }
892 897 .checkboxes {
893 898 float: left;
894 899 width: @checkboxes-width;
895 900 margin-bottom: @padding;
896 901
897 902 .checkbox {
898 903 width: 100%;
899 904
900 905 label {
901 906 margin: 0;
902 907 padding: 0;
903 908 }
904 909 }
905 910
906 911 .checkbox + .checkbox {
907 912 display: inline-block;
908 913 }
909 914
910 915 label {
911 916 margin-right: 1em;
912 917 }
913 918 }
914 919
915 920 // CHANGELOG
916 921 .container_header {
917 922 float: left;
918 923 display: block;
919 924 width: 100%;
920 925 margin: @padding 0 @padding;
921 926
922 927 #filter_changelog {
923 928 float: left;
924 929 margin-right: @padding;
925 930 }
926 931
927 932 .breadcrumbs_light {
928 933 display: inline-block;
929 934 }
930 935 }
931 936
932 937 .info_box {
933 938 float: right;
934 939 }
935 940
936 941
937 942 #graph_nodes {
938 943 padding-top: 43px;
939 944 }
940 945
941 946 #graph_content{
942 947
943 948 // adjust for table headers so that graph renders properly
944 949 // #graph_nodes padding - table cell padding
945 950 padding-top: (@space - (@basefontsize * 2.4));
946 951
947 952 &.graph_full_width {
948 953 width: 100%;
949 954 max-width: 100%;
950 955 }
951 956 }
952 957
953 958 #graph {
954 959 .flag_status {
955 960 margin: 0;
956 961 }
957 962
958 963 .pagination-left {
959 964 float: left;
960 965 clear: both;
961 966 }
962 967
963 968 .log-container {
964 969 max-width: 345px;
965 970
966 971 .message{
967 972 max-width: 340px;
968 973 }
969 974 }
970 975
971 976 .graph-col-wrapper {
972 977 padding-left: 110px;
973 978
974 979 #graph_nodes {
975 980 width: 100px;
976 981 margin-left: -110px;
977 982 float: left;
978 983 clear: left;
979 984 }
980 985 }
981 986
982 987 .load-more-commits {
983 988 text-align: center;
984 989 }
985 990 .load-more-commits:hover {
986 991 background-color: @grey7;
987 992 }
988 993 .load-more-commits {
989 994 a {
990 995 display: block;
991 996 }
992 997 }
993 998 }
994 999
995 1000 #filter_changelog {
996 1001 float: left;
997 1002 }
998 1003
999 1004
1000 1005 //--- THEME ------------------//
1001 1006
1002 1007 #logo {
1003 1008 float: left;
1004 1009 margin: 9px 0 0 0;
1005 1010
1006 1011 .header {
1007 1012 background-color: transparent;
1008 1013 }
1009 1014
1010 1015 a {
1011 1016 display: inline-block;
1012 1017 }
1013 1018
1014 1019 img {
1015 1020 height:30px;
1016 1021 }
1017 1022 }
1018 1023
1019 1024 .logo-wrapper {
1020 1025 float:left;
1021 1026 }
1022 1027
1023 1028 .branding{
1024 1029 float: left;
1025 1030 padding: 9px 2px;
1026 1031 line-height: 1em;
1027 1032 font-size: @navigation-fontsize;
1028 1033 }
1029 1034
1030 1035 img {
1031 1036 border: none;
1032 1037 outline: none;
1033 1038 }
1034 1039 user-profile-header
1035 1040 label {
1036 1041
1037 1042 input[type="checkbox"] {
1038 1043 margin-right: 1em;
1039 1044 }
1040 1045 input[type="radio"] {
1041 1046 margin-right: 1em;
1042 1047 }
1043 1048 }
1044 1049
1045 1050 .flag_status {
1046 1051 margin: 2px 8px 6px 2px;
1047 1052 &.under_review {
1048 1053 .circle(5px, @alert3);
1049 1054 }
1050 1055 &.approved {
1051 1056 .circle(5px, @alert1);
1052 1057 }
1053 1058 &.rejected,
1054 1059 &.forced_closed{
1055 1060 .circle(5px, @alert2);
1056 1061 }
1057 1062 &.not_reviewed {
1058 1063 .circle(5px, @grey5);
1059 1064 }
1060 1065 }
1061 1066
1062 1067 .flag_status_comment_box {
1063 1068 margin: 5px 6px 0px 2px;
1064 1069 }
1065 1070 .test_pattern_preview {
1066 1071 margin: @space 0;
1067 1072
1068 1073 p {
1069 1074 margin-bottom: 0;
1070 1075 border-bottom: @border-thickness solid @border-default-color;
1071 1076 color: @grey3;
1072 1077 }
1073 1078
1074 1079 .btn {
1075 1080 margin-bottom: @padding;
1076 1081 }
1077 1082 }
1078 1083 #test_pattern_result {
1079 1084 display: none;
1080 1085 &:extend(pre);
1081 1086 padding: .9em;
1082 1087 color: @grey3;
1083 1088 background-color: @grey7;
1084 1089 border-right: @border-thickness solid @border-default-color;
1085 1090 border-bottom: @border-thickness solid @border-default-color;
1086 1091 border-left: @border-thickness solid @border-default-color;
1087 1092 }
1088 1093
1089 1094 #repo_vcs_settings {
1090 1095 #inherit_overlay_vcs_default {
1091 1096 display: none;
1092 1097 }
1093 1098 #inherit_overlay_vcs_custom {
1094 1099 display: custom;
1095 1100 }
1096 1101 &.inherited {
1097 1102 #inherit_overlay_vcs_default {
1098 1103 display: block;
1099 1104 }
1100 1105 #inherit_overlay_vcs_custom {
1101 1106 display: none;
1102 1107 }
1103 1108 }
1104 1109 }
1105 1110
1106 1111 .issue-tracker-link {
1107 1112 color: @rcblue;
1108 1113 }
1109 1114
1110 1115 // Issue Tracker Table Show/Hide
1111 1116 #repo_issue_tracker {
1112 1117 #inherit_overlay {
1113 1118 display: none;
1114 1119 }
1115 1120 #custom_overlay {
1116 1121 display: custom;
1117 1122 }
1118 1123 &.inherited {
1119 1124 #inherit_overlay {
1120 1125 display: block;
1121 1126 }
1122 1127 #custom_overlay {
1123 1128 display: none;
1124 1129 }
1125 1130 }
1126 1131 }
1127 1132 table.issuetracker {
1128 1133 &.readonly {
1129 1134 tr, td {
1130 1135 color: @grey3;
1131 1136 }
1132 1137 }
1133 1138 .edit {
1134 1139 display: none;
1135 1140 }
1136 1141 .editopen {
1137 1142 .edit {
1138 1143 display: inline;
1139 1144 }
1140 1145 .entry {
1141 1146 display: none;
1142 1147 }
1143 1148 }
1144 1149 tr td.td-action {
1145 1150 min-width: 117px;
1146 1151 }
1147 1152 td input {
1148 1153 max-width: none;
1149 1154 min-width: 30px;
1150 1155 width: 80%;
1151 1156 }
1152 1157 .issuetracker_pref input {
1153 1158 width: 40%;
1154 1159 }
1155 1160 input.edit_issuetracker_update {
1156 1161 margin-right: 0;
1157 1162 width: auto;
1158 1163 }
1159 1164 }
1160 1165
1161 1166 table.integrations {
1162 1167 .td-icon {
1163 1168 width: 20px;
1164 1169 .integration-icon {
1165 1170 height: 20px;
1166 1171 width: 20px;
1167 1172 }
1168 1173 }
1169 1174 }
1170 1175
1171 1176 .integrations {
1172 1177 a.integration-box {
1173 1178 color: @text-color;
1174 1179 &:hover {
1175 1180 .panel {
1176 1181 background: #fbfbfb;
1177 1182 }
1178 1183 }
1179 1184 .integration-icon {
1180 1185 width: 30px;
1181 1186 height: 30px;
1182 1187 margin-right: 20px;
1183 1188 float: left;
1184 1189 }
1185 1190
1186 1191 .panel-body {
1187 1192 padding: 10px;
1188 1193 }
1189 1194 .panel {
1190 1195 margin-bottom: 10px;
1191 1196 }
1192 1197 h2 {
1193 1198 display: inline-block;
1194 1199 margin: 0;
1195 1200 min-width: 140px;
1196 1201 }
1197 1202 }
1198 1203 a.integration-box.dummy-integration {
1199 1204 color: @grey4
1200 1205 }
1201 1206 }
1202 1207
1203 1208 //Permissions Settings
1204 1209 #add_perm {
1205 1210 margin: 0 0 @padding;
1206 1211 cursor: pointer;
1207 1212 }
1208 1213
1209 1214 .perm_ac {
1210 1215 input {
1211 1216 width: 95%;
1212 1217 }
1213 1218 }
1214 1219
1215 1220 .autocomplete-suggestions {
1216 1221 width: auto !important; // overrides autocomplete.js
1217 1222 margin: 0;
1218 1223 border: @border-thickness solid @rcblue;
1219 1224 border-radius: @border-radius;
1220 1225 color: @rcblue;
1221 1226 background-color: white;
1222 1227 }
1223 1228 .autocomplete-selected {
1224 1229 background: #F0F0F0;
1225 1230 }
1226 1231 .ac-container-wrap {
1227 1232 margin: 0;
1228 1233 padding: 8px;
1229 1234 border-bottom: @border-thickness solid @rclightblue;
1230 1235 list-style-type: none;
1231 1236 cursor: pointer;
1232 1237
1233 1238 &:hover {
1234 1239 background-color: @rclightblue;
1235 1240 }
1236 1241
1237 1242 img {
1238 1243 height: @gravatar-size;
1239 1244 width: @gravatar-size;
1240 1245 margin-right: 1em;
1241 1246 }
1242 1247
1243 1248 strong {
1244 1249 font-weight: normal;
1245 1250 }
1246 1251 }
1247 1252
1248 1253 // Settings Dropdown
1249 1254 .user-menu .container {
1250 1255 padding: 0 4px;
1251 1256 margin: 0;
1252 1257 }
1253 1258
1254 1259 .user-menu .gravatar {
1255 1260 cursor: pointer;
1256 1261 }
1257 1262
1258 1263 .codeblock {
1259 1264 margin-bottom: @padding;
1260 1265 clear: both;
1261 1266
1262 1267 .stats{
1263 1268 overflow: hidden;
1264 1269 }
1265 1270
1266 1271 .message{
1267 1272 textarea{
1268 1273 margin: 0;
1269 1274 }
1270 1275 }
1271 1276
1272 1277 .code-header {
1273 1278 .stats {
1274 1279 line-height: 2em;
1275 1280
1276 1281 .revision_id {
1277 1282 margin-left: 0;
1278 1283 }
1279 1284 .buttons {
1280 1285 padding-right: 0;
1281 1286 }
1282 1287 }
1283 1288
1284 1289 .item{
1285 1290 margin-right: 0.5em;
1286 1291 }
1287 1292 }
1288 1293
1289 1294 #editor_container{
1290 1295 position: relative;
1291 1296 margin: @padding;
1292 1297 }
1293 1298 }
1294 1299
1295 1300 #file_history_container {
1296 1301 display: none;
1297 1302 }
1298 1303
1299 1304 .file-history-inner {
1300 1305 margin-bottom: 10px;
1301 1306 }
1302 1307
1303 1308 // Pull Requests
1304 1309 .summary-details {
1305 1310 width: 72%;
1306 1311 }
1307 1312 .pr-summary {
1308 1313 border-bottom: @border-thickness solid @grey5;
1309 1314 margin-bottom: @space;
1310 1315 }
1311 1316 .reviewers-title {
1312 1317 width: 25%;
1313 1318 min-width: 200px;
1314 1319 }
1315 1320 .reviewers {
1316 1321 width: 25%;
1317 1322 min-width: 200px;
1318 1323 }
1319 1324 .reviewers ul li {
1320 1325 position: relative;
1321 1326 width: 100%;
1322 1327 margin-bottom: 8px;
1323 1328 }
1324 1329
1325 1330 .reviewer_entry {
1326 1331 min-height: 55px;
1327 1332 }
1328 1333
1329 1334 .reviewers_member {
1330 1335 width: 100%;
1331 1336 overflow: auto;
1332 1337 }
1333 1338 .reviewer_reason {
1334 1339 padding-left: 20px;
1335 1340 }
1336 1341 .reviewer_status {
1337 1342 display: inline-block;
1338 1343 vertical-align: top;
1339 1344 width: 7%;
1340 1345 min-width: 20px;
1341 1346 height: 1.2em;
1342 1347 margin-top: 3px;
1343 1348 line-height: 1em;
1344 1349 }
1345 1350
1346 1351 .reviewer_name {
1347 1352 display: inline-block;
1348 1353 max-width: 83%;
1349 1354 padding-right: 20px;
1350 1355 vertical-align: middle;
1351 1356 line-height: 1;
1352 1357
1353 1358 .rc-user {
1354 1359 min-width: 0;
1355 1360 margin: -2px 1em 0 0;
1356 1361 }
1357 1362
1358 1363 .reviewer {
1359 1364 float: left;
1360 1365 }
1361 1366 }
1362 1367
1363 1368 .reviewer_member_mandatory,
1364 1369 .reviewer_member_mandatory_remove,
1365 1370 .reviewer_member_remove {
1366 1371 position: absolute;
1367 1372 right: 0;
1368 1373 top: 0;
1369 1374 width: 16px;
1370 1375 margin-bottom: 10px;
1371 1376 padding: 0;
1372 1377 color: black;
1373 1378 }
1374 1379
1375 1380 .reviewer_member_mandatory_remove {
1376 1381 color: @grey4;
1377 1382 }
1378 1383
1379 1384 .reviewer_member_mandatory {
1380 1385 padding-top:20px;
1381 1386 }
1382 1387
1383 1388 .reviewer_member_status {
1384 1389 margin-top: 5px;
1385 1390 }
1386 1391 .pr-summary #summary{
1387 1392 width: 100%;
1388 1393 }
1389 1394 .pr-summary .action_button:hover {
1390 1395 border: 0;
1391 1396 cursor: pointer;
1392 1397 }
1393 1398 .pr-details-title {
1394 1399 padding-bottom: 8px;
1395 1400 border-bottom: @border-thickness solid @grey5;
1396 1401
1397 1402 .action_button.disabled {
1398 1403 color: @grey4;
1399 1404 cursor: inherit;
1400 1405 }
1401 1406 .action_button {
1402 1407 color: @rcblue;
1403 1408 }
1404 1409 }
1405 1410 .pr-details-content {
1406 1411 margin-top: @textmargin;
1407 1412 margin-bottom: @textmargin;
1408 1413 }
1409 1414 .pr-description {
1410 1415 white-space:pre-wrap;
1411 1416 }
1412 1417
1413 1418 .pr-reviewer-rules {
1414 1419 padding: 10px 0px 20px 0px;
1415 1420 }
1416 1421
1417 1422 .group_members {
1418 1423 margin-top: 0;
1419 1424 padding: 0;
1420 1425 list-style: outside none none;
1421 1426
1422 1427 img {
1423 1428 height: @gravatar-size;
1424 1429 width: @gravatar-size;
1425 1430 margin-right: .5em;
1426 1431 margin-left: 3px;
1427 1432 }
1428 1433
1429 1434 .to-delete {
1430 1435 .user {
1431 1436 text-decoration: line-through;
1432 1437 }
1433 1438 }
1434 1439 }
1435 1440
1436 1441 .compare_view_commits_title {
1437 1442 .disabled {
1438 1443 cursor: inherit;
1439 1444 &:hover{
1440 1445 background-color: inherit;
1441 1446 color: inherit;
1442 1447 }
1443 1448 }
1444 1449 }
1445 1450
1446 1451 .subtitle-compare {
1447 1452 margin: -15px 0px 0px 0px;
1448 1453 }
1449 1454
1450 1455 .comments-summary-td {
1451 1456 border-top: 1px dashed @grey5;
1452 1457 }
1453 1458
1454 1459 // new entry in group_members
1455 1460 .td-author-new-entry {
1456 1461 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1457 1462 }
1458 1463
1459 1464 .usergroup_member_remove {
1460 1465 width: 16px;
1461 1466 margin-bottom: 10px;
1462 1467 padding: 0;
1463 1468 color: black !important;
1464 1469 cursor: pointer;
1465 1470 }
1466 1471
1467 1472 .reviewer_ac .ac-input {
1468 1473 width: 92%;
1469 1474 margin-bottom: 1em;
1470 1475 }
1471 1476
1472 1477 .compare_view_commits tr{
1473 1478 height: 20px;
1474 1479 }
1475 1480 .compare_view_commits td {
1476 1481 vertical-align: top;
1477 1482 padding-top: 10px;
1478 1483 }
1479 1484 .compare_view_commits .author {
1480 1485 margin-left: 5px;
1481 1486 }
1482 1487
1483 1488 .compare_view_commits {
1484 1489 .color-a {
1485 1490 color: @alert1;
1486 1491 }
1487 1492
1488 1493 .color-c {
1489 1494 color: @color3;
1490 1495 }
1491 1496
1492 1497 .color-r {
1493 1498 color: @color5;
1494 1499 }
1495 1500
1496 1501 .color-a-bg {
1497 1502 background-color: @alert1;
1498 1503 }
1499 1504
1500 1505 .color-c-bg {
1501 1506 background-color: @alert3;
1502 1507 }
1503 1508
1504 1509 .color-r-bg {
1505 1510 background-color: @alert2;
1506 1511 }
1507 1512
1508 1513 .color-a-border {
1509 1514 border: 1px solid @alert1;
1510 1515 }
1511 1516
1512 1517 .color-c-border {
1513 1518 border: 1px solid @alert3;
1514 1519 }
1515 1520
1516 1521 .color-r-border {
1517 1522 border: 1px solid @alert2;
1518 1523 }
1519 1524
1520 1525 .commit-change-indicator {
1521 1526 width: 15px;
1522 1527 height: 15px;
1523 1528 position: relative;
1524 1529 left: 15px;
1525 1530 }
1526 1531
1527 1532 .commit-change-content {
1528 1533 text-align: center;
1529 1534 vertical-align: middle;
1530 1535 line-height: 15px;
1531 1536 }
1532 1537 }
1533 1538
1534 1539 .compare_view_files {
1535 1540 width: 100%;
1536 1541
1537 1542 td {
1538 1543 vertical-align: middle;
1539 1544 }
1540 1545 }
1541 1546
1542 1547 .compare_view_filepath {
1543 1548 color: @grey1;
1544 1549 }
1545 1550
1546 1551 .show_more {
1547 1552 display: inline-block;
1548 1553 position: relative;
1549 1554 vertical-align: middle;
1550 1555 width: 4px;
1551 1556 height: @basefontsize;
1552 1557
1553 1558 &:after {
1554 1559 content: "\00A0\25BE";
1555 1560 display: inline-block;
1556 1561 width:10px;
1557 1562 line-height: 5px;
1558 1563 font-size: 12px;
1559 1564 cursor: pointer;
1560 1565 }
1561 1566 }
1562 1567
1563 1568 .journal_more .show_more {
1564 1569 display: inline;
1565 1570
1566 1571 &:after {
1567 1572 content: none;
1568 1573 }
1569 1574 }
1570 1575
1571 1576 .open .show_more:after,
1572 1577 .select2-dropdown-open .show_more:after {
1573 1578 .rotate(180deg);
1574 1579 margin-left: 4px;
1575 1580 }
1576 1581
1577 1582
1578 1583 .compare_view_commits .collapse_commit:after {
1579 1584 cursor: pointer;
1580 1585 content: "\00A0\25B4";
1581 1586 margin-left: -3px;
1582 1587 font-size: 17px;
1583 1588 color: @grey4;
1584 1589 }
1585 1590
1586 1591 .diff_links {
1587 1592 margin-left: 8px;
1588 1593 }
1589 1594
1590 1595 div.ancestor {
1591 1596 margin: -30px 0px;
1592 1597 }
1593 1598
1594 1599 .cs_icon_td input[type="checkbox"] {
1595 1600 display: none;
1596 1601 }
1597 1602
1598 1603 .cs_icon_td .expand_file_icon:after {
1599 1604 cursor: pointer;
1600 1605 content: "\00A0\25B6";
1601 1606 font-size: 12px;
1602 1607 color: @grey4;
1603 1608 }
1604 1609
1605 1610 .cs_icon_td .collapse_file_icon:after {
1606 1611 cursor: pointer;
1607 1612 content: "\00A0\25BC";
1608 1613 font-size: 12px;
1609 1614 color: @grey4;
1610 1615 }
1611 1616
1612 1617 /*new binary
1613 1618 NEW_FILENODE = 1
1614 1619 DEL_FILENODE = 2
1615 1620 MOD_FILENODE = 3
1616 1621 RENAMED_FILENODE = 4
1617 1622 COPIED_FILENODE = 5
1618 1623 CHMOD_FILENODE = 6
1619 1624 BIN_FILENODE = 7
1620 1625 */
1621 1626 .cs_files_expand {
1622 1627 font-size: @basefontsize + 5px;
1623 1628 line-height: 1.8em;
1624 1629 float: right;
1625 1630 }
1626 1631
1627 1632 .cs_files_expand span{
1628 1633 color: @rcblue;
1629 1634 cursor: pointer;
1630 1635 }
1631 1636 .cs_files {
1632 1637 clear: both;
1633 1638 padding-bottom: @padding;
1634 1639
1635 1640 .cur_cs {
1636 1641 margin: 10px 2px;
1637 1642 font-weight: bold;
1638 1643 }
1639 1644
1640 1645 .node {
1641 1646 float: left;
1642 1647 }
1643 1648
1644 1649 .changes {
1645 1650 float: right;
1646 1651 color: white;
1647 1652 font-size: @basefontsize - 4px;
1648 1653 margin-top: 4px;
1649 1654 opacity: 0.6;
1650 1655 filter: Alpha(opacity=60); /* IE8 and earlier */
1651 1656
1652 1657 .added {
1653 1658 background-color: @alert1;
1654 1659 float: left;
1655 1660 text-align: center;
1656 1661 }
1657 1662
1658 1663 .deleted {
1659 1664 background-color: @alert2;
1660 1665 float: left;
1661 1666 text-align: center;
1662 1667 }
1663 1668
1664 1669 .bin {
1665 1670 background-color: @alert1;
1666 1671 text-align: center;
1667 1672 }
1668 1673
1669 1674 /*new binary*/
1670 1675 .bin.bin1 {
1671 1676 background-color: @alert1;
1672 1677 text-align: center;
1673 1678 }
1674 1679
1675 1680 /*deleted binary*/
1676 1681 .bin.bin2 {
1677 1682 background-color: @alert2;
1678 1683 text-align: center;
1679 1684 }
1680 1685
1681 1686 /*mod binary*/
1682 1687 .bin.bin3 {
1683 1688 background-color: @grey2;
1684 1689 text-align: center;
1685 1690 }
1686 1691
1687 1692 /*rename file*/
1688 1693 .bin.bin4 {
1689 1694 background-color: @alert4;
1690 1695 text-align: center;
1691 1696 }
1692 1697
1693 1698 /*copied file*/
1694 1699 .bin.bin5 {
1695 1700 background-color: @alert4;
1696 1701 text-align: center;
1697 1702 }
1698 1703
1699 1704 /*chmod file*/
1700 1705 .bin.bin6 {
1701 1706 background-color: @grey2;
1702 1707 text-align: center;
1703 1708 }
1704 1709 }
1705 1710 }
1706 1711
1707 1712 .cs_files .cs_added, .cs_files .cs_A,
1708 1713 .cs_files .cs_added, .cs_files .cs_M,
1709 1714 .cs_files .cs_added, .cs_files .cs_D {
1710 1715 height: 16px;
1711 1716 padding-right: 10px;
1712 1717 margin-top: 7px;
1713 1718 text-align: left;
1714 1719 }
1715 1720
1716 1721 .cs_icon_td {
1717 1722 min-width: 16px;
1718 1723 width: 16px;
1719 1724 }
1720 1725
1721 1726 .pull-request-merge {
1722 1727 border: 1px solid @grey5;
1723 1728 padding: 10px 0px 20px;
1724 1729 margin-top: 10px;
1725 1730 margin-bottom: 20px;
1726 1731 }
1727 1732
1728 1733 .pull-request-merge ul {
1729 1734 padding: 0px 0px;
1730 1735 }
1731 1736
1732 1737 .pull-request-merge li:before{
1733 1738 content:none;
1734 1739 }
1735 1740
1736 1741 .pull-request-merge .pull-request-wrap {
1737 1742 height: auto;
1738 1743 padding: 0px 0px;
1739 1744 text-align: right;
1740 1745 }
1741 1746
1742 1747 .pull-request-merge span {
1743 1748 margin-right: 5px;
1744 1749 }
1745 1750
1746 1751 .pull-request-merge-actions {
1747 1752 min-height: 30px;
1748 1753 padding: 0px 0px;
1749 1754 }
1750 1755
1751 1756 .pull-request-merge-info {
1752 1757 padding: 0px 5px 5px 0px;
1753 1758 }
1754 1759
1755 1760 .merge-status {
1756 1761 margin-right: 5px;
1757 1762 }
1758 1763
1759 1764 .merge-message {
1760 1765 font-size: 1.2em
1761 1766 }
1762 1767
1763 1768 .merge-message.success i,
1764 1769 .merge-icon.success i {
1765 1770 color:@alert1;
1766 1771 }
1767 1772
1768 1773 .merge-message.warning i,
1769 1774 .merge-icon.warning i {
1770 1775 color: @alert3;
1771 1776 }
1772 1777
1773 1778 .merge-message.error i,
1774 1779 .merge-icon.error i {
1775 1780 color:@alert2;
1776 1781 }
1777 1782
1778 1783 .pr-versions {
1779 1784 font-size: 1.1em;
1780 1785
1781 1786 table {
1782 1787 padding: 0px 5px;
1783 1788 }
1784 1789
1785 1790 td {
1786 1791 line-height: 15px;
1787 1792 }
1788 1793
1789 1794 .flag_status {
1790 1795 margin: 0;
1791 1796 }
1792 1797
1793 1798 .compare-radio-button {
1794 1799 position: relative;
1795 1800 top: -3px;
1796 1801 }
1797 1802 }
1798 1803
1799 1804
1800 1805 #close_pull_request {
1801 1806 margin-right: 0px;
1802 1807 }
1803 1808
1804 1809 .empty_data {
1805 1810 color: @grey4;
1806 1811 }
1807 1812
1808 1813 #changeset_compare_view_content {
1809 1814 margin-bottom: @space;
1810 1815 clear: both;
1811 1816 width: 100%;
1812 1817 box-sizing: border-box;
1813 1818 .border-radius(@border-radius);
1814 1819
1815 1820 .help-block {
1816 1821 margin: @padding 0;
1817 1822 color: @text-color;
1818 1823 &.pre-formatting {
1819 1824 white-space: pre;
1820 1825 }
1821 1826 }
1822 1827
1823 1828 .empty_data {
1824 1829 margin: @padding 0;
1825 1830 }
1826 1831
1827 1832 .alert {
1828 1833 margin-bottom: @space;
1829 1834 }
1830 1835 }
1831 1836
1832 1837 .table_disp {
1833 1838 .status {
1834 1839 width: auto;
1835 1840
1836 1841 .flag_status {
1837 1842 float: left;
1838 1843 }
1839 1844 }
1840 1845 }
1841 1846
1842 1847
1843 1848 .creation_in_progress {
1844 1849 color: @grey4
1845 1850 }
1846 1851
1847 1852 .status_box_menu {
1848 1853 margin: 0;
1849 1854 }
1850 1855
1851 1856 .notification-table{
1852 1857 margin-bottom: @space;
1853 1858 display: table;
1854 1859 width: 100%;
1855 1860
1856 1861 .container{
1857 1862 display: table-row;
1858 1863
1859 1864 .notification-header{
1860 1865 border-bottom: @border-thickness solid @border-default-color;
1861 1866 }
1862 1867
1863 1868 .notification-subject{
1864 1869 display: table-cell;
1865 1870 }
1866 1871 }
1867 1872 }
1868 1873
1869 1874 // Notifications
1870 1875 .notification-header{
1871 1876 display: table;
1872 1877 width: 100%;
1873 1878 padding: floor(@basefontsize/2) 0;
1874 1879 line-height: 1em;
1875 1880
1876 1881 .desc, .delete-notifications, .read-notifications{
1877 1882 display: table-cell;
1878 1883 text-align: left;
1879 1884 }
1880 1885
1881 1886 .desc{
1882 1887 width: 1163px;
1883 1888 }
1884 1889
1885 1890 .delete-notifications, .read-notifications{
1886 1891 width: 35px;
1887 1892 min-width: 35px; //fixes when only one button is displayed
1888 1893 }
1889 1894 }
1890 1895
1891 1896 .notification-body {
1892 1897 .markdown-block,
1893 1898 .rst-block {
1894 1899 padding: @padding 0;
1895 1900 }
1896 1901
1897 1902 .notification-subject {
1898 1903 padding: @textmargin 0;
1899 1904 border-bottom: @border-thickness solid @border-default-color;
1900 1905 }
1901 1906 }
1902 1907
1903 1908
1904 1909 .notifications_buttons{
1905 1910 float: right;
1906 1911 }
1907 1912
1908 1913 #notification-status{
1909 1914 display: inline;
1910 1915 }
1911 1916
1912 1917 // Repositories
1913 1918
1914 1919 #summary.fields{
1915 1920 display: table;
1916 1921
1917 1922 .field{
1918 1923 display: table-row;
1919 1924
1920 1925 .label-summary{
1921 1926 display: table-cell;
1922 1927 min-width: @label-summary-minwidth;
1923 1928 padding-top: @padding/2;
1924 1929 padding-bottom: @padding/2;
1925 1930 padding-right: @padding/2;
1926 1931 }
1927 1932
1928 1933 .input{
1929 1934 display: table-cell;
1930 1935 padding: @padding/2;
1931 1936
1932 1937 input{
1933 1938 min-width: 29em;
1934 1939 padding: @padding/4;
1935 1940 }
1936 1941 }
1937 1942 .statistics, .downloads{
1938 1943 .disabled{
1939 1944 color: @grey4;
1940 1945 }
1941 1946 }
1942 1947 }
1943 1948 }
1944 1949
1945 1950 #summary{
1946 1951 width: 70%;
1947 1952 }
1948 1953
1949 1954
1950 1955 // Journal
1951 1956 .journal.title {
1952 1957 h5 {
1953 1958 float: left;
1954 1959 margin: 0;
1955 1960 width: 70%;
1956 1961 }
1957 1962
1958 1963 ul {
1959 1964 float: right;
1960 1965 display: inline-block;
1961 1966 margin: 0;
1962 1967 width: 30%;
1963 1968 text-align: right;
1964 1969
1965 1970 li {
1966 1971 display: inline;
1967 1972 font-size: @journal-fontsize;
1968 1973 line-height: 1em;
1969 1974
1970 1975 &:before { content: none; }
1971 1976 }
1972 1977 }
1973 1978 }
1974 1979
1975 1980 .filterexample {
1976 1981 position: absolute;
1977 1982 top: 95px;
1978 1983 left: @contentpadding;
1979 1984 color: @rcblue;
1980 1985 font-size: 11px;
1981 1986 font-family: @text-regular;
1982 1987 cursor: help;
1983 1988
1984 1989 &:hover {
1985 1990 color: @rcdarkblue;
1986 1991 }
1987 1992
1988 1993 @media (max-width:768px) {
1989 1994 position: relative;
1990 1995 top: auto;
1991 1996 left: auto;
1992 1997 display: block;
1993 1998 }
1994 1999 }
1995 2000
1996 2001
1997 2002 #journal{
1998 2003 margin-bottom: @space;
1999 2004
2000 2005 .journal_day{
2001 2006 margin-bottom: @textmargin/2;
2002 2007 padding-bottom: @textmargin/2;
2003 2008 font-size: @journal-fontsize;
2004 2009 border-bottom: @border-thickness solid @border-default-color;
2005 2010 }
2006 2011
2007 2012 .journal_container{
2008 2013 margin-bottom: @space;
2009 2014
2010 2015 .journal_user{
2011 2016 display: inline-block;
2012 2017 }
2013 2018 .journal_action_container{
2014 2019 display: block;
2015 2020 margin-top: @textmargin;
2016 2021
2017 2022 div{
2018 2023 display: inline;
2019 2024 }
2020 2025
2021 2026 div.journal_action_params{
2022 2027 display: block;
2023 2028 }
2024 2029
2025 2030 div.journal_repo:after{
2026 2031 content: "\A";
2027 2032 white-space: pre;
2028 2033 }
2029 2034
2030 2035 div.date{
2031 2036 display: block;
2032 2037 margin-bottom: @textmargin;
2033 2038 }
2034 2039 }
2035 2040 }
2036 2041 }
2037 2042
2038 2043 // Files
2039 2044 .edit-file-title {
2040 2045 border-bottom: @border-thickness solid @border-default-color;
2041 2046
2042 2047 .breadcrumbs {
2043 2048 margin-bottom: 0;
2044 2049 }
2045 2050 }
2046 2051
2047 2052 .edit-file-fieldset {
2048 2053 margin-top: @sidebarpadding;
2049 2054
2050 2055 .fieldset {
2051 2056 .left-label {
2052 2057 width: 13%;
2053 2058 }
2054 2059 .right-content {
2055 2060 width: 87%;
2056 2061 max-width: 100%;
2057 2062 }
2058 2063 .filename-label {
2059 2064 margin-top: 13px;
2060 2065 }
2061 2066 .commit-message-label {
2062 2067 margin-top: 4px;
2063 2068 }
2064 2069 .file-upload-input {
2065 2070 input {
2066 2071 display: none;
2067 2072 }
2068 2073 margin-top: 10px;
2069 2074 }
2070 2075 .file-upload-label {
2071 2076 margin-top: 10px;
2072 2077 }
2073 2078 p {
2074 2079 margin-top: 5px;
2075 2080 }
2076 2081
2077 2082 }
2078 2083 .custom-path-link {
2079 2084 margin-left: 5px;
2080 2085 }
2081 2086 #commit {
2082 2087 resize: vertical;
2083 2088 }
2084 2089 }
2085 2090
2086 2091 .delete-file-preview {
2087 2092 max-height: 250px;
2088 2093 }
2089 2094
2090 2095 .new-file,
2091 2096 #filter_activate,
2092 2097 #filter_deactivate {
2093 2098 float: left;
2094 2099 margin: 0 0 0 15px;
2095 2100 }
2096 2101
2097 2102 h3.files_location{
2098 2103 line-height: 2.4em;
2099 2104 }
2100 2105
2101 2106 .browser-nav {
2102 2107 display: table;
2103 2108 margin-bottom: @space;
2104 2109
2105 2110
2106 2111 .info_box {
2107 2112 display: inline-table;
2108 2113 height: 2.5em;
2109 2114
2110 2115 .browser-cur-rev, .info_box_elem {
2111 2116 display: table-cell;
2112 2117 vertical-align: middle;
2113 2118 }
2114 2119
2115 2120 .info_box_elem {
2116 2121 border-top: @border-thickness solid @rcblue;
2117 2122 border-bottom: @border-thickness solid @rcblue;
2118 2123
2119 2124 #at_rev, a {
2120 2125 padding: 0.6em 0.9em;
2121 2126 margin: 0;
2122 2127 .box-shadow(none);
2123 2128 border: 0;
2124 2129 height: 12px;
2125 2130 }
2126 2131
2127 2132 input#at_rev {
2128 2133 max-width: 50px;
2129 2134 text-align: right;
2130 2135 }
2131 2136
2132 2137 &.previous {
2133 2138 border: @border-thickness solid @rcblue;
2134 2139 .disabled {
2135 2140 color: @grey4;
2136 2141 cursor: not-allowed;
2137 2142 }
2138 2143 }
2139 2144
2140 2145 &.next {
2141 2146 border: @border-thickness solid @rcblue;
2142 2147 .disabled {
2143 2148 color: @grey4;
2144 2149 cursor: not-allowed;
2145 2150 }
2146 2151 }
2147 2152 }
2148 2153
2149 2154 .browser-cur-rev {
2150 2155
2151 2156 span{
2152 2157 margin: 0;
2153 2158 color: @rcblue;
2154 2159 height: 12px;
2155 2160 display: inline-block;
2156 2161 padding: 0.7em 1em ;
2157 2162 border: @border-thickness solid @rcblue;
2158 2163 margin-right: @padding;
2159 2164 }
2160 2165 }
2161 2166 }
2162 2167
2163 2168 .search_activate {
2164 2169 display: table-cell;
2165 2170 vertical-align: middle;
2166 2171
2167 2172 input, label{
2168 2173 margin: 0;
2169 2174 padding: 0;
2170 2175 }
2171 2176
2172 2177 input{
2173 2178 margin-left: @textmargin;
2174 2179 }
2175 2180
2176 2181 }
2177 2182 }
2178 2183
2179 2184 .browser-cur-rev{
2180 2185 margin-bottom: @textmargin;
2181 2186 }
2182 2187
2183 2188 #node_filter_box_loading{
2184 2189 .info_text;
2185 2190 }
2186 2191
2187 2192 .browser-search {
2188 2193 margin: -25px 0px 5px 0px;
2189 2194 }
2190 2195
2191 2196 .node-filter {
2192 2197 font-size: @repo-title-fontsize;
2193 2198 padding: 4px 0px 0px 0px;
2194 2199
2195 2200 .node-filter-path {
2196 2201 float: left;
2197 2202 color: @grey4;
2198 2203 }
2199 2204 .node-filter-input {
2200 2205 float: left;
2201 2206 margin: -2px 0px 0px 2px;
2202 2207 input {
2203 2208 padding: 2px;
2204 2209 border: none;
2205 2210 font-size: @repo-title-fontsize;
2206 2211 }
2207 2212 }
2208 2213 }
2209 2214
2210 2215
2211 2216 .browser-result{
2212 2217 td a{
2213 2218 margin-left: 0.5em;
2214 2219 display: inline-block;
2215 2220
2216 2221 em{
2217 2222 font-family: @text-bold;
2218 2223 }
2219 2224 }
2220 2225 }
2221 2226
2222 2227 .browser-highlight{
2223 2228 background-color: @grey5-alpha;
2224 2229 }
2225 2230
2226 2231
2227 2232 // Search
2228 2233
2229 2234 .search-form{
2230 2235 #q {
2231 2236 width: @search-form-width;
2232 2237 }
2233 2238 .fields{
2234 2239 margin: 0 0 @space;
2235 2240 }
2236 2241
2237 2242 label{
2238 2243 display: inline-block;
2239 2244 margin-right: @textmargin;
2240 2245 padding-top: 0.25em;
2241 2246 }
2242 2247
2243 2248
2244 2249 .results{
2245 2250 clear: both;
2246 2251 margin: 0 0 @padding;
2247 2252 }
2248 2253 }
2249 2254
2250 2255 div.search-feedback-items {
2251 2256 display: inline-block;
2252 2257 padding:0px 0px 0px 96px;
2253 2258 }
2254 2259
2255 2260 div.search-code-body {
2256 2261 background-color: #ffffff; padding: 5px 0 5px 10px;
2257 2262 pre {
2258 2263 .match { background-color: #faffa6;}
2259 2264 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2260 2265 }
2261 2266 }
2262 2267
2263 2268 .expand_commit.search {
2264 2269 .show_more.open {
2265 2270 height: auto;
2266 2271 max-height: none;
2267 2272 }
2268 2273 }
2269 2274
2270 2275 .search-results {
2271 2276
2272 2277 h2 {
2273 2278 margin-bottom: 0;
2274 2279 }
2275 2280 .codeblock {
2276 2281 border: none;
2277 2282 background: transparent;
2278 2283 }
2279 2284
2280 2285 .codeblock-header {
2281 2286 border: none;
2282 2287 background: transparent;
2283 2288 }
2284 2289
2285 2290 .code-body {
2286 2291 border: @border-thickness solid @border-default-color;
2287 2292 .border-radius(@border-radius);
2288 2293 }
2289 2294
2290 2295 .td-commit {
2291 2296 &:extend(pre);
2292 2297 border-bottom: @border-thickness solid @border-default-color;
2293 2298 }
2294 2299
2295 2300 .message {
2296 2301 height: auto;
2297 2302 max-width: 350px;
2298 2303 white-space: normal;
2299 2304 text-overflow: initial;
2300 2305 overflow: visible;
2301 2306
2302 2307 .match { background-color: #faffa6;}
2303 2308 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2304 2309 }
2305 2310
2306 2311 }
2307 2312
2308 2313 table.rctable td.td-search-results div {
2309 2314 max-width: 100%;
2310 2315 }
2311 2316
2312 2317 #tip-box, .tip-box{
2313 2318 padding: @menupadding/2;
2314 2319 display: block;
2315 2320 border: @border-thickness solid @border-highlight-color;
2316 2321 .border-radius(@border-radius);
2317 2322 background-color: white;
2318 2323 z-index: 99;
2319 2324 white-space: pre-wrap;
2320 2325 }
2321 2326
2322 2327 #linktt {
2323 2328 width: 79px;
2324 2329 }
2325 2330
2326 2331 #help_kb .modal-content{
2327 2332 max-width: 750px;
2328 2333 margin: 10% auto;
2329 2334
2330 2335 table{
2331 2336 td,th{
2332 2337 border-bottom: none;
2333 2338 line-height: 2.5em;
2334 2339 }
2335 2340 th{
2336 2341 padding-bottom: @textmargin/2;
2337 2342 }
2338 2343 td.keys{
2339 2344 text-align: center;
2340 2345 }
2341 2346 }
2342 2347
2343 2348 .block-left{
2344 2349 width: 45%;
2345 2350 margin-right: 5%;
2346 2351 }
2347 2352 .modal-footer{
2348 2353 clear: both;
2349 2354 }
2350 2355 .key.tag{
2351 2356 padding: 0.5em;
2352 2357 background-color: @rcblue;
2353 2358 color: white;
2354 2359 border-color: @rcblue;
2355 2360 .box-shadow(none);
2356 2361 }
2357 2362 }
2358 2363
2359 2364
2360 2365
2361 2366 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2362 2367
2363 2368 @import 'statistics-graph';
2364 2369 @import 'tables';
2365 2370 @import 'forms';
2366 2371 @import 'diff';
2367 2372 @import 'summary';
2368 2373 @import 'navigation';
2369 2374
2370 2375 //--- SHOW/HIDE SECTIONS --//
2371 2376
2372 2377 .btn-collapse {
2373 2378 float: right;
2374 2379 text-align: right;
2375 2380 font-family: @text-light;
2376 2381 font-size: @basefontsize;
2377 2382 cursor: pointer;
2378 2383 border: none;
2379 2384 color: @rcblue;
2380 2385 }
2381 2386
2382 2387 table.rctable,
2383 2388 table.dataTable {
2384 2389 .btn-collapse {
2385 2390 float: right;
2386 2391 text-align: right;
2387 2392 }
2388 2393 }
2389 2394
2390 2395
2391 2396 // TODO: johbo: Fix for IE10, this avoids that we see a border
2392 2397 // and padding around checkboxes and radio boxes. Move to the right place,
2393 2398 // or better: Remove this once we did the form refactoring.
2394 2399 input[type=checkbox],
2395 2400 input[type=radio] {
2396 2401 padding: 0;
2397 2402 border: none;
2398 2403 }
2399 2404
2400 2405 .toggle-ajax-spinner{
2401 2406 height: 16px;
2402 2407 width: 16px;
2403 2408 }
@@ -1,297 +1,314 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4
5 5 <%def name="title()">
6 6 ${_('%s Changelog') % c.repo_name}
7 7 %if c.changelog_for_path:
8 8 /${c.changelog_for_path}
9 9 %endif
10 10 %if c.rhodecode_name:
11 11 &middot; ${h.branding(c.rhodecode_name)}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="breadcrumbs_links()">
16 16 %if c.changelog_for_path:
17 17 /${c.changelog_for_path}
18 18 %endif
19 19 </%def>
20 20
21 21 <%def name="menu_bar_nav()">
22 22 ${self.menu_items(active='repositories')}
23 23 </%def>
24 24
25 25 <%def name="menu_bar_subnav()">
26 26 ${self.repo_menu(active='changelog')}
27 27 </%def>
28 28
29 29 <%def name="main()">
30 30
31 31 <div class="box">
32 32 <div class="title">
33 33 ${self.repo_page_title(c.rhodecode_db_repo)}
34 34 <ul class="links">
35 35 <li>
36 36 <a href="#" class="btn btn-small" id="rev_range_container" style="display:none;"></a>
37 37 %if c.rhodecode_db_repo.fork:
38 38 <span>
39 39 <a id="compare_fork_button"
40 40 title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
41 41 class="btn btn-small"
42 42 href="${h.route_path('repo_compare',
43 43 repo_name=c.rhodecode_db_repo.fork.repo_name,
44 44 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
45 45 source_ref=c.rhodecode_db_repo.landing_rev[1],
46 46 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
47 47 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
48 48 _query=dict(merge=1, target_repo=c.repo_name))}"
49 49 >
50 50 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
51 51 </a>
52 52 </span>
53 53 %endif
54 54
55 55 ## pr open link
56 56 %if h.is_hg(c.rhodecode_repo) or h.is_git(c.rhodecode_repo):
57 57 <span>
58 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 59 ${_('Open new pull request')}
60 60 </a>
61 61 </span>
62 62 %endif
63 63
64 64 ## clear selection
65 65 <div title="${_('Clear selection')}" class="btn" id="rev_range_clear" style="display:none">
66 66 ${_('Clear selection')}
67 67 </div>
68 68
69 69 </li>
70 70 </ul>
71 71 </div>
72 72
73 73 % if c.pagination:
74 74 <script type="text/javascript" src="${h.asset('js/src/plugins/jquery.commits-graph.js')}"></script>
75 75
76 76 <div class="graph-header">
77 77 <div id="filter_changelog">
78 78 ${h.hidden('branch_filter')}
79 79 %if c.selected_name:
80 80 <div class="btn btn-default" id="clear_filter" >
81 81 ${_('Clear filter')}
82 82 </div>
83 83 %endif
84 84 </div>
85 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 97 <div id="commit-counter" data-total=${c.total_cs} class="pull-right">
87 98 ${_ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
88 99 </div>
89 100 </div>
90 101
91 102 <div id="graph">
92 103 <div class="graph-col-wrapper">
93 104 <div id="graph_nodes">
94 105 <div id="graph_canvas"></div>
95 106 </div>
96 107 <div id="graph_content" class="main-content graph_full_width">
97 108
98 109 <div class="table">
99 110 <table id="changesets" class="rctable">
100 111 <tr>
101 112 ## checkbox
102 113 <th></th>
103 114 <th colspan="2"></th>
104 115
105 116 <th>${_('Commit')}</th>
117 ## Mercurial phase/evolve state
118 <th></th>
106 119 ## commit message expand arrow
107 120 <th></th>
108 121 <th>${_('Commit Message')}</th>
109 122
110 123 <th>${_('Age')}</th>
111 124 <th>${_('Author')}</th>
112 125
113 126 <th>${_('Refs')}</th>
114 127 </tr>
115 128
116 129 <tbody class="commits-range">
117 130 <%include file='changelog_elements.mako'/>
118 131 </tbody>
119 132 </table>
120 133 </div>
121 134 </div>
122 135 <div class="pagination-wh pagination-left">
123 136 ${c.pagination.pager('$link_previous ~2~ $link_next')}
124 137 </div>
125 138 </div>
126 139
127 140 <script type="text/javascript">
128 141 var cache = {};
129 142 $(function(){
130 143
131 144 // Create links to commit ranges when range checkboxes are selected
132 145 var $commitCheckboxes = $('.commit-range');
133 146 // cache elements
134 147 var $commitRangeContainer = $('#rev_range_container');
135 148 var $commitRangeClear = $('#rev_range_clear');
136 149
137 150 var checkboxRangeSelector = function(e){
138 151 var selectedCheckboxes = [];
139 152 for (pos in $commitCheckboxes){
140 153 if($commitCheckboxes[pos].checked){
141 154 selectedCheckboxes.push($commitCheckboxes[pos]);
142 155 }
143 156 }
144 157 var open_new_pull_request = $('#open_new_pull_request');
145 158 if(open_new_pull_request){
146 159 var selected_changes = selectedCheckboxes.length;
147 160 if (selected_changes > 1 || selected_changes == 1 && templateContext.repo_type != 'hg') {
148 161 open_new_pull_request.hide();
149 162 } else {
150 163 if (selected_changes == 1) {
151 164 open_new_pull_request.html(_gettext('Open new pull request for selected commit'));
152 165 } else if (selected_changes == 0) {
153 166 open_new_pull_request.html(_gettext('Open new pull request'));
154 167 }
155 168 open_new_pull_request.show();
156 169 }
157 170 }
158 171
159 172 if (selectedCheckboxes.length>0){
160 173 var revEnd = selectedCheckboxes[0].name;
161 174 var revStart = selectedCheckboxes[selectedCheckboxes.length-1].name;
162 175 var url = pyroutes.url('repo_commit',
163 176 {'repo_name': '${c.repo_name}',
164 177 'commit_id': revStart+'...'+revEnd});
165 178
166 179 var link = (revStart == revEnd)
167 180 ? _gettext('Show selected commit __S')
168 181 : _gettext('Show selected commits __S ... __E');
169 182
170 183 link = link.replace('__S', revStart.substr(0,6));
171 184 link = link.replace('__E', revEnd.substr(0,6));
172 185
173 186 $commitRangeContainer
174 187 .attr('href',url)
175 188 .html(link)
176 189 .show();
177 190
178 191 $commitRangeClear.show();
179 192 var _url = pyroutes.url('pullrequest_new',
180 193 {'repo_name': '${c.repo_name}',
181 194 'commit': revEnd});
182 195 open_new_pull_request.attr('href', _url);
183 196 $('#compare_fork_button').hide();
184 197 } else {
185 198 $commitRangeContainer.hide();
186 199 $commitRangeClear.hide();
187 200
188 201 %if c.branch_name:
189 202 var _url = pyroutes.url('pullrequest_new',
190 203 {'repo_name': '${c.repo_name}',
191 204 'branch':'${c.branch_name}'});
192 205 open_new_pull_request.attr('href', _url);
193 206 %else:
194 207 var _url = pyroutes.url('pullrequest_new',
195 208 {'repo_name': '${c.repo_name}'});
196 209 open_new_pull_request.attr('href', _url);
197 210 %endif
198 211 $('#compare_fork_button').show();
199 212 }
200 213 };
201 214
202 215 $commitCheckboxes.on('click', checkboxRangeSelector);
203 216
204 217 $commitRangeClear.on('click',function(e) {
205 218 $commitCheckboxes.attr('checked', false);
206 219 checkboxRangeSelector();
207 220 e.preventDefault();
208 221 });
209 222
210 223 // make sure the buttons are consistent when navigate back and forth
211 224 checkboxRangeSelector();
212 225
213 226 var msgs = $('.message');
214 227 // get first element height
215 228 var el = $('#graph_content .container')[0];
216 229 var row_h = el.clientHeight;
217 230 for (var i=0; i < msgs.length; i++) {
218 231 var m = msgs[i];
219 232
220 233 var h = m.clientHeight;
221 234 var pad = $(m).css('padding');
222 235 if (h > row_h) {
223 236 var offset = row_h - (h+12);
224 237 $(m.nextElementSibling).css('display','block');
225 238 $(m.nextElementSibling).css('margin-top',offset+'px');
226 239 }
227 240 }
228 241
229 242 $("#clear_filter").on("click", function() {
230 243 var filter = {'repo_name': '${c.repo_name}'};
231 244 window.location = pyroutes.url('repo_changelog', filter);
232 245 });
233 246
234 247 $("#branch_filter").select2({
235 248 'dropdownAutoWidth': true,
236 249 'width': 'resolve',
237 250 'placeholder': "${c.selected_name or _('Filter changelog')}",
238 251 containerCssClass: "drop-menu",
239 252 dropdownCssClass: "drop-menu-dropdown",
240 253 query: function(query){
241 254 var key = 'cache';
242 255 var cached = cache[key] ;
243 256 if(cached) {
244 257 var data = {results: []};
245 258 //filter results
246 259 $.each(cached.results, function(){
247 260 var section = this.text;
248 261 var children = [];
249 262 $.each(this.children, function(){
250 263 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
251 264 children.push({'id': this.id, 'text': this.text, 'type': this.type})
252 265 }
253 266 });
254 267 data.results.push({'text': section, 'children': children});
255 268 query.callback({results: data.results});
256 269 });
257 270 }else{
258 271 $.ajax({
259 272 url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}),
260 273 data: {},
261 274 dataType: 'json',
262 275 type: 'GET',
263 276 success: function(data) {
264 277 cache[key] = data;
265 278 query.callback({results: data.results});
266 279 }
267 280 })
268 281 }
269 282 }
270 283 });
271 284 $('#branch_filter').on('change', function(e){
272 285 var data = $('#branch_filter').select2('data');
286 //type: branch_closed
273 287 var selected = data.text;
274 288 var filter = {'repo_name': '${c.repo_name}'};
275 289 if(data.type == 'branch' || data.type == 'branch_closed'){
276 290 filter["branch"] = selected;
291 if (data.type == 'branch_closed') {
292 filter["evolve"] = '1';
293 }
277 294 }
278 295 else if (data.type == 'book'){
279 296 filter["bookmark"] = selected;
280 297 }
281 298 window.location = pyroutes.url('repo_changelog', filter);
282 299 });
283 300
284 301 commitsController = new CommitsController();
285 302 % if not c.changelog_for_path:
286 303 commitsController.reloadGraph();
287 304 % endif
288 305
289 306 });
290 307
291 308 </script>
292 309 </div>
293 310 % else:
294 311 ${_('There are no changes yet')}
295 312 % endif
296 313 </div>
297 314 </%def>
@@ -1,144 +1,146 b''
1 1 ## small box that displays changed/added/removed details fetched by AJAX
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4
5 5 % if c.prev_page:
6 6 <tr>
7 7 <td colspan="9" class="load-more-commits">
8 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 9 ${_('load previous')}
10 10 </a>
11 11 </td>
12 12 </tr>
13 13 % endif
14 14
15 15 % for cnt,commit in enumerate(c.pagination):
16 16 <tr id="sha_${commit.raw_id}" class="changelogRow container ${'tablerow%s' % (cnt%2)}">
17 17
18 18 <td class="td-checkbox">
19 19 ${h.checkbox(commit.raw_id,class_="commit-range")}
20 20 </td>
21 21 <td class="td-status">
22 22
23 23 %if c.statuses.get(commit.raw_id):
24 24 <div class="changeset-status-ico">
25 25 %if c.statuses.get(commit.raw_id)[2]:
26 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 27 <div class="${'flag_status {}'.format(c.statuses.get(commit.raw_id)[0])}"></div>
28 28 </a>
29 29 %else:
30 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 31 <div class="${'flag_status {}'.format(c.statuses.get(commit.raw_id)[0])}"></div>
32 32 </a>
33 33 %endif
34 34 </div>
35 35 %else:
36 36 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
37 37 %endif
38 38 </td>
39 39 <td class="td-comments comments-col">
40 40 %if c.comments.get(commit.raw_id):
41 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 42 <i class="icon-comment"></i> ${len(c.comments[commit.raw_id])}
43 43 </a>
44 44 %endif
45 45 </td>
46 46 <td class="td-hash">
47 47 <code>
48 48
49 49 <a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">
50 50 <span class="${'commit_hash obsolete' if getattr(commit, 'obsolete', None) else 'commit_hash'}">${h.show_id(commit)}</span>
51 51 </a>
52 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 57 % if hasattr(commit, 'phase'):
54 58 % if commit.phase != 'public':
55 59 <span class="tag phase-${commit.phase} tooltip" title="${_('Commit phase')}">${commit.phase}</span>
56 60 % endif
57 61 % endif
58 62
59 63 ## obsolete commits
60 64 % if hasattr(commit, 'obsolete'):
61 65 % if commit.obsolete:
62 66 <span class="tag obsolete-${commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
63 67 % endif
64 68 % endif
65 69
66 70 ## hidden commits
67 71 % if hasattr(commit, 'hidden'):
68 72 % if commit.hidden:
69 73 <span class="tag obsolete-${commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
70 74 % endif
71 75 % endif
72
73 </code>
74 76 </td>
75 77 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
76 78 <div class="show_more_col">
77 79 <i class="show_more"></i>&nbsp;
78 80 </div>
79 81 </td>
80 82 <td class="td-description mid">
81 83 <div class="log-container truncate-wrap">
82 84 <div class="message truncate" id="c-${commit.raw_id}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
83 85 </div>
84 86 </td>
85 87
86 88 <td class="td-time">
87 89 ${h.age_component(commit.date)}
88 90 </td>
89 91 <td class="td-user">
90 92 ${base.gravatar_with_user(commit.author)}
91 93 </td>
92 94
93 95 <td class="td-tags tags-col">
94 96 <div id="t-${commit.raw_id}">
95 97
96 98 ## merge
97 99 %if commit.merge:
98 100 <span class="tag mergetag">
99 101 <i class="icon-merge"></i>${_('merge')}
100 102 </span>
101 103 %endif
102 104
103 105 ## branch
104 106 %if commit.branch:
105 107 <span class="tag branchtag" title="${h.tooltip(_('Branch %s') % commit.branch)}">
106 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 109 </span>
108 110 %endif
109 111
110 112 ## bookmarks
111 113 %if h.is_hg(c.rhodecode_repo):
112 114 %for book in commit.bookmarks:
113 115 <span class="tag booktag" title="${h.tooltip(_('Bookmark %s') % book)}">
114 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 117 </span>
116 118 %endfor
117 119 %endif
118 120
119 121 ## tags
120 122 %for tag in commit.tags:
121 123 <span class="tag tagtag" title="${h.tooltip(_('Tag %s') % tag)}">
122 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 125 </span>
124 126 %endfor
125 127
126 128 </div>
127 129 </td>
128 130 </tr>
129 131 % endfor
130 132
131 133 % if c.next_page:
132 134 <tr>
133 <td colspan="9" class="load-more-commits">
135 <td colspan="10" class="load-more-commits">
134 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 137 ${_('load next')}
136 138 </a>
137 139 </td>
138 140 </tr>
139 141 % endif
140 142 <tr class="chunk-graph-data" style="display:none"
141 143 data-graph='${c.graph_data|n}'
142 144 data-node='${c.prev_page}:${c.next_page}'
143 145 data-commits='${c.graph_commits|n}'>
144 146 </tr> No newline at end of file
@@ -1,575 +1,590 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import datetime
22 22 import time
23 23
24 24 import pytest
25 25
26 26 from rhodecode.lib.vcs.backends.base import (
27 27 CollectionGenerator, FILEMODE_DEFAULT, EmptyCommit)
28 28 from rhodecode.lib.vcs.exceptions import (
29 29 BranchDoesNotExistError, CommitDoesNotExistError,
30 30 RepositoryError, EmptyRepositoryError)
31 31 from rhodecode.lib.vcs.nodes import (
32 32 FileNode, AddedFileNodesGenerator,
33 33 ChangedFileNodesGenerator, RemovedFileNodesGenerator)
34 34 from rhodecode.tests import get_new_dir
35 35 from rhodecode.tests.vcs.base import BackendTestMixin
36 36
37 37
38 38 class TestBaseChangeset:
39 39
40 40 def test_is_deprecated(self):
41 41 from rhodecode.lib.vcs.backends.base import BaseChangeset
42 42 pytest.deprecated_call(BaseChangeset)
43 43
44 44
45 45 class TestEmptyCommit:
46 46
47 47 def test_branch_without_alias_returns_none(self):
48 48 commit = EmptyCommit()
49 49 assert commit.branch is None
50 50
51 51
52 52 class TestCommitsInNonEmptyRepo(BackendTestMixin):
53 53 recreate_repo_per_test = True
54 54
55 55 @classmethod
56 56 def _get_commits(cls):
57 57 start_date = datetime.datetime(2010, 1, 1, 20)
58 58 for x in xrange(5):
59 59 yield {
60 60 'message': 'Commit %d' % x,
61 61 'author': 'Joe Doe <joe.doe@example.com>',
62 62 'date': start_date + datetime.timedelta(hours=12 * x),
63 63 'added': [
64 64 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
65 65 ],
66 66 }
67 67
68 68 def test_walk_returns_empty_list_in_case_of_file(self):
69 69 result = list(self.tip.walk('file_0.txt'))
70 70 assert result == []
71 71
72 72 @pytest.mark.backends("git", "hg")
73 73 def test_new_branch(self):
74 74 self.imc.add(FileNode('docs/index.txt',
75 75 content='Documentation\n'))
76 76 foobar_tip = self.imc.commit(
77 77 message=u'New branch: foobar',
78 78 author=u'joe',
79 79 branch='foobar',
80 80 )
81 81 assert 'foobar' in self.repo.branches
82 82 assert foobar_tip.branch == 'foobar'
83 83 # 'foobar' should be the only branch that contains the new commit
84 84 branch = self.repo.branches.values()
85 85 assert branch[0] != branch[1]
86 86
87 87 @pytest.mark.backends("git", "hg")
88 88 def test_new_head_in_default_branch(self):
89 89 tip = self.repo.get_commit()
90 90 self.imc.add(FileNode('docs/index.txt',
91 91 content='Documentation\n'))
92 92 foobar_tip = self.imc.commit(
93 93 message=u'New branch: foobar',
94 94 author=u'joe',
95 95 branch='foobar',
96 96 parents=[tip],
97 97 )
98 98 self.imc.change(FileNode('docs/index.txt',
99 99 content='Documentation\nand more...\n'))
100 100 newtip = self.imc.commit(
101 101 message=u'At default branch',
102 102 author=u'joe',
103 103 branch=foobar_tip.branch,
104 104 parents=[foobar_tip],
105 105 )
106 106
107 107 newest_tip = self.imc.commit(
108 108 message=u'Merged with %s' % foobar_tip.raw_id,
109 109 author=u'joe',
110 110 branch=self.backend_class.DEFAULT_BRANCH_NAME,
111 111 parents=[newtip, foobar_tip],
112 112 )
113 113
114 114 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
115 115
116 116 @pytest.mark.backends("git", "hg")
117 117 def test_get_commits_respects_branch_name(self):
118 118 """
119 119 * e1930d0 (HEAD, master) Back in default branch
120 120 | * e1930d0 (docs) New Branch: docs2
121 121 | * dcc14fa New branch: docs
122 122 |/
123 123 * e63c41a Initial commit
124 124 ...
125 125 * 624d3db Commit 0
126 126
127 127 :return:
128 128 """
129 129 DEFAULT_BRANCH = self.repo.DEFAULT_BRANCH_NAME
130 130 TEST_BRANCH = 'docs'
131 131 org_tip = self.repo.get_commit()
132 132
133 133 self.imc.add(FileNode('readme.txt', content='Document\n'))
134 134 initial = self.imc.commit(
135 135 message=u'Initial commit',
136 136 author=u'joe',
137 137 parents=[org_tip],
138 138 branch=DEFAULT_BRANCH,)
139 139
140 140 self.imc.add(FileNode('newdoc.txt', content='foobar\n'))
141 141 docs_branch_commit1 = self.imc.commit(
142 142 message=u'New branch: docs',
143 143 author=u'joe',
144 144 parents=[initial],
145 145 branch=TEST_BRANCH,)
146 146
147 147 self.imc.add(FileNode('newdoc2.txt', content='foobar2\n'))
148 148 docs_branch_commit2 = self.imc.commit(
149 149 message=u'New branch: docs2',
150 150 author=u'joe',
151 151 parents=[docs_branch_commit1],
152 152 branch=TEST_BRANCH,)
153 153
154 154 self.imc.add(FileNode('newfile', content='hello world\n'))
155 155 self.imc.commit(
156 156 message=u'Back in default branch',
157 157 author=u'joe',
158 158 parents=[initial],
159 159 branch=DEFAULT_BRANCH,)
160 160
161 161 default_branch_commits = self.repo.get_commits(
162 162 branch_name=DEFAULT_BRANCH)
163 163 assert docs_branch_commit1 not in list(default_branch_commits)
164 164 assert docs_branch_commit2 not in list(default_branch_commits)
165 165
166 166 docs_branch_commits = self.repo.get_commits(
167 167 start_id=self.repo.commit_ids[0], end_id=self.repo.commit_ids[-1],
168 168 branch_name=TEST_BRANCH)
169 169 assert docs_branch_commit1 in list(docs_branch_commits)
170 170 assert docs_branch_commit2 in list(docs_branch_commits)
171 171
172 172 @pytest.mark.backends("svn")
173 173 def test_get_commits_respects_branch_name_svn(self, vcsbackend_svn):
174 174 repo = vcsbackend_svn['svn-simple-layout']
175 175 commits = repo.get_commits(branch_name='trunk')
176 176 commit_indexes = [c.idx for c in commits]
177 177 assert commit_indexes == [1, 2, 3, 7, 12, 15]
178 178
179 179 def test_get_commit_by_branch(self):
180 180 for branch, commit_id in self.repo.branches.iteritems():
181 181 assert commit_id == self.repo.get_commit(branch).raw_id
182 182
183 183 def test_get_commit_by_tag(self):
184 184 for tag, commit_id in self.repo.tags.iteritems():
185 185 assert commit_id == self.repo.get_commit(tag).raw_id
186 186
187 187 def test_get_commit_parents(self):
188 188 repo = self.repo
189 189 for test_idx in [1, 2, 3]:
190 190 commit = repo.get_commit(commit_idx=test_idx - 1)
191 191 assert [commit] == repo.get_commit(commit_idx=test_idx).parents
192 192
193 193 def test_get_commit_children(self):
194 194 repo = self.repo
195 195 for test_idx in [1, 2, 3]:
196 196 commit = repo.get_commit(commit_idx=test_idx + 1)
197 197 assert [commit] == repo.get_commit(commit_idx=test_idx).children
198 198
199 199
200 200 class TestCommits(BackendTestMixin):
201 201 recreate_repo_per_test = False
202 202
203 203 @classmethod
204 204 def _get_commits(cls):
205 205 start_date = datetime.datetime(2010, 1, 1, 20)
206 206 for x in xrange(5):
207 207 yield {
208 208 'message': u'Commit %d' % x,
209 209 'author': u'Joe Doe <joe.doe@example.com>',
210 210 'date': start_date + datetime.timedelta(hours=12 * x),
211 211 'added': [
212 212 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
213 213 ],
214 214 }
215 215
216 216 def test_simple(self):
217 217 tip = self.repo.get_commit()
218 218 assert tip.date, datetime.datetime(2010, 1, 3 == 20)
219 219
220 220 def test_simple_serialized_commit(self):
221 221 tip = self.repo.get_commit()
222 222 # json.dumps(tip) uses .__json__() method
223 223 data = tip.__json__()
224 224 assert 'branch' in data
225 225 assert data['revision']
226 226
227 227 def test_retrieve_tip(self):
228 228 tip = self.repo.get_commit('tip')
229 229 assert tip == self.repo.get_commit()
230 230
231 231 def test_invalid(self):
232 232 with pytest.raises(CommitDoesNotExistError):
233 233 self.repo.get_commit(commit_idx=123456789)
234 234
235 235 def test_idx(self):
236 236 commit = self.repo[0]
237 237 assert commit.idx == 0
238 238
239 239 def test_negative_idx(self):
240 240 commit = self.repo.get_commit(commit_idx=-1)
241 241 assert commit.idx >= 0
242 242
243 243 def test_revision_is_deprecated(self):
244 244 def get_revision(commit):
245 245 return commit.revision
246 246
247 247 commit = self.repo[0]
248 248 pytest.deprecated_call(get_revision, commit)
249 249
250 250 def test_size(self):
251 251 tip = self.repo.get_commit()
252 252 size = 5 * len('Foobar N') # Size of 5 files
253 253 assert tip.size == size
254 254
255 255 def test_size_at_commit(self):
256 256 tip = self.repo.get_commit()
257 257 size = 5 * len('Foobar N') # Size of 5 files
258 258 assert self.repo.size_at_commit(tip.raw_id) == size
259 259
260 260 def test_size_at_first_commit(self):
261 261 commit = self.repo[0]
262 262 size = len('Foobar N') # Size of 1 file
263 263 assert self.repo.size_at_commit(commit.raw_id) == size
264 264
265 265 def test_author(self):
266 266 tip = self.repo.get_commit()
267 267 assert_text_equal(tip.author, u'Joe Doe <joe.doe@example.com>')
268 268
269 269 def test_author_name(self):
270 270 tip = self.repo.get_commit()
271 271 assert_text_equal(tip.author_name, u'Joe Doe')
272 272
273 273 def test_author_email(self):
274 274 tip = self.repo.get_commit()
275 275 assert_text_equal(tip.author_email, u'joe.doe@example.com')
276 276
277 277 def test_message(self):
278 278 tip = self.repo.get_commit()
279 279 assert_text_equal(tip.message, u'Commit 4')
280 280
281 281 def test_diff(self):
282 282 tip = self.repo.get_commit()
283 283 diff = tip.diff()
284 284 assert "+Foobar 4" in diff.raw
285 285
286 286 def test_prev(self):
287 287 tip = self.repo.get_commit()
288 288 prev_commit = tip.prev()
289 289 assert prev_commit.message == 'Commit 3'
290 290
291 291 def test_prev_raises_on_first_commit(self):
292 292 commit = self.repo.get_commit(commit_idx=0)
293 293 with pytest.raises(CommitDoesNotExistError):
294 294 commit.prev()
295 295
296 296 def test_prev_works_on_second_commit_issue_183(self):
297 297 commit = self.repo.get_commit(commit_idx=1)
298 298 prev_commit = commit.prev()
299 299 assert prev_commit.idx == 0
300 300
301 301 def test_next(self):
302 302 commit = self.repo.get_commit(commit_idx=2)
303 303 next_commit = commit.next()
304 304 assert next_commit.message == 'Commit 3'
305 305
306 306 def test_next_raises_on_tip(self):
307 307 commit = self.repo.get_commit()
308 308 with pytest.raises(CommitDoesNotExistError):
309 309 commit.next()
310 310
311 311 def test_get_file_commit(self):
312 312 commit = self.repo.get_commit()
313 313 commit.get_file_commit('file_4.txt')
314 314 assert commit.message == 'Commit 4'
315 315
316 316 def test_get_filenodes_generator(self):
317 317 tip = self.repo.get_commit()
318 318 filepaths = [node.path for node in tip.get_filenodes_generator()]
319 319 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
320 320
321 321 def test_get_file_annotate(self):
322 322 file_added_commit = self.repo.get_commit(commit_idx=3)
323 323 annotations = list(file_added_commit.get_file_annotate('file_3.txt'))
324 324
325 325 line_no, commit_id, commit_loader, line = annotations[0]
326 326
327 327 assert line_no == 1
328 328 assert commit_id == file_added_commit.raw_id
329 329 assert commit_loader() == file_added_commit
330 330 assert 'Foobar 3' in line
331 331
332 332 def test_get_file_annotate_does_not_exist(self):
333 333 file_added_commit = self.repo.get_commit(commit_idx=2)
334 334 # TODO: Should use a specific exception class here?
335 335 with pytest.raises(Exception):
336 336 list(file_added_commit.get_file_annotate('file_3.txt'))
337 337
338 338 def test_get_file_annotate_tip(self):
339 339 tip = self.repo.get_commit()
340 340 commit = self.repo.get_commit(commit_idx=3)
341 341 expected_values = list(commit.get_file_annotate('file_3.txt'))
342 342 annotations = list(tip.get_file_annotate('file_3.txt'))
343 343
344 344 # Note: Skip index 2 because the loader function is not the same
345 345 for idx in (0, 1, 3):
346 346 assert annotations[0][idx] == expected_values[0][idx]
347 347
348 348 def test_get_commits_is_ordered_by_date(self):
349 349 commits = self.repo.get_commits()
350 350 assert isinstance(commits, CollectionGenerator)
351 351 assert len(commits) == 0 or len(commits) != 0
352 352 commits = list(commits)
353 353 ordered_by_date = sorted(commits, key=lambda commit: commit.date)
354 354 assert commits == ordered_by_date
355 355
356 356 def test_get_commits_respects_start(self):
357 357 second_id = self.repo.commit_ids[1]
358 358 commits = self.repo.get_commits(start_id=second_id)
359 359 assert isinstance(commits, CollectionGenerator)
360 360 commits = list(commits)
361 361 assert len(commits) == 4
362 362
363 363 def test_get_commits_includes_start_commit(self):
364 364 second_id = self.repo.commit_ids[1]
365 365 commits = self.repo.get_commits(start_id=second_id)
366 366 assert isinstance(commits, CollectionGenerator)
367 367 commits = list(commits)
368 368 assert commits[0].raw_id == second_id
369 369
370 370 def test_get_commits_respects_end(self):
371 371 second_id = self.repo.commit_ids[1]
372 372 commits = self.repo.get_commits(end_id=second_id)
373 373 assert isinstance(commits, CollectionGenerator)
374 374 commits = list(commits)
375 375 assert commits[-1].raw_id == second_id
376 376 assert len(commits) == 2
377 377
378 378 def test_get_commits_respects_both_start_and_end(self):
379 379 second_id = self.repo.commit_ids[1]
380 380 third_id = self.repo.commit_ids[2]
381 381 commits = self.repo.get_commits(start_id=second_id, end_id=third_id)
382 382 assert isinstance(commits, CollectionGenerator)
383 383 commits = list(commits)
384 384 assert len(commits) == 2
385 385
386 386 def test_get_commits_on_empty_repo_raises_EmptyRepository_error(self):
387 387 repo_path = get_new_dir(str(time.time()))
388 388 repo = self.Backend(repo_path, create=True)
389 389
390 390 with pytest.raises(EmptyRepositoryError):
391 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 398 def test_get_commits_includes_end_commit(self):
394 399 second_id = self.repo.commit_ids[1]
395 400 commits = self.repo.get_commits(end_id=second_id)
396 401 assert isinstance(commits, CollectionGenerator)
397 402 assert len(commits) == 2
398 403 commits = list(commits)
399 404 assert commits[-1].raw_id == second_id
400 405
401 406 def test_get_commits_respects_start_date(self):
402 407 start_date = datetime.datetime(2010, 1, 2)
403 408 commits = self.repo.get_commits(start_date=start_date)
404 409 assert isinstance(commits, CollectionGenerator)
405 410 # Should be 4 commits after 2010-01-02 00:00:00
406 411 assert len(commits) == 4
407 412 for c in commits:
408 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 425 def test_get_commits_respects_start_date_and_end_date(self):
411 426 start_date = datetime.datetime(2010, 1, 2)
412 427 end_date = datetime.datetime(2010, 1, 3)
413 428 commits = self.repo.get_commits(start_date=start_date,
414 429 end_date=end_date)
415 430 assert isinstance(commits, CollectionGenerator)
416 431 assert len(commits) == 2
417 432 for c in commits:
418 433 assert c.date >= start_date
419 434 assert c.date <= end_date
420 435
421 436 def test_get_commits_respects_end_date(self):
422 437 end_date = datetime.datetime(2010, 1, 2)
423 438 commits = self.repo.get_commits(end_date=end_date)
424 439 assert isinstance(commits, CollectionGenerator)
425 440 assert len(commits) == 1
426 441 for c in commits:
427 442 assert c.date <= end_date
428 443
429 444 def test_get_commits_respects_reverse(self):
430 445 commits = self.repo.get_commits() # no longer reverse support
431 446 assert isinstance(commits, CollectionGenerator)
432 447 assert len(commits) == 5
433 448 commit_ids = reversed([c.raw_id for c in commits])
434 449 assert list(commit_ids) == list(reversed(self.repo.commit_ids))
435 450
436 451 def test_get_commits_slice_generator(self):
437 452 commits = self.repo.get_commits(
438 453 branch_name=self.repo.DEFAULT_BRANCH_NAME)
439 454 assert isinstance(commits, CollectionGenerator)
440 455 commit_slice = list(commits[1:3])
441 456 assert len(commit_slice) == 2
442 457
443 458 def test_get_commits_raise_commitdoesnotexist_for_wrong_start(self):
444 459 with pytest.raises(CommitDoesNotExistError):
445 460 list(self.repo.get_commits(start_id='foobar'))
446 461
447 462 def test_get_commits_raise_commitdoesnotexist_for_wrong_end(self):
448 463 with pytest.raises(CommitDoesNotExistError):
449 464 list(self.repo.get_commits(end_id='foobar'))
450 465
451 466 def test_get_commits_raise_branchdoesnotexist_for_wrong_branch_name(self):
452 467 with pytest.raises(BranchDoesNotExistError):
453 468 list(self.repo.get_commits(branch_name='foobar'))
454 469
455 470 def test_get_commits_raise_repositoryerror_for_wrong_start_end(self):
456 471 start_id = self.repo.commit_ids[-1]
457 472 end_id = self.repo.commit_ids[0]
458 473 with pytest.raises(RepositoryError):
459 474 list(self.repo.get_commits(start_id=start_id, end_id=end_id))
460 475
461 476 def test_get_commits_raises_for_numerical_ids(self):
462 477 with pytest.raises(TypeError):
463 478 self.repo.get_commits(start_id=1, end_id=2)
464 479
465 480 def test_commit_equality(self):
466 481 commit1 = self.repo.get_commit(self.repo.commit_ids[0])
467 482 commit2 = self.repo.get_commit(self.repo.commit_ids[1])
468 483
469 484 assert commit1 == commit1
470 485 assert commit2 == commit2
471 486 assert commit1 != commit2
472 487 assert commit2 != commit1
473 488 assert commit1 != None
474 489 assert None != commit1
475 490 assert 1 != commit1
476 491 assert 'string' != commit1
477 492
478 493
479 494 @pytest.mark.parametrize("filename, expected", [
480 495 ("README.rst", False),
481 496 ("README", True),
482 497 ])
483 498 def test_commit_is_link(vcsbackend, filename, expected):
484 499 commit = vcsbackend.repo.get_commit()
485 500 link_status = commit.is_link(filename)
486 501 assert link_status is expected
487 502
488 503
489 504 class TestCommitsChanges(BackendTestMixin):
490 505 recreate_repo_per_test = False
491 506
492 507 @classmethod
493 508 def _get_commits(cls):
494 509 return [
495 510 {
496 511 'message': u'Initial',
497 512 'author': u'Joe Doe <joe.doe@example.com>',
498 513 'date': datetime.datetime(2010, 1, 1, 20),
499 514 'added': [
500 515 FileNode('foo/bar', content='foo'),
501 516 FileNode('foo/bał', content='foo'),
502 517 FileNode('foobar', content='foo'),
503 518 FileNode('qwe', content='foo'),
504 519 ],
505 520 },
506 521 {
507 522 'message': u'Massive changes',
508 523 'author': u'Joe Doe <joe.doe@example.com>',
509 524 'date': datetime.datetime(2010, 1, 1, 22),
510 525 'added': [FileNode('fallout', content='War never changes')],
511 526 'changed': [
512 527 FileNode('foo/bar', content='baz'),
513 528 FileNode('foobar', content='baz'),
514 529 ],
515 530 'removed': [FileNode('qwe')],
516 531 },
517 532 ]
518 533
519 534 def test_initial_commit(self, local_dt_to_utc):
520 535 commit = self.repo.get_commit(commit_idx=0)
521 536 assert set(commit.added) == set([
522 537 commit.get_node('foo/bar'),
523 538 commit.get_node('foo/bał'),
524 539 commit.get_node('foobar'),
525 540 commit.get_node('qwe'),
526 541 ])
527 542 assert set(commit.changed) == set()
528 543 assert set(commit.removed) == set()
529 544 assert set(commit.affected_files) == set(
530 545 ['foo/bar', 'foo/bał', 'foobar', 'qwe'])
531 546 assert commit.date == local_dt_to_utc(
532 547 datetime.datetime(2010, 1, 1, 20, 0))
533 548
534 549 def test_head_added(self):
535 550 commit = self.repo.get_commit()
536 551 assert isinstance(commit.added, AddedFileNodesGenerator)
537 552 assert set(commit.added) == set([commit.get_node('fallout')])
538 553 assert isinstance(commit.changed, ChangedFileNodesGenerator)
539 554 assert set(commit.changed) == set([
540 555 commit.get_node('foo/bar'),
541 556 commit.get_node('foobar'),
542 557 ])
543 558 assert isinstance(commit.removed, RemovedFileNodesGenerator)
544 559 assert len(commit.removed) == 1
545 560 assert list(commit.removed)[0].path == 'qwe'
546 561
547 562 def test_get_filemode(self):
548 563 commit = self.repo.get_commit()
549 564 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bar')
550 565
551 566 def test_get_filemode_non_ascii(self):
552 567 commit = self.repo.get_commit()
553 568 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bał')
554 569 assert FILEMODE_DEFAULT == commit.get_file_mode(u'foo/bał')
555 570
556 571 def test_get_file_history(self):
557 572 commit = self.repo.get_commit()
558 573 history = commit.get_file_history('foo/bar')
559 574 assert len(history) == 2
560 575
561 576 def test_get_file_history_with_limit(self):
562 577 commit = self.repo.get_commit()
563 578 history = commit.get_file_history('foo/bar', limit=1)
564 579 assert len(history) == 1
565 580
566 581 def test_get_file_history_first_commit(self):
567 582 commit = self.repo[0]
568 583 history = commit.get_file_history('foo/bar')
569 584 assert len(history) == 1
570 585
571 586
572 587 def assert_text_equal(expected, given):
573 588 assert expected == given
574 589 assert isinstance(expected, unicode)
575 590 assert isinstance(given, unicode)
General Comments 0
You need to be logged in to leave comments. Login now