##// END OF EJS Templates
compare: resolve revisions in the right repository for diffs between repos
Mads Kiilerich -
r4039:b44fabc4 default
parent child Browse files
Show More
@@ -1,283 +1,283 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.compare
3 rhodecode.controllers.compare
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 compare controller for pylons showing differences between two
6 compare controller for pylons showing differences between two
7 repos, branches, bookmarks or tips
7 repos, branches, bookmarks or tips
8
8
9 :created_on: May 6, 2012
9 :created_on: May 6, 2012
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import logging
27 import logging
28 import traceback
28 import traceback
29 import re
29 import re
30
30
31 from webob.exc import HTTPNotFound
31 from webob.exc import HTTPNotFound
32 from pylons import request, response, session, tmpl_context as c, url
32 from pylons import request, response, session, tmpl_context as c, url
33 from pylons.controllers.util import abort, redirect
33 from pylons.controllers.util import abort, redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35
35
36 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
36 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
37 from rhodecode.lib.vcs.utils import safe_str
37 from rhodecode.lib.vcs.utils import safe_str
38 from rhodecode.lib.vcs.utils.hgcompat import scmutil, unionrepo
38 from rhodecode.lib.vcs.utils.hgcompat import scmutil, unionrepo
39 from rhodecode.lib import helpers as h
39 from rhodecode.lib import helpers as h
40 from rhodecode.lib.base import BaseRepoController, render
40 from rhodecode.lib.base import BaseRepoController, render
41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
42 from rhodecode.lib import diffs
42 from rhodecode.lib import diffs
43
43
44 from rhodecode.model.db import Repository
44 from rhodecode.model.db import Repository
45 from webob.exc import HTTPBadRequest
45 from webob.exc import HTTPBadRequest
46 from rhodecode.lib.diffs import LimitedDiffContainer
46 from rhodecode.lib.diffs import LimitedDiffContainer
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 class CompareController(BaseRepoController):
52 class CompareController(BaseRepoController):
53
53
54 def __before__(self):
54 def __before__(self):
55 super(CompareController, self).__before__()
55 super(CompareController, self).__before__()
56
56
57 def __get_cs_or_redirect(self, ref, repo, redirect_after=True,
57 def __get_rev_or_redirect(self, ref, repo, redirect_after=True,
58 partial=False):
58 partial=False):
59 """
59 """
60 Safe way to get changeset if error occur it redirects to changeset with
60 Safe way to get changeset if error occur it redirects to changeset with
61 proper message. If partial is set then don't do redirect raise Exception
61 proper message. If partial is set then don't do redirect raise Exception
62 instead
62 instead
63
63
64 :param rev: revision to fetch
64 :param rev: revision to fetch
65 :param repo: repo instance
65 :param repo: repo instance
66 """
66 """
67
67
68 rev = ref[1] # default and used for git
68 rev = ref[1] # default and used for git
69 if repo.scm_instance.alias == 'hg':
69 if repo.scm_instance.alias == 'hg':
70 # lookup up the exact node id
70 # lookup up the exact node id
71 _revset_predicates = {
71 _revset_predicates = {
72 'branch': 'branch',
72 'branch': 'branch',
73 'book': 'bookmark',
73 'book': 'bookmark',
74 'tag': 'tag',
74 'tag': 'tag',
75 'rev': 'id',
75 'rev': 'id',
76 }
76 }
77 rev_spec = "max(%s(%%s))" % _revset_predicates[ref[0]]
77 rev_spec = "max(%s(%%s))" % _revset_predicates[ref[0]]
78 revs = repo.scm_instance._repo.revs(rev_spec, safe_str(ref[1]))
78 revs = repo.scm_instance._repo.revs(rev_spec, safe_str(ref[1]))
79 if revs:
79 if revs:
80 rev = revs[-1]
80 rev = revs[-1]
81 # else: TODO: just report 'not found'
81 # else: TODO: just report 'not found'
82
82
83 try:
83 try:
84 return repo.scm_instance.get_changeset(rev)
84 return repo.scm_instance.get_changeset(rev).raw_id
85 except EmptyRepositoryError, e:
85 except EmptyRepositoryError, e:
86 if not redirect_after:
86 if not redirect_after:
87 return None
87 return None
88 h.flash(h.literal(_('There are no changesets yet')),
88 h.flash(h.literal(_('There are no changesets yet')),
89 category='warning')
89 category='warning')
90 redirect(url('summary_home', repo_name=repo.repo_name))
90 redirect(url('summary_home', repo_name=repo.repo_name))
91
91
92 except RepositoryError, e:
92 except RepositoryError, e:
93 log.error(traceback.format_exc())
93 log.error(traceback.format_exc())
94 h.flash(str(e), category='warning')
94 h.flash(str(e), category='warning')
95 if not partial:
95 if not partial:
96 redirect(h.url('summary_home', repo_name=repo.repo_name))
96 redirect(h.url('summary_home', repo_name=repo.repo_name))
97 raise HTTPBadRequest()
97 raise HTTPBadRequest()
98
98
99 def _get_changesets(self, alias, org_repo, org_ref, other_repo, other_ref, merge):
99 def _get_changesets(self, alias, org_repo, org_ref, other_repo, other_ref, merge):
100 """
100 """
101 Returns a list of changesets that can be merged from org_repo@org_ref
101 Returns a list of changesets that can be merged from org_repo@org_ref
102 to other_repo@other_ref ... and the ancestor that would be used for merge
102 to other_repo@other_ref ... and the ancestor that would be used for merge
103
103
104 :param org_repo:
104 :param org_repo:
105 :param org_ref:
105 :param org_ref:
106 :param other_repo:
106 :param other_repo:
107 :param other_ref:
107 :param other_ref:
108 :param tmp:
108 :param tmp:
109 """
109 """
110
110
111 ancestor = None
111 ancestor = None
112
112
113 if alias == 'hg':
113 if alias == 'hg':
114 # lookup up the exact node id
114 # lookup up the exact node id
115 _revset_predicates = {
115 _revset_predicates = {
116 'branch': 'branch',
116 'branch': 'branch',
117 'book': 'bookmark',
117 'book': 'bookmark',
118 'tag': 'tag',
118 'tag': 'tag',
119 'rev': 'id',
119 'rev': 'id',
120 }
120 }
121
121
122 org_rev_spec = "max(%s(%%s))" % _revset_predicates[org_ref[0]]
122 org_rev_spec = "max(%s(%%s))" % _revset_predicates[org_ref[0]]
123 org_revs = org_repo._repo.revs(org_rev_spec, safe_str(org_ref[1]))
123 org_revs = org_repo._repo.revs(org_rev_spec, safe_str(org_ref[1]))
124 org_rev = org_repo._repo[org_revs[-1] if org_revs else -1].hex()
124 org_rev = org_repo._repo[org_revs[-1] if org_revs else -1].hex()
125
125
126 other_revs_spec = "max(%s(%%s))" % _revset_predicates[other_ref[0]]
126 other_revs_spec = "max(%s(%%s))" % _revset_predicates[other_ref[0]]
127 other_revs = other_repo._repo.revs(other_revs_spec, safe_str(other_ref[1]))
127 other_revs = other_repo._repo.revs(other_revs_spec, safe_str(other_ref[1]))
128 other_rev = other_repo._repo[other_revs[-1] if other_revs else -1].hex()
128 other_rev = other_repo._repo[other_revs[-1] if other_revs else -1].hex()
129
129
130 #case two independent repos
130 #case two independent repos
131 if org_repo != other_repo:
131 if org_repo != other_repo:
132 hgrepo = unionrepo.unionrepository(other_repo.baseui,
132 hgrepo = unionrepo.unionrepository(other_repo.baseui,
133 other_repo.path,
133 other_repo.path,
134 org_repo.path)
134 org_repo.path)
135 # all the changesets we are looking for will be in other_repo,
135 # all the changesets we are looking for will be in other_repo,
136 # so rev numbers from hgrepo can be used in other_repo
136 # so rev numbers from hgrepo can be used in other_repo
137
137
138 #no remote compare do it on the same repository
138 #no remote compare do it on the same repository
139 else:
139 else:
140 hgrepo = other_repo._repo
140 hgrepo = other_repo._repo
141
141
142 if merge:
142 if merge:
143 revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
143 revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
144 other_rev, org_rev, org_rev)
144 other_rev, org_rev, org_rev)
145
145
146 ancestors = hgrepo.revs("ancestor(id(%s), id(%s))", org_rev, other_rev)
146 ancestors = hgrepo.revs("ancestor(id(%s), id(%s))", org_rev, other_rev)
147 if ancestors:
147 if ancestors:
148 # pick arbitrary ancestor - but there is usually only one
148 # pick arbitrary ancestor - but there is usually only one
149 ancestor = hgrepo[ancestors[0]].hex()
149 ancestor = hgrepo[ancestors[0]].hex()
150 else:
150 else:
151 # TODO: have both + and - changesets
151 # TODO: have both + and - changesets
152 revs = hgrepo.revs("id(%s) :: id(%s) - id(%s)",
152 revs = hgrepo.revs("id(%s) :: id(%s) - id(%s)",
153 org_rev, other_rev, org_rev)
153 org_rev, other_rev, org_rev)
154
154
155 changesets = [other_repo.get_changeset(rev) for rev in revs]
155 changesets = [other_repo.get_changeset(rev) for rev in revs]
156
156
157 elif alias == 'git':
157 elif alias == 'git':
158 if org_repo != other_repo:
158 if org_repo != other_repo:
159 raise Exception('Comparing of different GIT repositories is not'
159 raise Exception('Comparing of different GIT repositories is not'
160 'allowed. Got %s != %s' % (org_repo, other_repo))
160 'allowed. Got %s != %s' % (org_repo, other_repo))
161
161
162 so, se = org_repo.run_git_command(
162 so, se = org_repo.run_git_command(
163 'log --reverse --pretty="format: %%H" -s -p %s..%s'
163 'log --reverse --pretty="format: %%H" -s -p %s..%s'
164 % (org_ref[1], other_ref[1])
164 % (org_ref[1], other_ref[1])
165 )
165 )
166 changesets = [org_repo.get_changeset(cs)
166 changesets = [org_repo.get_changeset(cs)
167 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
167 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
168
168
169 return changesets, ancestor
169 return changesets, ancestor
170
170
171 @LoginRequired()
171 @LoginRequired()
172 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
172 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
173 'repository.admin')
173 'repository.admin')
174 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
174 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
175 # org_ref will be evaluated in org_repo
175 # org_ref will be evaluated in org_repo
176 org_repo = c.rhodecode_db_repo.repo_name
176 org_repo = c.rhodecode_db_repo.repo_name
177 org_ref = (org_ref_type, org_ref)
177 org_ref = (org_ref_type, org_ref)
178 # other_ref will be evaluated in other_repo
178 # other_ref will be evaluated in other_repo
179 other_ref = (other_ref_type, other_ref)
179 other_ref = (other_ref_type, other_ref)
180 other_repo = request.GET.get('other_repo', org_repo)
180 other_repo = request.GET.get('other_repo', org_repo)
181 # If merge is True:
181 # If merge is True:
182 # Show what org would get if merged with other:
182 # Show what org would get if merged with other:
183 # List changesets that are ancestors of other but not of org.
183 # List changesets that are ancestors of other but not of org.
184 # New changesets in org is thus ignored.
184 # New changesets in org is thus ignored.
185 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
185 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
186 # If merge is False:
186 # If merge is False:
187 # Make a raw diff from org to other, no matter if related or not.
187 # Make a raw diff from org to other, no matter if related or not.
188 # Changesets in one and not in the other will be ignored
188 # Changesets in one and not in the other will be ignored
189 merge = bool(request.GET.get('merge'))
189 merge = bool(request.GET.get('merge'))
190 # fulldiff disables cut_off_limit
190 # fulldiff disables cut_off_limit
191 c.fulldiff = request.GET.get('fulldiff')
191 c.fulldiff = request.GET.get('fulldiff')
192 # partial uses compare_cs.html template directly
192 # partial uses compare_cs.html template directly
193 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
193 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
194 # as_form puts hidden input field with changeset revisions
194 # as_form puts hidden input field with changeset revisions
195 c.as_form = partial and request.GET.get('as_form')
195 c.as_form = partial and request.GET.get('as_form')
196 # swap url for compare_diff page - never partial and never as_form
196 # swap url for compare_diff page - never partial and never as_form
197 c.swap_url = h.url('compare_url',
197 c.swap_url = h.url('compare_url',
198 repo_name=other_repo,
198 repo_name=other_repo,
199 org_ref_type=other_ref[0], org_ref=other_ref[1],
199 org_ref_type=other_ref[0], org_ref=other_ref[1],
200 other_repo=org_repo,
200 other_repo=org_repo,
201 other_ref_type=org_ref[0], other_ref=org_ref[1],
201 other_ref_type=org_ref[0], other_ref=org_ref[1],
202 merge=merge or '')
202 merge=merge or '')
203
203
204 org_repo = Repository.get_by_repo_name(org_repo)
204 org_repo = Repository.get_by_repo_name(org_repo)
205 other_repo = Repository.get_by_repo_name(other_repo)
205 other_repo = Repository.get_by_repo_name(other_repo)
206
206
207 if org_repo is None:
207 if org_repo is None:
208 log.error('Could not find org repo %s' % org_repo)
208 log.error('Could not find org repo %s' % org_repo)
209 raise HTTPNotFound
209 raise HTTPNotFound
210 if other_repo is None:
210 if other_repo is None:
211 log.error('Could not find other repo %s' % other_repo)
211 log.error('Could not find other repo %s' % other_repo)
212 raise HTTPNotFound
212 raise HTTPNotFound
213
213
214 if org_repo != other_repo and h.is_git(org_repo):
214 if org_repo != other_repo and h.is_git(org_repo):
215 log.error('compare of two remote repos not available for GIT REPOS')
215 log.error('compare of two remote repos not available for GIT REPOS')
216 raise HTTPNotFound
216 raise HTTPNotFound
217
217
218 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
218 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
219 log.error('compare of two different kind of remote repos not available')
219 log.error('compare of two different kind of remote repos not available')
220 raise HTTPNotFound
220 raise HTTPNotFound
221
221
222 self.__get_cs_or_redirect(ref=org_ref, repo=org_repo, partial=partial)
222 org_rev = self.__get_rev_or_redirect(ref=org_ref, repo=org_repo, partial=partial)
223 self.__get_cs_or_redirect(ref=other_ref, repo=other_repo, partial=partial)
223 other_rev = self.__get_rev_or_redirect(ref=other_ref, repo=other_repo, partial=partial)
224
224
225 c.org_repo = org_repo
225 c.org_repo = org_repo
226 c.other_repo = other_repo
226 c.other_repo = other_repo
227 c.org_ref = org_ref[1]
227 c.org_ref = org_ref[1]
228 c.other_ref = other_ref[1]
228 c.other_ref = other_ref[1]
229 c.org_ref_type = org_ref[0]
229 c.org_ref_type = org_ref[0]
230 c.other_ref_type = other_ref[0]
230 c.other_ref_type = other_ref[0]
231
231
232 c.cs_ranges, c.ancestor = self._get_changesets(org_repo.scm_instance.alias,
232 c.cs_ranges, c.ancestor = self._get_changesets(org_repo.scm_instance.alias,
233 org_repo.scm_instance, org_ref,
233 org_repo.scm_instance, org_ref,
234 other_repo.scm_instance, other_ref,
234 other_repo.scm_instance, other_ref,
235 merge)
235 merge)
236
236
237 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
237 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
238 c.cs_ranges])
238 c.cs_ranges])
239 if merge and not c.ancestor:
239 if merge and not c.ancestor:
240 log.error('Unable to find ancestor revision')
240 log.error('Unable to find ancestor revision')
241
241
242 if partial:
242 if partial:
243 return render('compare/compare_cs.html')
243 return render('compare/compare_cs.html')
244
244
245 if c.ancestor:
245 if c.ancestor:
246 assert merge
246 assert merge
247 # case we want a simple diff without incoming changesets,
247 # case we want a simple diff without incoming changesets,
248 # previewing what will be merged.
248 # previewing what will be merged.
249 # Make the diff on the other repo (which is known to have other_ref)
249 # Make the diff on the other repo (which is known to have other_ref)
250 log.debug('Using ancestor %s as org_ref instead of %s'
250 log.debug('Using ancestor %s as org_ref instead of %s'
251 % (c.ancestor, org_ref))
251 % (c.ancestor, org_ref))
252 org_ref = ('rev', c.ancestor)
252 org_rev = c.ancestor
253 org_repo = other_repo
253 org_repo = other_repo
254
254
255 diff_limit = self.cut_off_limit if not c.fulldiff else None
255 diff_limit = self.cut_off_limit if not c.fulldiff else None
256
256
257 log.debug('running diff between %s and %s in %s'
257 log.debug('running diff between %s and %s in %s'
258 % (org_ref, other_ref, org_repo.scm_instance.path))
258 % (org_rev, other_rev, org_repo.scm_instance.path))
259 txtdiff = org_repo.scm_instance.get_diff(rev1=safe_str(org_ref[1]), rev2=safe_str(other_ref[1]))
259 txtdiff = org_repo.scm_instance.get_diff(rev1=org_rev, rev2=other_rev)
260
260
261 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
261 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
262 diff_limit=diff_limit)
262 diff_limit=diff_limit)
263 _parsed = diff_processor.prepare()
263 _parsed = diff_processor.prepare()
264
264
265 c.limited_diff = False
265 c.limited_diff = False
266 if isinstance(_parsed, LimitedDiffContainer):
266 if isinstance(_parsed, LimitedDiffContainer):
267 c.limited_diff = True
267 c.limited_diff = True
268
268
269 c.files = []
269 c.files = []
270 c.changes = {}
270 c.changes = {}
271 c.lines_added = 0
271 c.lines_added = 0
272 c.lines_deleted = 0
272 c.lines_deleted = 0
273 for f in _parsed:
273 for f in _parsed:
274 st = f['stats']
274 st = f['stats']
275 if not st['binary']:
275 if not st['binary']:
276 c.lines_added += st['added']
276 c.lines_added += st['added']
277 c.lines_deleted += st['deleted']
277 c.lines_deleted += st['deleted']
278 fid = h.FID('', f['filename'])
278 fid = h.FID('', f['filename'])
279 c.files.append([fid, f['operation'], f['filename'], f['stats']])
279 c.files.append([fid, f['operation'], f['filename'], f['stats']])
280 htmldiff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
280 htmldiff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
281 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
281 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
282
282
283 return render('compare/compare_diff.html')
283 return render('compare/compare_diff.html')
General Comments 0
You need to be logged in to leave comments. Login now