From 740051ae75e9a97ae86b9ff2374477e84b5972ed Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Fri, 8 Apr 2011 13:00:40 -0400 Subject: [PATCH] codereview: automatically port old diffs forward 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 | 114 ++++++++++++++++++++++++++++++----- 1 file changed, 100 insertions(+), 14 deletions(-) diff --git a/lib/codereview/codereview.py b/lib/codereview/codereview.py index 827054bf0b..766e827fc3 100644 --- a/lib/codereview/codereview.py +++ b/lib/codereview/codereview.py @@ -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': + 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 - text = link.get('href') - if not text.startswith(prefix) or not text.endswith('.diff'): - 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"(.*) \((.*)\)", 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",