changeset 4370:f295fad8adff

pull requests: make it possible to "update" PRs by creating a new PR based on the old one
author Mads Kiilerich <madski@unity3d.com>
date Fri, 18 Jul 2014 19:22:01 +0200
parents c7570745a2ea
children 1469afcacee5
files kallithea/config/routing.py kallithea/controllers/pullrequests.py kallithea/templates/pullrequests/pullrequest_show.html
diffstat 3 files changed, 177 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/kallithea/config/routing.py	Fri Jul 18 19:22:01 2014 +0200
+++ b/kallithea/config/routing.py	Fri Jul 18 19:22:01 2014 +0200
@@ -693,6 +693,11 @@
                  action='create', conditions=dict(function=check_repo,
                                                   method=["POST"]))
 
+    rmap.connect('pullrequest_copy_update',
+                 '/{repo_name:.*?}/pull-request-update/{pull_request_id}', controller='pullrequests',
+                 action='copy_update', conditions=dict(function=check_repo,
+                                                       method=["POST"]))
+
     rmap.connect('pullrequest_show',
                  '/{repo_name:.*?}/pull-request/{pull_request_id}',
                  controller='pullrequests',
--- a/kallithea/controllers/pullrequests.py	Fri Jul 18 19:22:01 2014 +0200
+++ b/kallithea/controllers/pullrequests.py	Fri Jul 18 19:22:01 2014 +0200
@@ -28,6 +28,7 @@
 import logging
 import traceback
 import formencode
+import re
 
 from webob.exc import HTTPNotFound, HTTPForbidden
 from collections import defaultdict
@@ -196,7 +197,7 @@
         c.other_repo = pull_request.other_repo
         (c.other_ref_type,
          c.other_ref_name,
-         c.other_rev) = pull_request.other_ref.split(':')
+         c.other_rev) = pull_request.other_ref.split(':') # other_rev is ancestor
 
         org_scm_instance = c.org_repo.scm_instance # property with expensive cache invalidation check!!!
         c.cs_repo = c.org_repo
@@ -205,6 +206,22 @@
         revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
         c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
 
+        c.available = []
+        c.org_branch_name = c.org_ref_name
+        if org_scm_instance.alias == 'hg' and c.other_ref_name != 'ancestor':
+            if c.org_ref_type != 'branch':
+                c.org_branch_name = org_scm_instance.get_changeset(c.org_ref_name).branch # use ref_type ?
+            other_branch_name = c.other_ref_name
+            if c.other_ref_type != 'branch':
+                other_branch_name = c.other_repo.scm_instance.get_changeset(c.other_ref_name).branch # use ref_type ?
+            # candidates: descendants of old head that are on the right branch
+            #             and not are the old head itself ...
+            #             and nothing at all if old head is a descendent of target ref name
+            arevs = org_scm_instance._repo.revs('%s:: & branch(%s) - %s - (%s&::%s)::',
+                                                revs[0], c.org_branch_name,
+                                                revs[0], revs[0], other_branch_name)
+            c.available = [org_scm_instance.get_changeset(x) for x in arevs]
+
         raw_ids = [x.raw_id for x in c.cs_ranges]
         c.cs_comments = c.org_repo.get_comments(raw_ids)
         c.statuses = c.org_repo.statuses(raw_ids)
@@ -391,7 +408,7 @@
                                               )
         revisions = [cs.raw_id for cs in cs_ranges]
 
-        # hack: ancestor_rev is not an other_ref but we want to show the
+        # hack: ancestor_rev is not an other_rev but we want to show the
         # requested destination and have the exact ancestor
         other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
 
@@ -423,6 +440,118 @@
     @NotAnonymous()
     @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                    'repository.admin')
+    def copy_update(self, repo_name, pull_request_id):
+        old_pull_request = PullRequest.get_or_404(pull_request_id)
+        assert old_pull_request.other_repo.repo_name == repo_name
+
+        org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
+        org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
+        updaterev = request.POST.get('updaterev')
+        if updaterev:
+            new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
+        else:
+            # assert org_ref_type == 'branch', org_ref_type # TODO: what if not?
+            new_org_rev = self._get_ref_rev(org_repo, org_ref_type, org_ref_name)
+
+        other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
+        other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
+        #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
+        new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
+
+        cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
+            other_repo.scm_instance, new_other_rev, # org and other "swapped"
+            org_repo.scm_instance, new_org_rev)
+
+        old_revisions = set(old_pull_request.revisions)
+        revisions = [cs.raw_id for cs in cs_ranges]
+        new_revisions = [r for r in revisions if r not in old_revisions]
+        lost = old_revisions.difference(revisions)
+
+        infos = ['','', 'This is an update of %s "%s".' %
+                 (url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
+                      pull_request_id=pull_request_id, qualified=True),
+                  old_pull_request.title)]
+
+        if lost:
+            infos.append(_('Missing changesets since the previous version:'))
+            for r in old_pull_request.revisions:
+                if r in lost:
+                    desc = org_repo.get_changeset(r).message.split('\n')[0]
+                    infos.append('  %s "%s"' % (h.short_id(r), desc))
+
+        if new_revisions:
+            infos.append(_('New changesets on %s %s since the previous version:') % (org_ref_type, org_ref_name))
+            for r in reversed(revisions):
+                if r in new_revisions:
+                    desc = org_repo.get_changeset(r).message.split('\n')[0]
+                    infos.append('  %s "%s"' % (h.short_id(r), desc))
+
+            if ancestor_rev == other_rev:
+                infos.append(_("Ancestor didn't change - show diff since previous version: %s .") %
+                             url('compare_url',
+                                 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
+                                 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
+                                 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
+                                 qualified=True)) # note: linear diff, merge or not doesn't matter
+            else:
+                infos.append(_('This pull request uses another merge ancestor than the previous version and they are not directly comparable.'))
+        else:
+           infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
+           # TODO: fail?
+
+        # hack: ancestor_rev is not an other_ref but we want to show the
+        # requested destination and have the exact ancestor
+        new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
+        new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
+
+        reviewers = [r.user_id for r in old_pull_request.reviewers]
+        try:
+            old_title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', old_pull_request.title).groups()
+            v = int(old_v) + 1
+        except (AttributeError, ValueError):
+            old_title = old_pull_request.title
+            v = 2
+        title = '%s (v%s)' % (old_title.strip(), v)
+        description = (old_pull_request.description.rstrip() +
+                       '\n'.join(infos))
+
+        try:
+            pull_request = PullRequestModel().create(
+                self.authuser.user_id,
+                old_pull_request.org_repo.repo_name, new_org_ref,
+                old_pull_request.other_repo.repo_name, new_other_ref,
+                revisions, reviewers, title, description
+            )
+        except Exception:
+            h.flash(_('Error occurred while creating pull request'),
+                    category='error')
+            log.error(traceback.format_exc())
+            return redirect(url('pullrequest_show', repo_name=repo_name,
+                                pull_request_id=pull_request_id))
+
+        comm = ChangesetCommentsModel().create(
+            text=_('Closed, replaced by %s .') % url('pullrequest_show',
+                                                   repo_name=old_pull_request.other_repo.repo_name,
+                                                   pull_request_id=pull_request.pull_request_id,
+                                                   qualified=True),
+            repo=old_pull_request.other_repo.repo_id,
+            user=c.authuser.user_id,
+            pull_request=pull_request_id,
+            closing_pr=True)
+        PullRequestModel().close_pull_request(pull_request_id)
+
+        Session().commit()
+        h.flash(_('Pull request update created'),
+                category='success')
+
+        return redirect(url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
+                            pull_request_id=pull_request.pull_request_id))
+
+    # pullrequest_post for PR editing
+    @LoginRequired()
+    @NotAnonymous()
+    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
+                                   'repository.admin')
     def post(self, repo_name, pull_request_id):
         repo = RepoModel()._get_repo(repo_name)
         pull_request = PullRequest.get_or_404(pull_request_id)
@@ -438,6 +567,7 @@
         return redirect(url('pullrequest_show', repo_name=repo.repo_name,
                             pull_request_id=pull_request_id))
 
+    # pullrequest_update for updating reviewer list
     @LoginRequired()
     @NotAnonymous()
     @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
--- a/kallithea/templates/pullrequests/pullrequest_show.html	Fri Jul 18 19:22:01 2014 +0200
+++ b/kallithea/templates/pullrequests/pullrequest_show.html	Fri Jul 18 19:22:01 2014 +0200
@@ -112,26 +112,25 @@
         </div>
         <div class="field">
           <div class="label-summary">
-              <label>${_('Origin repository')}:</label>
+              <label>${_('Origin')}:</label>
           </div>
           <div class="input">
-              <div>
-              <span><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name)}">${c.pull_request.org_repo.clone_url()}</a></span>
-
-              ## branch link is only valid if it is a branch
-              <span class="spantag"><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name, anchor=c.org_ref_name)}">${c.org_ref_type}: ${c.org_ref_name}</a></span>
-              </div>
+            <div>
+              ${h.link_to_ref(c.pull_request.org_repo.repo_name, c.org_ref_type, c.org_ref_name, c.org_rev)}
+              %if c.org_ref_type != 'branch':
+                ${_('on')} ${h.link_to_ref(c.pull_request.org_repo.repo_name, 'branch', c.org_branch_name)}
+              %endif
+            </div>
           </div>
         </div>
         <div class="field">
           <div class="label-summary">
-              <label>${_('Target repository')}:</label>
+              <label>${_('Target')}:</label>
           </div>
           <div class="input">
               <div>
-              <span><a href="${h.url('summary_home', repo_name=c.pull_request.other_repo.repo_name)}">${c.pull_request.other_repo.clone_url()}</a></span>
-              ## branch link is only valid if it is a branch
-              <span class="spantag"><a href="${h.url('summary_home', repo_name=c.pull_request.other_repo.repo_name, anchor=c.other_ref_name)}">${c.other_ref_type}: ${c.other_ref_name}</a></span>
+              ${h.link_to_ref(c.pull_request.other_repo.repo_name, c.other_ref_type, c.other_ref_name)}
+              ## we don't know other rev - c.other_rev is ancestor and not necessarily on other_name_branch branch
               </div>
           </div>
         </div>
@@ -172,6 +171,36 @@
               </div>
           </div>
         </div>
+
+        ${h.form(url('pullrequest_copy_update',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id), method='post')}
+          <div class="field">
+            <div class="label-summary">
+              <label>${_('Update')}:</label>
+            </div>
+            %if c.available:
+              <div class="input" style="max-height:200px; overflow-y:auto; overflow-x:hidden">
+                ${_("Changesets on %s not included in this pull request:") % c.org_branch_name}
+                <table class="noborder">
+                  %for cnt, cs in enumerate(reversed(c.available)):
+                    <tr>
+                    <td>${h.radio(name='updaterev', value=cs.raw_id)}</td>
+                    <td>${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.org_repo.repo_name,revision=cs.raw_id))}</td>
+                    <td><div class="message" style="white-space:normal; height:1.1em; max-width: 500px; padding:0">${h.urlify_commit(cs.message, c.repo_name)}</div></td>
+                    </tr>
+                  %endfor
+                </table>
+              </div>
+              <div class="buttons">
+                ${h.submit('copy_update',_('Create pull request update'),class_="btn btn-small")}
+              </div>
+            %else:
+              <div class="input">
+                ${_("No changesets found for updating this pull request.")}
+              </div>
+            %endif
+          </div>
+        ${h.end_form()}
+
       </div>
     </div>
     ## REVIEWERS