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 |
|
|
50 |
from rhodecode.lib.vcs.backends.base import |
|
|
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 |
|
|
|
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 |
@@ -840,11 +850,21 b' class PullrequestsController(BaseRepoCon' | |||
|
840 | 850 | statuses = ChangesetStatus.STATUSES |
|
841 | 851 | c.commit_statuses = statuses |
|
842 | 852 | |
|
843 | c.ancestor = None # TODO: add ancestor here | |
|
853 | c.ancestor = None # TODO: add ancestor here | |
|
844 | 854 | c.pull_request = pull_request_display_obj |
|
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' % ( |
|
|
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,22 +112,26 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 |
<div class=" |
|
|
117 | <div class="label-summary"> | |
|
118 | <label>Merge:</label> | |
|
115 | <div class="field"> | |
|
116 | <div class="label-summary"> | |
|
117 | <label>${_('Merge')}:</label> | |
|
118 | </div> | |
|
119 | <div class="input"> | |
|
120 | % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref: | |
|
121 | <div class="pr-mergeinfo"> | |
|
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"> | |
|
124 | %elif h.is_git(c.pull_request.target_repo): | |
|
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 | |
|
119 | 127 | </div> |
|
120 |
|
|
|
121 |
|
|
|
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"> | |
|
124 | %elif h.is_git(c.pull_request.target_repo): | |
|
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 | |
|
127 | </div> | |
|
128 | % else: | |
|
129 | <div class=""> | |
|
130 | ${_('Shadow repository data not available')}. | |
|
128 | 131 | </div> |
|
132 | % endif | |
|
129 | 133 | </div> |
|
130 |
|
|
|
134 | </div> | |
|
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 |
|
|
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 |
|
|
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. |
|
|
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 |
< |
|
|
364 | <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a> | |
|
333 | 365 | % else: |
|
334 |
< |
|
|
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