]> Cypherpunks repositories - gostls13.git/commitdiff
codereview: automatically port old diffs forward
authorRuss Cox <rsc@golang.org>
Fri, 8 Apr 2011 17:00:40 +0000 (13:00 -0400)
committerRuss Cox <rsc@golang.org>
Fri, 8 Apr 2011 17:00:40 +0000 (13:00 -0400)
In the current codereview, if a patch was written against
a version of a file that had subsequently been edited,
hg clpatch would fail, even if the patch and the edits were
in different parts of the file.  In this situation the reviewer
typically wrote back saying "please hg sync and hg mail
to update the patch".

This change rewrites the patch automatically, using the
same transformation that hg sync + hg mail would.

If the interim changes (since the patch was created)
affect the same line ranges as the patch, clpatch will
still refuse to apply it.  But this CL should make
of the trivial conflicts we see just go away.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/4377046

lib/codereview/codereview.py

index 827054bf0bac445488dd0d10df5567077ae45c89..766e827fc38b63050319408df2cb0756eec593d2 100644 (file)
@@ -1135,12 +1135,27 @@ def clpatch(ui, repo, clname, **opts):
        if missing_codereview:
                return missing_codereview
 
-       cl, patch, err = DownloadCL(ui, repo, clname)
+       cl, vers, patch, err = DownloadCL(ui, repo, clname)
        if err != "":
                return err
        if patch == emptydiff:
                return "codereview issue %s has no diff" % clname
 
+       if not repo[vers]:
+               return "codereview issue %s is newer than the current repository; hg sync" % clname
+
+       # find current hg version (hg identify)
+       ctx = repo[None]
+       parents = ctx.parents()
+       id = '+'.join([short(p.node()) for p in parents])
+
+       # if version does not match the patch version,
+       # try to update the patch line numbers.
+       if id != vers:
+               patch, err = portPatch(repo, patch, vers, id)
+               if err != "":
+                       return "codereview issue %s is out of date: %s" % (clname, err)
+
        argv = ["hgpatch"]
        if opts["no_incoming"]:
                argv += ["--checksync=false"]
@@ -1163,6 +1178,67 @@ def clpatch(ui, repo, clname, **opts):
        cl.Flush(ui, repo)
        ui.write(cl.PendingText() + "\n")
 
+# portPatch rewrites patch from being a patch against
+# oldver to being a patch against newver.
+def portPatch(repo, patch, oldver, newver):
+       lines = patch.splitlines(True) # True = keep \n
+       delta = None
+       for i in range(len(lines)):
+               line = lines[i]
+               if line.startswith('--- a/'):
+                       file = line[6:-1]
+                       delta = fileDeltas(repo, file, oldver, newver)
+               if not delta or not line.startswith('@@ '):
+                       continue
+               # @@ -x,y +z,w @@ means the patch chunk replaces
+               # the original file's line numbers x up to x+y with the
+               # line numbers z up to z+w in the new file.
+               # Find the delta from x in the original to the same
+               # line in the current version and add that delta to both
+               # x and z.
+               m = re.match('@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@', line)
+               if not m:
+                       return None, "error parsing patch line numbers"
+               n1, len1, n2, len2 = int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
+               d, err = lineDelta(delta, n1, len1)
+               if err != "":
+                       return "", err
+               n1 += d
+               n2 += d
+               lines[i] = "@@ -%d,%d +%d,%d @@\n" % (n1, len1, n2, len2)
+               
+       newpatch = ''.join(lines)
+       return newpatch, ""
+
+# fileDelta returns the line number deltas for the given file's
+# changes from oldver to newver.
+# The deltas are a list of (n, len, newdelta) triples that say
+# lines [n, n+len) were modified, and after that range the
+# line numbers are +newdelta from what they were before.
+def fileDeltas(repo, file, oldver, newver):
+       cmd = ["hg", "diff", "--git", "-r", oldver + ":" + newver, "path:" + file]
+       data = RunShell(cmd, silent_ok=True)
+       deltas = []
+       for line in data.splitlines():
+               m = re.match('@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@', line)
+               if not m:
+                       continue
+               n1, len1, n2, len2 = int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
+               deltas.append((n1, len1, n2+len2-(n1+len1)))
+       return deltas
+
+# lineDelta finds the appropriate line number delta to apply to the lines [n, n+len).
+# It returns an error if those lines were rewritten by the patch.
+def lineDelta(deltas, n, len):
+       d = 0
+       for (old, oldlen, newdelta) in deltas:
+               if old >= n+len:
+                       break
+               if old+len > n:
+                       return 0, "patch and recent changes conflict"
+               d = newdelta
+       return d, ""
+
 def download(ui, repo, clname, **opts):
        """download a change from the code review server
 
@@ -1172,7 +1248,7 @@ def download(ui, repo, clname, **opts):
        if missing_codereview:
                return missing_codereview
 
-       cl, patch, err = DownloadCL(ui, repo, clname)
+       cl, vers, patch, err = DownloadCL(ui, repo, clname)
        if err != "":
                return err
        ui.write(cl.EditorText() + "\n")
@@ -1741,25 +1817,35 @@ def DownloadCL(ui, repo, clname):
        set_status("downloading CL " + clname)
        cl, err = LoadCL(ui, repo, clname)
        if err != "":
-               return None, None, "error loading CL %s: %s" % (clname, err)
+               return None, None, None, "error loading CL %s: %s" % (clname, err)
 
        # Grab RSS feed to learn about CL
        feed = XMLGet(ui, "/rss/issue/" + clname)
        if feed is None:
-               return None, None, "cannot download CL"
+               return None, None, None, "cannot download CL"
 
        # Find most recent diff
        diff = None
        prefix = 'http://' + server + '/'
-       for link in feed.findall("{http://www.w3.org/2005/Atom}entry/{http://www.w3.org/2005/Atom}link"):
-               if link.get('rel') != 'alternate':
-                       continue
-               text = link.get('href')
-               if not text.startswith(prefix) or not text.endswith('.diff'):
+       vers = ""
+       for entry in feed.findall("{http://www.w3.org/2005/Atom}entry"):
+               thisVers = ""
+               for title in entry.findall("{http://www.w3.org/2005/Atom}title"):
+                       m = re.search('diff -r ([0-9a-f]+) ', title.text)
+                       if m:
+                               thisVers = m.group(1)
+               if thisVers == "":
                        continue
-               diff = text[len(prefix)-1:]
+               for link in entry.findall("{http://www.w3.org/2005/Atom}link"):
+                       if link.get('rel') != 'alternate':
+                               continue
+                       text = link.get('href')
+                       if not text.startswith(prefix) or not text.endswith('.diff'):
+                               continue
+                       diff = text[len(prefix)-1:]
+                       vers = thisVers
        if diff is None:
-               return None, None, "CL has no diff"
+               return None, None, None, "CL has no diff"
        diffdata = MySend(diff, force_auth=False)
 
        # Find author - first entry will be author who created CL.
@@ -1768,7 +1854,7 @@ def DownloadCL(ui, repo, clname):
                nick = author.text.strip()
                break
        if not nick:
-               return None, None, "CL has no author"
+               return None, None, None, "CL has no author"
 
        # The author is just a nickname: get the real email address.
        try:
@@ -1778,7 +1864,7 @@ def DownloadCL(ui, repo, clname):
        except:
                ui.warn("error looking up %s: %s\n" % (nick, ExceptionDetail()))
                cl.copied_from = nick+"@needtofix"
-               return cl, diffdata, ""
+               return cl, vers, diffdata, ""
        match = re.match(r"<b>(.*) \((.*)\)</b>", data)
        if not match:
                return None, None, "error looking up %s: cannot parse result %s" % (nick, repr(data))
@@ -1792,7 +1878,7 @@ def DownloadCL(ui, repo, clname):
        if him != me:
                cl.copied_from = email
 
-       return cl, diffdata, ""
+       return cl, vers, diffdata, ""
 
 def MySend(request_path, payload=None,
                content_type="application/octet-stream",