##// 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 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 import types
24 25
25 26 import peppercorn
26 27 import formencode
27 28 import logging
28 29
30
29 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 32 from pylons import request, tmpl_context as c, url
31 33 from pylons.controllers.util import redirect
@@ -46,8 +48,9 b' from rhodecode.lib.channelstream import '
46 48 from rhodecode.lib.compat import OrderedDict
47 49 from rhodecode.lib.utils import jsonify
48 50 from rhodecode.lib.utils2 import (
49 safe_int, safe_str, str2bool, safe_unicode, StrictAttributeDict)
50 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
51 safe_int, safe_str, str2bool, safe_unicode)
52 from rhodecode.lib.vcs.backends.base import (
53 EmptyCommit, UpdateFailureReason, EmptyRepository)
51 54 from rhodecode.lib.vcs.exceptions import (
52 55 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
53 56 NodeDoesNotExistError)
@@ -680,7 +683,13 b' class PullrequestsController(BaseRepoCon'
680 683 def _get_pr_version(self, pull_request_id, version=None):
681 684 pull_request_id = safe_int(pull_request_id)
682 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 693 pull_request_ver = PullRequestVersion.get_or_404(version)
685 694 pull_request_obj = pull_request_ver
686 695 _org_pull_request_obj = pull_request_ver.pull_request
@@ -688,57 +697,58 b' class PullrequestsController(BaseRepoCon'
688 697 else:
689 698 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
690 699
691 class PullRequestDisplay(object):
692 """
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
700 pull_request_display_obj = PullRequest.get_pr_display_object(
701 pull_request_obj, _org_pull_request_obj)
739 702 return _org_pull_request_obj, pull_request_obj, \
740 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 752 @LoginRequired()
743 753 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
744 754 'repository.admin')
@@ -763,7 +773,7 b' class PullrequestsController(BaseRepoCon'
763 773 pull_request_at_ver)
764 774
765 775 pr_closed = pull_request_latest.is_closed()
766 if at_version:
776 if at_version and not at_version == 'latest':
767 777 c.allowed_to_change_status = False
768 778 c.allowed_to_update = False
769 779 c.allowed_to_merge = False
@@ -845,6 +855,16 b' class PullrequestsController(BaseRepoCon'
845 855 c.pull_request_latest = pull_request_latest
846 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 868 return render('/pullrequests/pullrequest_show.html')
849 869
850 870 @LoginRequired()
@@ -665,7 +665,8 b' class StrictAttributeDict(dict):'
665 665 try:
666 666 return self[attr]
667 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 670 __setattr__ = dict.__setitem__
670 671 __delattr__ = dict.__delitem__
671 672
@@ -1442,6 +1442,15 b' class EmptyChangeset(EmptyCommit):'
1442 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 1454 class CollectionGenerator(object):
1446 1455
1447 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 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 glob2re)
56 glob2re, StrictAttributeDict)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
@@ -3213,6 +3213,64 b' class PullRequest(Base, _PullRequestBase'
3213 3213 cascade="all, delete, delete-orphan",
3214 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 3274 def is_closed(self):
3217 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 1395 // new entry in group_members
1386 1396 .td-author-new-entry {
1387 1397 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
@@ -45,7 +45,7 b''
45 45 <div class="summary-details block-left">
46 46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
47 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 49 %if c.allowed_to_update:
50 50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 51 % if c.allowed_to_delete:
@@ -112,12 +112,12 b''
112 112 </div>
113 113
114 114 ## Link to the shadow repository.
115 %if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
116 115 <div class="field">
117 116 <div class="label-summary">
118 <label>Merge:</label>
117 <label>${_('Merge')}:</label>
119 118 </div>
120 119 <div class="input">
120 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
121 121 <div class="pr-mergeinfo">
122 122 %if h.is_hg(c.pull_request.target_repo):
123 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 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 126 %endif
127 127 </div>
128 % else:
129 <div class="">
130 ${_('Shadow repository data not available')}.
131 </div>
132 % endif
128 133 </div>
129 134 </div>
130 %endif
131 135
132 136 <div class="field">
133 137 <div class="label-summary">
@@ -187,21 +191,23 b''
187 191
188 192 <div class="field">
189 193 <div class="label-summary">
190 <label>${_('Versions')}:</label>
194 <label>${_('Versions')} (${len(c.versions)}):</label>
191 195 </div>
196
192 197 <div>
198 % if c.show_version_changes:
193 199 <table>
194 200 <tr>
195 201 <td>
196 % if c.at_version == None:
202 % if c.at_version in [None, 'latest']:
197 203 <i class="icon-ok link"></i>
198 204 % endif
199 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 207 <td>
202 208 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
203 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 211 </tr>
206 212 % for ver in reversed(c.pull_request.versions()):
207 213 <tr>
@@ -214,10 +220,36 b''
214 220 <td>
215 221 <code>${ver.source_ref_parts.commit_id[:6]}</code>
216 222 </td>
217 <td>${_('created')} ${h.age_component(ver.created_on)}</td>
223 <td>${_('created')} ${h.age_component(ver.updated_on)}</td>
218 224 </tr>
219 225 % endfor
220 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 253 </div>
222 254 </div>
223 255
@@ -329,9 +361,9 b''
329 361 % endif
330 362 <div class="compare_view_commits_title">
331 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 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 367 % endif
336 368 % if len(c.commit_ranges):
337 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 1 ## -*- coding: utf-8 -*-
2 Auto status change to |under_review|
2 Pull request updated. Auto status change to |under_review|
3 3
4 4 .. role:: added
5 5 .. role:: removed
@@ -95,7 +95,7 b' def test_rst_xss_raw_directive():'
95 95
96 96 def test_render_rst_template_without_files():
97 97 expected = u'''\
98 Auto status change to |under_review|
98 Pull request updated. Auto status change to |under_review|
99 99
100 100 .. role:: added
101 101 .. role:: removed
@@ -125,7 +125,7 b' Auto status change to |under_review|'
125 125
126 126 def test_render_rst_template_with_files():
127 127 expected = u'''\
128 Auto status change to |under_review|
128 Pull request updated. Auto status change to |under_review|
129 129
130 130 .. role:: added
131 131 .. role:: removed
@@ -722,7 +722,7 b' def test_update_adds_a_comment_to_the_pu'
722 722 # Expect to find a new comment about the change
723 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 727 .. role:: added
728 728 .. role:: removed
General Comments 0
You need to be logged in to leave comments. Login now