hgext/phabricator.py
changeset 44644 dbe9182c90f5
parent 44643 0437959de6f5
child 44645 419fec8237b7
--- a/hgext/phabricator.py	Wed Feb 26 13:13:49 2020 -0500
+++ b/hgext/phabricator.py	Fri Mar 06 17:03:04 2020 -0500
@@ -1067,14 +1067,42 @@
     if actions:
         transactions += actions
 
-    # Parse commit message and update related fields.
-    desc = ctx.description()
-    info = callconduit(
-        repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
-    )
-    for k, v in info[b'fields'].items():
-        if k in [b'title', b'summary', b'testPlan']:
-            transactions.append({b'type': k, b'value': v})
+    # When folding multiple local commits into a single review, arcanist will
+    # take the summary line of the first commit as the title, and then
+    # concatenate the rest of the remaining messages (including each of their
+    # first lines) to the rest of the first commit message (each separated by
+    # an empty line), and use that as the summary field.  Do the same here.
+    # For commits with only a one line message, there is no summary field, as
+    # this gets assigned to the title.
+    fields = util.sortdict()  # sorted for stable wire protocol in tests
+
+    for i, _ctx in enumerate([ctx]):
+        # Parse commit message and update related fields.
+        desc = _ctx.description()
+        info = callconduit(
+            repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
+        )
+
+        for k in [b'title', b'summary', b'testPlan']:
+            v = info[b'fields'].get(k)
+            if not v:
+                continue
+
+            if i == 0:
+                # Title, summary and test plan (if present) are taken verbatim
+                # for the first commit.
+                fields[k] = v.rstrip()
+                continue
+            elif k == b'title':
+                # Add subsequent titles (i.e. the first line of the commit
+                # message) back to the summary.
+                k = b'summary'
+
+            # Append any current field to the existing composite field
+            fields[k] = b'\n\n'.join(filter(None, [fields.get(k), v.rstrip()]))
+
+    for k, v in fields.items():
+        transactions.append({b'type': k, b'value': v})
 
     params = {b'transactions': transactions}
     if revid is not None:
@@ -1266,7 +1294,7 @@
                 old = unfi[rev]
                 drevid = drevids[i]
                 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
-                newdesc = getdescfromdrev(drev)
+                newdesc = get_amended_desc(drev, old, False)
                 # Make sure commit message contain "Differential Revision"
                 if old.description() != newdesc:
                     if old.phase() == phases.public:
@@ -1593,6 +1621,33 @@
     return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
 
 
+def get_amended_desc(drev, ctx, folded):
+    """similar to ``getdescfromdrev``, but supports a folded series of commits
+
+    This is used when determining if an individual commit needs to have its
+    message amended after posting it for review.  The determination is made for
+    each individual commit, even when they were folded into one review.
+    """
+    if not folded:
+        return getdescfromdrev(drev)
+
+    uri = b'Differential Revision: %s' % drev[b'uri']
+
+    # Since the commit messages were combined when posting multiple commits
+    # with --fold, the fields can't be read from Phabricator here, or *all*
+    # affected local revisions will end up with the same commit message after
+    # the URI is amended in.  Append in the DREV line, or update it if it
+    # exists.  At worst, this means commit message or test plan updates on
+    # Phabricator aren't propagated back to the repository, but that seems
+    # reasonable for the case where local commits are effectively combined
+    # in Phabricator.
+    m = _differentialrevisiondescre.search(ctx.description())
+    if not m:
+        return b'\n\n'.join([ctx.description(), uri])
+
+    return _differentialrevisiondescre.sub(uri, ctx.description())
+
+
 def getdiffmeta(diff):
     """get commit metadata (date, node, user, p1) from a diff object