##// END OF EJS Templates
pull-requests: expose version browsing of pull requests....
marcink -
r1255:952cdf08 default
parent child Browse files
Show More
@@ -21,11 +21,13 b''
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24
25
25 import peppercorn
26 import peppercorn
26 import formencode
27 import formencode
27 import logging
28 import logging
28
29
30
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
@@ -46,8 +48,9 b' from rhodecode.lib.channelstream import '
46 from rhodecode.lib.compat import OrderedDict
48 from rhodecode.lib.compat import OrderedDict
47 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils2 import (
50 from rhodecode.lib.utils2 import (
49 safe_int, safe_str, str2bool, safe_unicode, StrictAttributeDict)
51 safe_int, safe_str, str2bool, safe_unicode)
50 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
52 from rhodecode.lib.vcs.backends.base import (
53 EmptyCommit, UpdateFailureReason, EmptyRepository)
51 from rhodecode.lib.vcs.exceptions import (
54 from rhodecode.lib.vcs.exceptions import (
52 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
53 NodeDoesNotExistError)
56 NodeDoesNotExistError)
@@ -680,7 +683,13 b' class PullrequestsController(BaseRepoCon'
680 def _get_pr_version(self, pull_request_id, version=None):
683 def _get_pr_version(self, pull_request_id, version=None):
681 pull_request_id = safe_int(pull_request_id)
684 pull_request_id = safe_int(pull_request_id)
682 at_version = None
685 at_version = None
683 if version:
686
687 if version and version == 'latest':
688 pull_request_ver = PullRequest.get(pull_request_id)
689 pull_request_obj = pull_request_ver
690 _org_pull_request_obj = pull_request_obj
691 at_version = 'latest'
692 elif version:
684 pull_request_ver = PullRequestVersion.get_or_404(version)
693 pull_request_ver = PullRequestVersion.get_or_404(version)
685 pull_request_obj = pull_request_ver
694 pull_request_obj = pull_request_ver
686 _org_pull_request_obj = pull_request_ver.pull_request
695 _org_pull_request_obj = pull_request_ver.pull_request
@@ -688,57 +697,58 b' class PullrequestsController(BaseRepoCon'
688 else:
697 else:
689 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
698 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
690
699
691 class PullRequestDisplay(object):
700 pull_request_display_obj = PullRequest.get_pr_display_object(
692 """
701 pull_request_obj, _org_pull_request_obj)
693 Special object wrapper for showing PullRequest data via Versions
694 It mimics PR object as close as possible. This is read only object
695 just for display
696 """
697 def __init__(self, attrs):
698 self.attrs = attrs
699 # internal have priority over the given ones via attrs
700 self.internal = ['versions']
701
702 def __getattr__(self, item):
703 if item in self.internal:
704 return getattr(self, item)
705 try:
706 return self.attrs[item]
707 except KeyError:
708 raise AttributeError(
709 '%s object has no attribute %s' % (self, item))
710
711 def versions(self):
712 return pull_request_obj.versions.order_by(
713 PullRequestVersion.pull_request_version_id).all()
714
715 def is_closed(self):
716 return pull_request_obj.is_closed()
717
718 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
719
720 attrs.author = StrictAttributeDict(
721 pull_request_obj.author.get_api_data())
722 if pull_request_obj.target_repo:
723 attrs.target_repo = StrictAttributeDict(
724 pull_request_obj.target_repo.get_api_data())
725 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
726
727 if pull_request_obj.source_repo:
728 attrs.source_repo = StrictAttributeDict(
729 pull_request_obj.source_repo.get_api_data())
730 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
731
732 attrs.source_ref_parts = pull_request_obj.source_ref_parts
733 attrs.target_ref_parts = pull_request_obj.target_ref_parts
734
735 attrs.shadow_merge_ref = _org_pull_request_obj.shadow_merge_ref
736
737 pull_request_display_obj = PullRequestDisplay(attrs)
738
739 return _org_pull_request_obj, pull_request_obj, \
702 return _org_pull_request_obj, pull_request_obj, \
740 pull_request_display_obj, at_version
703 pull_request_display_obj, at_version
741
704
705 def _get_pr_version_changes(self, version, pull_request_latest):
706 """
707 Generate changes commits, and diff data based on the current pr version
708 """
709
710 #TODO(marcink): save those changes as JSON metadata for chaching later.
711
712 # fake the version to add the "initial" state object
713 pull_request_initial = PullRequest.get_pr_display_object(
714 pull_request_latest, pull_request_latest,
715 internal_methods=['get_commit', 'versions'])
716 pull_request_initial.revisions = []
717 pull_request_initial.source_repo.get_commit = types.MethodType(
718 lambda *a, **k: EmptyCommit(), pull_request_initial)
719 pull_request_initial.source_repo.scm_instance = types.MethodType(
720 lambda *a, **k: EmptyRepository(), pull_request_initial)
721
722 _changes_versions = [pull_request_latest] + \
723 list(reversed(c.versions)) + \
724 [pull_request_initial]
725
726 if version == 'latest':
727 index = 0
728 else:
729 for pos, prver in enumerate(_changes_versions):
730 ver = getattr(prver, 'pull_request_version_id', -1)
731 if ver == safe_int(version):
732 index = pos
733 break
734 else:
735 index = 0
736
737 cur_obj = _changes_versions[index]
738 prev_obj = _changes_versions[index + 1]
739
740 old_commit_ids = set(prev_obj.revisions)
741 new_commit_ids = set(cur_obj.revisions)
742
743 changes = PullRequestModel()._calculate_commit_id_changes(
744 old_commit_ids, new_commit_ids)
745
746 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
747 cur_obj, prev_obj)
748 file_changes = PullRequestModel()._calculate_file_changes(
749 old_diff_data, new_diff_data)
750 return changes, file_changes
751
742 @LoginRequired()
752 @LoginRequired()
743 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
753 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
744 'repository.admin')
754 'repository.admin')
@@ -763,7 +773,7 b' class PullrequestsController(BaseRepoCon'
763 pull_request_at_ver)
773 pull_request_at_ver)
764
774
765 pr_closed = pull_request_latest.is_closed()
775 pr_closed = pull_request_latest.is_closed()
766 if at_version:
776 if at_version and not at_version == 'latest':
767 c.allowed_to_change_status = False
777 c.allowed_to_change_status = False
768 c.allowed_to_update = False
778 c.allowed_to_update = False
769 c.allowed_to_merge = False
779 c.allowed_to_merge = False
@@ -845,6 +855,16 b' class PullrequestsController(BaseRepoCon'
845 c.pull_request_latest = pull_request_latest
855 c.pull_request_latest = pull_request_latest
846 c.at_version = at_version
856 c.at_version = at_version
847
857
858 c.versions = pull_request_display_obj.versions()
859 c.changes = None
860 c.file_changes = None
861
862 c.show_version_changes = 1
863
864 if at_version and c.show_version_changes:
865 c.changes, c.file_changes = self._get_pr_version_changes(
866 version, pull_request_latest)
867
848 return render('/pullrequests/pullrequest_show.html')
868 return render('/pullrequests/pullrequest_show.html')
849
869
850 @LoginRequired()
870 @LoginRequired()
@@ -665,7 +665,8 b' class StrictAttributeDict(dict):'
665 try:
665 try:
666 return self[attr]
666 return self[attr]
667 except KeyError:
667 except KeyError:
668 raise AttributeError('%s object has no attribute %s' % (self, attr))
668 raise AttributeError('%s object has no attribute %s' % (
669 self.__class__, attr))
669 __setattr__ = dict.__setitem__
670 __setattr__ = dict.__setitem__
670 __delattr__ = dict.__delitem__
671 __delattr__ = dict.__delitem__
671
672
@@ -1442,6 +1442,15 b' class EmptyChangeset(EmptyCommit):'
1442 self.idx = value
1442 self.idx = value
1443
1443
1444
1444
1445 class EmptyRepository(BaseRepository):
1446 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1447 pass
1448
1449 def get_diff(self, *args, **kwargs):
1450 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1451 return GitDiff('')
1452
1453
1445 class CollectionGenerator(object):
1454 class CollectionGenerator(object):
1446
1455
1447 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1456 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
@@ -53,7 +53,7 b' from rhodecode.lib.vcs.backends.base imp'
53 from rhodecode.lib.utils2 import (
53 from rhodecode.lib.utils2 import (
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 glob2re)
56 glob2re, StrictAttributeDict)
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.ext_json import json
59 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.caching_query import FromCache
@@ -3213,6 +3213,64 b' class PullRequest(Base, _PullRequestBase'
3213 cascade="all, delete, delete-orphan",
3213 cascade="all, delete, delete-orphan",
3214 lazy='dynamic')
3214 lazy='dynamic')
3215
3215
3216
3217 @classmethod
3218 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3219 internal_methods=None):
3220
3221 class PullRequestDisplay(object):
3222 """
3223 Special object wrapper for showing PullRequest data via Versions
3224 It mimics PR object as close as possible. This is read only object
3225 just for display
3226 """
3227
3228 def __init__(self, attrs, internal=None):
3229 self.attrs = attrs
3230 # internal have priority over the given ones via attrs
3231 self.internal = internal or ['versions']
3232
3233 def __getattr__(self, item):
3234 if item in self.internal:
3235 return getattr(self, item)
3236 try:
3237 return self.attrs[item]
3238 except KeyError:
3239 raise AttributeError(
3240 '%s object has no attribute %s' % (self, item))
3241
3242 def __repr__(self):
3243 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3244
3245 def versions(self):
3246 return pull_request_obj.versions.order_by(
3247 PullRequestVersion.pull_request_version_id).all()
3248
3249 def is_closed(self):
3250 return pull_request_obj.is_closed()
3251
3252 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3253
3254 attrs.author = StrictAttributeDict(
3255 pull_request_obj.author.get_api_data())
3256 if pull_request_obj.target_repo:
3257 attrs.target_repo = StrictAttributeDict(
3258 pull_request_obj.target_repo.get_api_data())
3259 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3260
3261 if pull_request_obj.source_repo:
3262 attrs.source_repo = StrictAttributeDict(
3263 pull_request_obj.source_repo.get_api_data())
3264 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3265
3266 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3267 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3268 attrs.revisions = pull_request_obj.revisions
3269
3270 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3271
3272 return PullRequestDisplay(attrs, internal=internal_methods)
3273
3216 def is_closed(self):
3274 def is_closed(self):
3217 return self.status == self.STATUS_CLOSED
3275 return self.status == self.STATUS_CLOSED
3218
3276
@@ -1382,6 +1382,16 b' table.integrations {'
1382 }
1382 }
1383 }
1383 }
1384
1384
1385 .compare_view_commits_title {
1386 .disabled {
1387 cursor: inherit;
1388 &:hover{
1389 background-color: inherit;
1390 color: inherit;
1391 }
1392 }
1393 }
1394
1385 // new entry in group_members
1395 // new entry in group_members
1386 .td-author-new-entry {
1396 .td-author-new-entry {
1387 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1397 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
@@ -45,7 +45,7 b''
45 <div class="summary-details block-left">
45 <div class="summary-details block-left">
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
47 <div class="pr-details-title">
47 <div class="pr-details-title">
48 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('From')} ${h.format_date(c.pull_request.created_on)}
48 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 %if c.allowed_to_update:
49 %if c.allowed_to_update:
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 % if c.allowed_to_delete:
51 % if c.allowed_to_delete:
@@ -112,12 +112,12 b''
112 </div>
112 </div>
113
113
114 ## Link to the shadow repository.
114 ## Link to the shadow repository.
115 %if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
116 <div class="field">
115 <div class="field">
117 <div class="label-summary">
116 <div class="label-summary">
118 <label>Merge:</label>
117 <label>${_('Merge')}:</label>
119 </div>
118 </div>
120 <div class="input">
119 <div class="input">
120 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
121 <div class="pr-mergeinfo">
121 <div class="pr-mergeinfo">
122 %if h.is_hg(c.pull_request.target_repo):
122 %if h.is_hg(c.pull_request.target_repo):
123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
@@ -125,9 +125,13 b''
125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 %endif
126 %endif
127 </div>
127 </div>
128 % else:
129 <div class="">
130 ${_('Shadow repository data not available')}.
131 </div>
132 % endif
128 </div>
133 </div>
129 </div>
134 </div>
130 %endif
131
135
132 <div class="field">
136 <div class="field">
133 <div class="label-summary">
137 <div class="label-summary">
@@ -187,21 +191,23 b''
187
191
188 <div class="field">
192 <div class="field">
189 <div class="label-summary">
193 <div class="label-summary">
190 <label>${_('Versions')}:</label>
194 <label>${_('Versions')} (${len(c.versions)}):</label>
191 </div>
195 </div>
196
192 <div>
197 <div>
198 % if c.show_version_changes:
193 <table>
199 <table>
194 <tr>
200 <tr>
195 <td>
201 <td>
196 % if c.at_version == None:
202 % if c.at_version in [None, 'latest']:
197 <i class="icon-ok link"></i>
203 <i class="icon-ok link"></i>
198 % endif
204 % endif
199 </td>
205 </td>
200 <td><code><a href="${h.url.current()}">latest</a></code></td>
206 <td><code><a href="${h.url.current(version='latest')}">latest</a></code></td>
201 <td>
207 <td>
202 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
208 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
203 </td>
209 </td>
204 <td>${_('created')} ${h.age_component(c.pull_request.created_on)}</td>
210 <td>${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}</td>
205 </tr>
211 </tr>
206 % for ver in reversed(c.pull_request.versions()):
212 % for ver in reversed(c.pull_request.versions()):
207 <tr>
213 <tr>
@@ -214,10 +220,36 b''
214 <td>
220 <td>
215 <code>${ver.source_ref_parts.commit_id[:6]}</code>
221 <code>${ver.source_ref_parts.commit_id[:6]}</code>
216 </td>
222 </td>
217 <td>${_('created')} ${h.age_component(ver.created_on)}</td>
223 <td>${_('created')} ${h.age_component(ver.updated_on)}</td>
218 </tr>
224 </tr>
219 % endfor
225 % endfor
220 </table>
226 </table>
227
228 % if c.at_version:
229 <pre>
230 Changed commits:
231 * added: ${len(c.changes.added)}
232 * removed: ${len(c.changes.removed)}
233
234 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
235 No file changes found
236 % else:
237 Changed files:
238 %for file_name in c.file_changes.added:
239 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
240 %endfor
241 %for file_name in c.file_changes.modified:
242 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
243 %endfor
244 %for file_name in c.file_changes.removed:
245 * R ${file_name}
246 %endfor
247 % endif
248 </pre>
249 % endif
250 % else:
251 ${_('Pull request versions not available')}.
252 % endif
221 </div>
253 </div>
222 </div>
254 </div>
223
255
@@ -329,9 +361,9 b''
329 % endif
361 % endif
330 <div class="compare_view_commits_title">
362 <div class="compare_view_commits_title">
331 % if c.allowed_to_update and not c.pull_request.is_closed():
363 % if c.allowed_to_update and not c.pull_request.is_closed():
332 <button id="update_commits" class="btn pull-right">${_('Update commits')}</button>
364 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
333 % else:
365 % else:
334 <button class="btn disabled pull-right" disabled="disabled">${_('Update commits')}</button>
366 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
335 % endif
367 % endif
336 % if len(c.commit_ranges):
368 % if len(c.commit_ranges):
337 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
369 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
@@ -1,5 +1,5 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 Auto status change to |under_review|
2 Pull request updated. Auto status change to |under_review|
3
3
4 .. role:: added
4 .. role:: added
5 .. role:: removed
5 .. role:: removed
@@ -95,7 +95,7 b' def test_rst_xss_raw_directive():'
95
95
96 def test_render_rst_template_without_files():
96 def test_render_rst_template_without_files():
97 expected = u'''\
97 expected = u'''\
98 Auto status change to |under_review|
98 Pull request updated. Auto status change to |under_review|
99
99
100 .. role:: added
100 .. role:: added
101 .. role:: removed
101 .. role:: removed
@@ -125,7 +125,7 b' Auto status change to |under_review|'
125
125
126 def test_render_rst_template_with_files():
126 def test_render_rst_template_with_files():
127 expected = u'''\
127 expected = u'''\
128 Auto status change to |under_review|
128 Pull request updated. Auto status change to |under_review|
129
129
130 .. role:: added
130 .. role:: added
131 .. role:: removed
131 .. role:: removed
@@ -722,7 +722,7 b' def test_update_adds_a_comment_to_the_pu'
722 # Expect to find a new comment about the change
722 # Expect to find a new comment about the change
723 expected_message = textwrap.dedent(
723 expected_message = textwrap.dedent(
724 """\
724 """\
725 Auto status change to |under_review|
725 Pull request updated. Auto status change to |under_review|
726
726
727 .. role:: added
727 .. role:: added
728 .. role:: removed
728 .. role:: removed
General Comments 0
You need to be logged in to leave comments. Login now