summaryrefslogtreecommitdiff
path: root/lib/codereview
diff options
context:
space:
mode:
authorRuss Cox <rsc@golang.org>2011-04-08 13:00:40 -0400
committerRuss Cox <rsc@golang.org>2011-04-08 13:00:40 -0400
commitce0b5a5a2a0850c32d9b8ae16ce08c2938a96b88 (patch)
tree0bdf93ba5017032229f70fc29d4f85ed5e9cd940 /lib/codereview
parentd0e5b5d2c01ed68941c225d6e4201a95eb8e4d6b (diff)
downloadgo-ce0b5a5a2a0850c32d9b8ae16ce08c2938a96b88.tar.gz
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 http://codereview.appspot.com/4377046
Diffstat (limited to 'lib/codereview')
-rw-r--r--lib/codereview/codereview.py114
1 files changed, 100 insertions, 14 deletions
diff --git a/lib/codereview/codereview.py b/lib/codereview/codereview.py
index 827054bf0..766e827fc 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':
- 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",