Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Improve comment propagation

Reimplemented comment propagation code to make it more robust and more
capable of dealing with non-trivial situations.  Known issues fixed or
improved by this is:

* Comments against lines in files modified by a later merge or rebase in
  the review were not propagate forward properly to commits after the
  merge or rebase.  Issues raised in such circumstances would never be
  marked as addressed by later commits.

* Crash when attempting to comment lines in a version of a file that was
  introduced into the review by a merge commit or rebase but where the
  file is not included in the (equivalent) merge commit's diff because
  there were no (suspected) conflicts in the file.

* Reopening an issue against lines that are themselves modified by a
  later commit.  In that case, the issue is now left as "addressed" only
  with a new commit as the "addressed by" commit.  It's possible to
  reopen the issue again from there, eventually reaching either an open
  issue or an issue that is marked as addressed by the commit that
  actually addressed it.

* Support for writing comments via /showfile even if the file in question
  has been changed in the review.  This was previously not supported
  simply because the old comment propagation code was slightly difficult
  to reuse.

Notable changes:

* Introduced a new utility class Propagation used whenever comments are
  propagated between file versions (when initially created, when new
  commits are added to the review and when reopening an issue.)

* Dropped the column 'commit' from the table commentchainlines.  It was
  confusing since it was not included in the primary key of the table,
  and the columns that make up the primary key could have identical
  values for several different commits.  The value in the 'commit' column
  was thus just a commit (the first) where the comment existed against
  the specified lines.  It wasn't used a lot, and where it was, it was
  either wrong or inaccurate.

* Added columns 'from_addressed_by' and 'to_addressed_by' to the table
  commentchainchanges to properly record the changes to the column
  'addressed_by' in the table 'commentchains' when reopening an issue
  that ends up being addressed again by a different commit.  Previously,
  when reopening an issue, the 'addressed_by' column was always set to
  NULL so changes to it needn't be recorded explicitly.
  • Loading branch information...
commit af2eb625caaef6b1a1555c22221dcf8409d3274f 1 parent 5185b1f
@jensl authored
View
2  changeset/html.py
@@ -220,7 +220,7 @@ def render(db, target, user, changeset, review=None, review_mode=None, context_l
for file in changeset.files:
if file.hasChanges() and not file.wasRemoved():
- comment_chains = review_comment.loadCommentChains(db, review, user, file, changeset, local_comments_only=local_comments_only)
+ comment_chains = review_comment.loadCommentChains(db, review, user, file=file, changeset=changeset, local_comments_only=local_comments_only)
if comment_chains:
comment_chains_per_file[file.path] = comment_chains
View
5 dbschema.comments.sql
@@ -71,7 +71,9 @@ CREATE TABLE commentchainchanges
from_state commentchainstate,
to_state commentchainstate,
from_last_commit INTEGER REFERENCES commits,
- to_last_commit INTEGER REFERENCES commits );
+ to_last_commit INTEGER REFERENCES commits,
+ from_addressed_by INTEGER REFERENCES commits,
+ to_addressed_by INTEGER REFERENCES commits );
CREATE INDEX commentchainchanges_review_uid_state ON commentchainchanges (review, uid, state);
CREATE INDEX commentchainchanges_batch ON commentchainchanges(batch);
CREATE INDEX commentchainchanges_chain ON commentchainchanges(chain);
@@ -86,7 +88,6 @@ CREATE TABLE commentchainlines
uid INTEGER REFERENCES users,
time TIMESTAMP NOT NULL DEFAULT now(),
state commentchainlinesstate NOT NULL DEFAULT 'draft',
- commit INTEGER REFERENCES commits,
sha1 CHAR(40) NOT NULL,
first_line INTEGER NOT NULL,
last_line INTEGER NOT NULL,
View
109 dbutils/review.py
@@ -36,16 +36,19 @@ def countDraftItems(db, user, review):
cursor.execute("SELECT count(*) FROM commentchains, comments WHERE commentchains.review=%s AND comments.chain=commentchains.id AND comments.uid=%s AND comments.state='draft'", [review.id, user.id])
comments = cursor.fetchone()[0]
- cursor.execute("""SELECT count(*) FROM commentchains, commentchainchanges
+ cursor.execute("""SELECT DISTINCT commentchains.id
+ FROM commentchains
+ JOIN commentchainchanges ON (commentchainchanges.chain=commentchains.id)
WHERE commentchains.review=%s
- AND commentchains.state=commentchainchanges.from_state
- AND commentchainchanges.chain=commentchains.id
AND commentchainchanges.uid=%s
AND commentchainchanges.state='draft'
- AND (commentchainchanges.from_state='addressed' OR commentchainchanges.from_state='closed')
- AND commentchainchanges.to_state='open'""",
+ AND ((commentchains.state=commentchainchanges.from_state
+ AND commentchainchanges.from_state IN ('addressed', 'closed')
+ AND commentchainchanges.to_state='open')
+ OR (commentchainchanges.from_addressed_by IS NOT NULL
+ AND commentchainchanges.to_addressed_by IS NOT NULL))""",
[review.id, user.id])
- reopened = cursor.fetchone()[0]
+ reopened = len(cursor.fetchall())
cursor.execute("""SELECT count(*) FROM commentchains, commentchainchanges
WHERE commentchains.review=%s
@@ -121,6 +124,53 @@ def __str__(self):
if issues: return "%s and %s" % (progress, issues)
else: return progress
+class ReviewRebase(object):
+ def __init__(self, review, old_head, new_head, old_upstream, new_upstream, user):
+ self.review = review
+ self.old_head = old_head
+ self.new_head = new_head
+ self.old_upstream = old_upstream
+ self.new_upstream = new_upstream
+ self.user = user
+
+class ReviewRebases(list):
+ def __init__(self, db, review):
+ import gitutils
+ from dbutils import User
+
+ self.__old_head_map = {}
+ self.__new_head_map = {}
+
+ cursor = db.cursor()
+ cursor.execute("""SELECT old_head, new_head, old_upstream, new_upstream, uid
+ FROM reviewrebases
+ WHERE review=%s
+ AND new_head IS NOT NULL""",
+ (review.id,))
+
+ for old_head_id, new_head_id, old_upstream_id, new_upstream_id, user_id in cursor:
+ old_head = gitutils.Commit.fromId(db, review.repository, old_head_id)
+ new_head = gitutils.Commit.fromId(db, review.repository, new_head_id)
+
+ if old_upstream_id is not None and new_upstream_id is not None:
+ old_upstream = gitutils.Commit.fromId(db, review.repository, old_upstream_id)
+ new_upstream = gitutils.Commit.fromId(db, review.repository, new_upstream_id)
+ else:
+ old_upstream = new_upstream = None
+
+ user = User.fromId(db, user_id)
+ rebase = ReviewRebase(review, old_head, new_head, old_upstream, new_upstream, user)
+
+ self.append(rebase)
+ self.__old_head_map[old_head] = rebase
+ self.__new_head_map[new_head] = rebase
+
+ def fromOldHead(self, commit):
+ return self.__old_head_map.get(commit)
+
+ def fromNewHead(self, commit):
+ return self.__new_head_map.get(commit)
+
class Review(object):
def __init__(self, review_id, owners, review_type, branch, state, serial, summary, description, applyfilters, applyparentfilters):
self.id = review_id
@@ -185,7 +235,29 @@ def getReviewState(self, db):
return ReviewState(self, self.accepted(db), pending, reviewed, issues)
- def containsCommit(self, db, commit):
+ def getReviewRebases(self, db):
+ return ReviewRebases(db, self)
+
+ def getCommitSet(self, db):
+ import gitutils
+ import log.commitset
+
+ cursor = db.cursor()
+ cursor.execute("""SELECT DISTINCT commits.id, commits.sha1
+ FROM commits
+ JOIN changesets ON (changesets.child=commits.id)
+ JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id)
+ WHERE reviewchangesets.review=%s""",
+ (self.id,))
+
+ commits = []
+
+ for commit_id, commit_sha1 in cursor:
+ commits.append(gitutils.Commit.fromSHA1(db, self.repository, commit_sha1, commit_id))
+
+ return log.commitset.CommitSet(commits)
+
+ def containsCommit(self, db, commit, include_head_and_tails=False):
import gitutils
commit_id = None
@@ -196,8 +268,10 @@ def containsCommit(self, db, commit):
else: commit_sha1 = commit.sha1
elif isinstance(commit, str):
commit_sha1 = self.repository.revparse(commit)
+ commit = None
elif isinstance(commit, int):
commit_id = commit
+ commit = None
else:
raise TypeError
@@ -219,7 +293,26 @@ def containsCommit(self, db, commit):
AND commits.sha1=%s""",
(self.id, commit_sha1))
- return cursor.fetchone() is not None
+ if cursor.fetchone() is not None:
+ return True
+
+ if include_head_and_tails:
+ head_and_tails = set([self.branch.head])
+
+ commitset = self.getCommitSet(db)
+
+ if commitset:
+ head_and_tails |= commitset.getTails()
+
+ if commit_sha1 is None:
+ if commit is None:
+ commit = gitutils.Commit.fromId(db, self.repository, commit_id)
+ commit_sha1 = commit.sha1
+
+ if commit_sha1 in head_and_tails:
+ return True
+
+ return False
def getJS(self):
return "var review = critic.review = { id: %d, branch: { id: %d, name: %r }, owners: [ %s ], serial: %d };" % (self.id, self.branch.id, self.branch.name, ", ".join(owner.getJSConstructor() for owner in self.owners), self.serial)
View
12 gitutils.py
@@ -629,12 +629,16 @@ def isAncestorOf(self, other):
return self.repository.mergebase([self.sha1, other_sha1]) == self.sha1
- def getFileSHA1(self, path):
+ def getFileEntry(self, path):
try:
- tree = Tree.fromPath(self, os.path.dirname(path))
- return tree[os.path.basename(path)].sha1
- except KeyError:
+ tree = Tree.fromPath(self, "/" + os.path.dirname(path).lstrip("/"))
+ except GitCommandError:
return None
+ return tree.get(os.path.basename(path))
+
+ def getFileSHA1(self, path):
+ entry = self.getFileEntry(path)
+ return entry.sha1 if entry else None
RE_LSTREE_LINE = re.compile("^([0-9]{6}) (blob|tree|commit) ([0-9a-f]{40}) *([0-9]+|-)\t(.*)$")
View
54 installation/migrations/dbschema.altertable.commentchainchanges.addressed_by.py
@@ -0,0 +1,54 @@
+# -*- mode: python; encoding: utf-8 -*-
+#
+# Copyright 2012 Jens Lindström, Opera Software ASA
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import sys
+import psycopg2
+import json
+import argparse
+import os
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--uid", type=int)
+parser.add_argument("--gid", type=int)
+
+arguments = parser.parse_args()
+
+os.setgid(arguments.gid)
+os.setuid(arguments.uid)
+
+data = json.load(sys.stdin)
+
+db = psycopg2.connect(database="critic")
+cursor = db.cursor()
+
+try:
+ # Make sure the columns don't already exist.
+ cursor.execute("SELECT from_addressed_by, to_addressed_by FROM commentchainchanges")
+
+ # Above statement should have thrown a psycopg2.ProgrammingError, but it
+ # didn't, so just exit.
+ sys.exit(0)
+except psycopg2.ProgrammingError:
+ db.rollback()
+except:
+ raise
+
+cursor.execute("""ALTER TABLE commentchainchanges
+ ADD from_addressed_by INTEGER REFERENCES commits,
+ ADD to_addressed_by INTEGER REFERENCES commits""")
+
+db.commit()
+db.close()
View
47 installation/migrations/dbschema.altertable.commentchainlines.drop.commit.py
@@ -0,0 +1,47 @@
+# -*- mode: python; encoding: utf-8 -*-
+#
+# Copyright 2012 Jens Lindström, Opera Software ASA
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import sys
+import psycopg2
+import json
+import argparse
+import os
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--uid", type=int)
+parser.add_argument("--gid", type=int)
+
+arguments = parser.parse_args()
+
+os.setgid(arguments.gid)
+os.setuid(arguments.uid)
+
+data = json.load(sys.stdin)
+
+db = psycopg2.connect(database="critic")
+cursor = db.cursor()
+
+try:
+ # Check if the 'commit' column already doesn't exist.
+ cursor.execute("SELECT commit FROM commentchainlines")
+except psycopg2.ProgrammingError:
+ # Seems it doesn't, so just exit.
+ sys.exit(0)
+
+cursor.execute("""ALTER TABLE commentchainlines DROP commit""")
+
+db.commit()
+db.close()
View
8 log/commitset.py
@@ -174,10 +174,12 @@ def filtered(self, commits):
return CommitSet(filtered)
def without(self, commits):
- """Return a copy of this commit set without 'commit' and all any ancestors of
-'commit' that don't have other descendants in the commit set."""
+ """
+ Return a copy of this commit set without 'commit' and any ancestors of
+ 'commit' that don't have other descendants in the commit set.
+ """
- pending = set(commits)
+ pending = set(filter(None, (self.__commits.get(str(commit)) for commit in commits)))
commits = self.__commits.copy()
children = self.__children.copy()
View
8 operation/createcomment.py
@@ -22,14 +22,16 @@
class ValidateCommentChain(Operation):
def __init__(self):
Operation.__init__(self, { "review_id": int,
+ "origin": set(["old", "new"]),
+ "parent_id": int,
+ "child_id": int,
"file_id": int,
- "sha1": str,
"offset": int,
"count": int })
- def process(self, db, user, review_id, file_id, sha1, offset, count):
+ def process(self, db, user, review_id, origin, parent_id, child_id, file_id, offset, count):
review = dbutils.Review.fromId(db, review_id)
- verdict, data = validateCommentChain(db, review, file_id, sha1, offset, count)
+ verdict, data = validateCommentChain(db, review, origin, parent_id, child_id, file_id, offset, count)
return OperationResult(verdict=verdict, **data)
class CreateCommentChain(Operation):
View
29 operation/draftchanges.py
@@ -312,6 +312,22 @@ def process(self, db, user, review_id, remark=None):
profiler.check("commentchainchanges reject type changes")
+ # Reject all draft comment chain changes where the affected comment chain
+ # addressed_by isn't what it was in when the change was drafted.
+ cursor.execute("""UPDATE commentchainchanges
+ SET state='rejected',
+ time=now()
+ FROM commentchains
+ WHERE commentchains.review=%s
+ AND commentchainchanges.chain=commentchains.id
+ AND commentchainchanges.uid=%s
+ AND commentchainchanges.state='draft'
+ AND commentchainchanges.from_addressed_by IS NOT NULL
+ AND commentchainchanges.from_addressed_by!=commentchains.addressed_by""",
+ (review.id, user.id))
+
+ profiler.check("commentchainchanges reject addressed_by changes")
+
# Then perform the remaining draft comment chain changes by updating the
# state of the corresponding comment chain.
@@ -346,6 +362,19 @@ def process(self, db, user, review_id, remark=None):
profiler.check("commentchains reopen")
+ # Perform addressed->addressed changes, i.e. updating 'addressed_by'.
+ cursor.execute("""UPDATE commentchains
+ SET addressed_by=commentchainchanges.to_addressed_by
+ FROM commentchainchanges
+ WHERE commentchains.review=%s
+ AND commentchainchanges.chain=commentchains.id
+ AND commentchainchanges.uid=%s
+ AND commentchainchanges.state='draft'
+ AND commentchainchanges.to_addressed_by IS NOT NULL""",
+ (review.id, user.id))
+
+ profiler.check("commentchains reopen (partial)")
+
# Perform type changes.
cursor.execute("""UPDATE commentchains
SET type=commentchainchanges.to_type
View
84 operation/manipulatecomment.py
@@ -15,9 +15,10 @@
# the License.
import dbutils
+import gitutils
-from operation import Operation, OperationResult, OperationError, Optional
-from reviewing.comment import Comment, CommentChain
+from operation import Operation, OperationResult, OperationError, OperationFailure, Optional
+from reviewing.comment import Comment, CommentChain, propagate
class SetCommentChainState(Operation):
def __init__(self, parameters):
@@ -27,9 +28,13 @@ def setChainState(self, db, user, chain, old_state, new_state, new_last_commit=N
review = chain.review
if chain.state != old_state:
- raise OperationError, "the comment chain's state is not '%s'" % old_state
- if new_state == "open" and review.state != "open":
- raise OperationError, "can't reopen comment chain in %s review" % review.state
+ raise OperationFailure(code="invalidoperation",
+ title="Invalid operation",
+ message="The comment chain's state is not '%s'; can't change state to '%s'." % (old_state, new_state))
+ elif new_state == "open" and review.state != "open":
+ raise OperationFailure(code="invalidoperation",
+ title="Invalid operation",
+ message="Can't reopen comment chain in %s review!" % review.state)
if chain.last_commit:
old_last_commit = chain.last_commit.id
@@ -57,7 +62,8 @@ def setChainState(self, db, user, chain, old_state, new_state, new_last_commit=N
db.commit()
- return OperationResult(draft_status=review.getDraftStatus(db, user))
+ return OperationResult(old_state=old_state, new_state=new_state,
+ draft_status=review.getDraftStatus(db, user))
class ReopenResolvedCommentChain(SetCommentChainState):
def __init__(self):
@@ -78,13 +84,71 @@ def process(self, db, user, chain_id, commit_id, sha1, offset, count):
chain = CommentChain.fromId(db, chain_id, user)
existing = chain.lines_by_sha1.get(sha1)
+ if chain.state != "addressed":
+ raise OperationFailure(code="invalidoperation",
+ title="Invalid operation",
+ message="The comment chain is not marked as addressed!")
+
if not existing:
+ assert commit_id == chain.addressed_by.getId(db)
+
+ commits = chain.review.getCommitSet(db).without(chain.addressed_by.parents)
+
+ propagation = propagate.Propagation(db)
+ propagation.setExisting(chain.review, chain.id, chain.addressed_by, chain.file_id, offset, offset + count - 1, True)
+ propagation.calculateAdditionalLines(commits)
+
+ commentchainlines_values = []
+
+ for file_sha1, (first_line, last_line) in propagation.new_lines.items():
+ commentchainlines_values.append((chain.id, user.id, file_sha1, first_line, last_line))
+
cursor = db.cursor()
- cursor.execute("""INSERT INTO commentchainlines (chain, uid, commit, sha1, first_line, last_line)
- VALUES (%s, %s, %s, %s, %s, %s)""",
- (chain.id, user.id, commit_id, sha1, offset, offset + count - 1))
+ cursor.executemany("""INSERT INTO commentchainlines (chain, uid, sha1, first_line, last_line)
+ VALUES (%s, %s, %s, %s, %s)""",
+ commentchainlines_values)
+
+ if not propagation.active:
+ old_addressed_by_id = chain.addressed_by.getId(db)
+ new_addressed_by_id = propagation.addressed_by[0].child.getId(db)
+
+ if chain.addressed_by_is_draft:
+ cursor.execute("""UPDATE commentchainchanges
+ SET to_addressed_by=%s
+ WHERE chain=%s
+ AND uid=%s
+ AND state='draft'
+ AND to_addressed_by=%s""",
+ (new_addressed_by_id, chain.id, user.id, old_addressed_by_id))
+ else:
+ cursor.execute("""INSERT INTO commentchainchanges (review, uid, chain, from_addressed_by, to_addressed_by)
+ VALUES (%s, %s, %s, %s, %s)""",
+ (chain.review.id, user.id, chain.id, old_addressed_by_id, new_addressed_by_id))
+
+ old_last_commit_id = chain.last_commit.getId(db)
+ new_last_commit_id = chain.addressed_by.getId(db)
+
+ if chain.last_commit_is_draft:
+ cursor.execute("""UPDATE commentchainchanges
+ SET to_last_commit=%s
+ WHERE chain=%s
+ AND uid=%s
+ AND state='draft'
+ AND to_last_commit=%s""",
+ (new_last_commit_id, chain.id, user.id, old_last_commit_id))
+ else:
+ cursor.execute("""INSERT INTO commentchainchanges (review, uid, chain, from_last_commit, to_last_commit)
+ VALUES (%s, %s, %s, %s, %s)""",
+ (chain.review.id, user.id, chain.id, old_last_commit_id, new_last_commit_id))
+
+ db.commit()
+
+ return OperationResult(old_state='addressed', new_state='addressed',
+ draft_status=chain.review.getDraftStatus(db, user))
elif offset != existing[0] or count != existing[1]:
- raise OperationError, "the comment chain is already present at other lines in same file version"
+ raise OperationFailure(code="invalidoperation",
+ title="Invalid operation",
+ message="The comment chain is already present at other lines in same file version")
return self.setChainState(db, user, chain, "addressed", "open", new_last_commit=commit_id)
View
9 operation/rebasereview.py
@@ -213,15 +213,6 @@ def process(self, db, user, review_id, rebase_id):
AND addressed_by=%s""",
(review.id, old_head_id))
- # Delete any mappings of issues (or notes) to lines in the merge
- # commit.
- cursor.execute("""DELETE FROM commentchainlines
- USING commentchains
- WHERE commentchains.review=%s
- AND commentchains.id=commentchainlines.chain
- AND commentchainlines.commit=%s""",
- (review.id, old_head_id))
-
# Delete the review changesets (and, via cascade, all related
# assignments.)
cursor.execute("""DELETE FROM reviewchangesets
View
21 page/showcomment.py
@@ -119,7 +119,7 @@ def renderShowComments(req, db, user):
AND usergitemails.uid=%s
AND commentchains.state!='empty'
AND (commentchains.state!='draft' OR commentchains.uid=%s)
- ORDER BY commentchains.file, commentchainlines.commit, commentchainlines.first_line""",
+ ORDER BY commentchains.file, commentchainlines.first_line""",
(review.id, blame_user.id, user.id))
include_chain_ids = set([chain_id for (chain_id,) in cursor])
@@ -129,11 +129,23 @@ def renderShowComments(req, db, user):
include_chain_ids = None
if filter == "toread":
- query = "SELECT commentchains.id FROM commentchains JOIN comments ON (comments.chain=commentchains.id) JOIN commentstoread ON (commentstoread.comment=comments.id) LEFT OUTER JOIN commentchainlines ON (commentchainlines.chain=commentchains.id) WHERE review=%s AND commentstoread.uid=%s ORDER BY file, commit, first_line"
+ query = """SELECT commentchains.id
+ FROM commentchains
+ JOIN comments ON (comments.chain=commentchains.id)
+ JOIN commentstoread ON (commentstoread.comment=comments.id)
+ LEFT OUTER JOIN commentchainlines ON (commentchainlines.chain=commentchains.id)
+ WHERE review=%s
+ AND commentstoread.uid=%s
+ ORDER BY file, first_line"""
cursor.execute(query, (review.id, user.id))
else:
- query = "SELECT id FROM commentchains LEFT OUTER JOIN commentchainlines ON (chain=id) WHERE review=%s AND commentchains.state!='empty'"
+ query = """SELECT commentchains.id
+ FROM commentchains
+ LEFT OUTER JOIN commentchainlines ON (chain=id)
+ WHERE review=%s
+ AND commentchains.state!='empty'"""
+
arguments = [review.id]
if filter == "issues":
@@ -226,7 +238,8 @@ def renderShowComments(req, db, user):
if chain.file_id is not None:
file_ids.add(chain.file_id)
- changesets_files.setdefault((chain.first_commit, chain.last_commit), set()).add(chain.file_id)
+ parent, child = review_html.getCodeCommentChainChangeset(db, chain, original)
+ changesets_files.setdefault((parent, child), set()).add(chain.file_id)
profiler.check("load chains")
View
20 page/showfile.py
@@ -112,16 +112,16 @@ def renderShowFile(req, db, user):
document.addExternalStylesheet("resource/review.css")
document.addExternalScript("resource/review.js")
- cursor.execute("""SELECT DISTINCT id
- FROM commentchains
- JOIN commentchainlines ON (id=chain)
- WHERE review=%s
- AND file=%s
- AND sha1=%s
- AND ((commentchains.state!='draft' OR commentchains.uid=%s)
- AND commentchains.state!='empty')
- GROUP BY id""",
- [review.id, file_id, file_sha1, user.id])
+ cursor.execute("""SELECT DISTINCT commentchains.id
+ FROM commentchains
+ JOIN commentchainlines ON (commentchainlines.chain=commentchains.id)
+ WHERE commentchains.review=%s
+ AND commentchains.file=%s
+ AND commentchainlines.sha1=%s
+ AND ((commentchains.state!='draft' OR commentchains.uid=%s)
+ AND commentchains.state!='empty')
+ GROUP BY commentchains.id""",
+ (review.id, file_id, file_sha1, user.id))
comment_chain_script = ""
View
59 resources/comment.js
@@ -202,10 +202,12 @@ CommentChain.create = function (type_or_markers)
var lastLine = parseInt(m2[1]);
var data = { review_id: review.id,
+ origin: side == 'o' ? "old" : "new",
+ parent_id: useChangeset.parent.id,
+ child_id: useChangeset.child.id,
file_id: file,
- sha1: sha1,
offset: firstLine,
- count: lastLine + 1 - firstLine }
+ count: lastLine + 1 - firstLine };
var operation = new Operation({ action: "validate commented lines",
url: "validatecommentchain",
@@ -217,7 +219,7 @@ CommentChain.create = function (type_or_markers)
var content = $("<div title='Warning!'>"
+ "<p>"
+ "One or more of the lines you are commenting are modified by a "
- + "<a href='" + result.sha1 + "?review=" + review.id + "#f" + file + "o" + result.offset + "'>later commit</a> "
+ + "<a href='" + result.parent_sha1 + ".." + result.child_sha1 + "?review=" + review.id + "#f" + file + "o" + result.offset + "'>later commit</a> "
+ "in this review."
+ "</p>"
+ "</div>");
@@ -238,6 +240,24 @@ CommentChain.create = function (type_or_markers)
+ "This comment will appear against each version of the file."
+ "</p>";
}
+ else if (result.verdict == "invalid")
+ {
+ var content = $("<div title='Error!'>"
+ + "<p>"
+ + "<b>It is not possible to comment these lines.</b>"
+ + "</p>"
+ + "<p>"
+ + "This is probably because this/these commits are not part of the review."
+ + "</p>"
+ + "</div>");
+
+ content.dialog({ modal: true,
+ buttons: { "OK": function () { content.dialog("close"); }}
+ });
+
+ abort();
+ return;
+ }
markersLocation = "file";
}
@@ -661,7 +681,7 @@ CommentChain.prototype.reply = function (parentDialog)
resize();
};
-CommentChain.prototype.reopen = function (from_showcomment)
+CommentChain.prototype.reopen = function (from_showcomment, from_onload)
{
var self = this;
var content;
@@ -709,17 +729,28 @@ CommentChain.prototype.reopen = function (from_showcomment)
if (result)
{
- self.state = 'open';
-
- if (markers)
+ if (result.new_state == "open")
{
- self.markers.setType(self.type, self.state);
+ self.state = "open";
+
+ if (markers)
+ {
+ self.markers.setType(self.type, self.state);
- self.lines.sha1 = sha1;
- self.lines.firstLine = firstLine;
- self.lines.lastLine = lastLine;
+ self.lines.sha1 = sha1;
+ self.lines.firstLine = firstLine;
+ self.lines.lastLine = lastLine;
- self.markers.updatePosition();
+ self.markers.updatePosition();
+ }
+ }
+ else
+ {
+ showMessage("Reopen Issue",
+ "Issue still addressed!",
+ "The issue was successfully transferred to the selected lines, " +
+ "but those lines were in turn modified by a later commit in the " +
+ "review, so the issue is still marked as addressed.");
}
updateDraftStatus(result.draft_status);
@@ -740,7 +771,7 @@ CommentChain.prototype.reopen = function (from_showcomment)
{
if (from_showcomment || changeset.child.sha1 != this.addressed_by)
{
- content = $("<div title='Reopen Issue'>Addressed issues can only be reopened from the regular diff view. Would you like to go there?</div>");
+ content = $("<div title='Reopen Issue'>Addressed issues can only be reopened from a regular diff of the commit that addressed the issue. Would you like to go there?</div>");
function goThere()
{
@@ -1224,7 +1255,7 @@ $(window).load(function ()
scrollTo(0, first_line.offset().top - innerHeight / 2 + (last_line.offset().top + last_line.height() - first_line.offset().top) / 2);
- setTimeout(function () { chain.reopen(false); }, 10);
+ setTimeout(function () { chain.reopen(false, true); }, 10);
}
}
});
View
556 reviewing/comment/__init__.py
@@ -63,7 +63,7 @@ def fromId(db, id, user):
return Comment(CommentChain.fromId(db, chain_id, user), batch_id, id, state, dbutils.User.fromId(db, user_id), time, comment, code, cursor.fetchone() is not None)
class CommentChain:
- def __init__(self, id, user, review, batch_id, type, state, origin=None, file_id=None, first_commit=None, last_commit=None, closed_by=None, addressed_by=None, type_is_draft=False, state_is_draft=False, leader=None, count=None, unread=None):
+ def __init__(self, id, user, review, batch_id, type, state, origin=None, file_id=None, first_commit=None, last_commit=None, closed_by=None, addressed_by=None, type_is_draft=False, state_is_draft=False, last_commit_is_draft=False, addressed_by_is_draft=False, leader=None, count=None, unread=None):
self.id = id
self.user = user
self.review = review
@@ -76,9 +76,11 @@ def __init__(self, id, user, review, batch_id, type, state, origin=None, file_id
self.file_id = file_id
self.first_commit = first_commit
self.last_commit = last_commit
+ self.last_commit_is_draft = last_commit_is_draft
self.closed_by = closed_by
self.addressed_by = addressed_by
+ self.addressed_by_is_draft = addressed_by_is_draft
self.lines = None
self.lines_by_sha1 = None
@@ -237,24 +239,34 @@ def fromId(db, id, user, review=None, skip=None):
review_id, batch_id, user_id, type, state, origin, file_id, first_commit_id, last_commit_id, closed_by_id, addressed_by_id = row
type_is_draft = False
state_is_draft = False
+ last_commit_is_draft = False
+ addressed_by_is_draft = False
- cursor.execute("""SELECT from_type, to_type, from_state, to_state, from_last_commit, to_last_commit
+ cursor.execute("""SELECT from_type, to_type,
+ from_state, to_state,
+ from_last_commit, to_last_commit,
+ from_addressed_by, to_addressed_by
FROM commentchainchanges
WHERE chain=%s
AND uid=%s
AND state='draft'""",
[id, user.id])
- for from_type, to_type, from_state, to_state, from_last_commit_id, to_last_commit_id in cursor:
- if from_state == state and from_last_commit_id == last_commit_id:
+ for from_type, to_type, from_state, to_state, from_last_commit_id, to_last_commit_id, from_addressed_by_id, to_addressed_by_id in cursor:
+ if from_state == state:
state = to_state
state_is_draft = True
- last_commit_id = to_last_commit_id
if to_state != "open":
closed_by_id = user.id
if from_type == type:
type = to_type
type_is_draft = True
+ if from_last_commit_id == last_commit_id:
+ last_commit_id = from_last_commit_id
+ last_commit_is_draft = True
+ if from_addressed_by_id == addressed_by_id:
+ addressed_by_id = to_addressed_by_id
+ addressed_by_is_draft = True
if review is None:
review = dbutils.Review.fromId(db, review_id, load_commits=False)
@@ -271,7 +283,13 @@ def fromId(db, id, user, review=None, skip=None):
if closed_by_id: closed_by = dbutils.User.fromId(db, closed_by_id)
else: closed_by = None
- chain = CommentChain(id, dbutils.User.fromId(db, user_id), review, batch_id, type, state, origin, file_id, first_commit, last_commit, closed_by, addressed_by, type_is_draft=type_is_draft, state_is_draft=state_is_draft)
+ chain = CommentChain(id, dbutils.User.fromId(db, user_id), review,
+ batch_id, type, state, origin, file_id,
+ first_commit, last_commit, closed_by, addressed_by,
+ type_is_draft=type_is_draft,
+ state_is_draft=state_is_draft,
+ last_commit_is_draft=last_commit_is_draft,
+ addressed_by_is_draft=addressed_by_is_draft)
if not skip or 'lines' not in skip:
cursor.execute("SELECT sha1, first_line, last_line FROM commentchainlines WHERE chain=%s AND (state='current' OR uid=%s)", (id, user.id))
@@ -288,39 +306,33 @@ def loadCommentChains(db, review, user, file=None, changeset=None, commit=None,
if file is None and changeset is None and commit is None:
cursor.execute("SELECT id FROM commentchains WHERE review=%s AND file IS NULL", [review.id])
- elif file is not None and changeset is None:
- cursor.execute("""SELECT DISTINCT id
- FROM commentchains
- LEFT OUTER JOIN commentchainlines ON (id=chain)
- WHERE review=%s AND file=%s AND count(sha1)=0
- GROUP BY id, review, commentchains.uid, type, commentchains.state, file""",
- [review.id, file.id])
elif commit is not None:
cursor.execute("""SELECT DISTINCT id
- FROM commentchains
- JOIN commentchainlines ON (id=chain)
- WHERE review=%s
- AND file IS NULL
- AND commit=%s
- AND ((commentchains.state!='draft' OR commentchains.uid=%s)
- AND commentchains.state!='empty')
- GROUP BY id""",
+ FROM commentchains
+ WHERE review=%s
+ AND file IS NULL
+ AND first_commit=%s
+ AND ((state!='draft' OR uid=%s)
+ AND state!='empty')
+ GROUP BY id""",
[review.id, commit.getId(db), user.id])
elif local_comments_only:
- cursor.execute("""SELECT DISTINCT id
- FROM commentchains
- INNER JOIN commentchainlines ON (id=chain)
- INNER JOIN fileversions ON (commentchains.file=fileversions.file)
- WHERE review=%s
- AND commentchains.file=%s
- AND commentchains.state!='empty'
- AND ((commentchains.first_commit=%s AND commentchains.last_commit=%s)
- OR commentchains.addressed_by=%s)
- AND fileversions.changeset=%s
- AND (sha1=fileversions.old_sha1 OR sha1=fileversions.new_sha1)
- AND (commentchainlines.state='current' OR commentchainlines.uid=%s)
- ORDER BY id ASC""",
- [review.id, file.id, changeset.parent.getId(db), changeset.child.getId(db), changeset.child.getId(db), changeset.id, user.id])
+ cursor.execute("""SELECT DISTINCT commentchains.id
+ FROM commentchains
+ JOIN commentchainlines ON (commentchainlines.chain=commentchains.id)
+ JOIN fileversions ON (fileversions.file=commentchains.file)
+ WHERE commentchains.review=%s
+ AND commentchains.file=%s
+ AND commentchains.state!='empty'
+ AND ((commentchains.first_commit=%s AND commentchains.last_commit=%s)
+ OR commentchains.addressed_by=%s)
+ AND fileversions.changeset=%s
+ AND (commentchainlines.sha1=fileversions.old_sha1
+ OR commentchainlines.sha1=fileversions.new_sha1)
+ AND (commentchainlines.state='current'
+ OR commentchainlines.uid=%s)
+ ORDER BY commentchains.id ASC""",
+ (review.id, file.id, changeset.parent.getId(db), changeset.child.getId(db), changeset.child.getId(db), changeset.id, user.id))
else:
chain_ids = set()
@@ -329,16 +341,16 @@ def loadCommentChains(db, review, user, file=None, changeset=None, commit=None,
for file in files:
cursor.execute("""SELECT id
- FROM commentchains
- JOIN commentchainlines ON (commentchainlines.chain=commentchains.id)
- WHERE commentchains.review=%s
- AND commentchains.file=%s
- AND commentchains.state!='empty'
- AND (commentchains.state!='draft' OR commentchains.uid=%s)
- AND (commentchainlines.sha1=%s
- OR commentchainlines.sha1=%s)
- AND (commentchainlines.state='current'
- OR commentchainlines.uid=%s)""",
+ FROM commentchains
+ JOIN commentchainlines ON (commentchainlines.chain=commentchains.id)
+ WHERE commentchains.review=%s
+ AND commentchains.file=%s
+ AND commentchains.state!='empty'
+ AND (commentchains.state!='draft' OR commentchains.uid=%s)
+ AND (commentchainlines.sha1=%s
+ OR commentchainlines.sha1=%s)
+ AND (commentchainlines.state='current'
+ OR commentchainlines.uid=%s)""",
(review.id, file.id, user.id, file.old_sha1, file.new_sha1, user.id))
for (chain_id,) in cursor.fetchall():
@@ -358,6 +370,8 @@ def loadCommentChains(db, review, user, file=None, changeset=None, commit=None,
return result
def createCommentChain(db, user, review, chain_type, commit_id=None, origin=None, file_id=None, parent_id=None, child_id=None, old_sha1=None, new_sha1=None, offset=None, count=None):
+ import reviewing.comment.propagate
+
if chain_type == "issue" and review.state != "open":
raise OperationFailure(code="reviewclosed",
title="Review is closed!",
@@ -365,238 +379,52 @@ def createCommentChain(db, user, review, chain_type, commit_id=None, origin=None
cursor = db.cursor()
- if file_id is not None and (parent_id == child_id or parent_id is None):
- cursor.execute("""SELECT 1
- FROM reviewchangesets
- JOIN fileversions USING (changeset)
- WHERE reviewchangesets.review=%s
- AND fileversions.file=%s
- AND fileversions.old_sha1!='0000000000000000000000000000000000000000'
- AND fileversions.new_sha1!='0000000000000000000000000000000000000000'""",
- (review.id, file_id))
-
- if cursor.fetchone():
- cursor.execute("""SELECT parent, child
- FROM changesets
- JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id)
- JOIN fileversions ON (fileversions.changeset=changesets.id)
- WHERE fileversions.file=%s
- AND fileversions.new_sha1=%s""",
- (file_id, new_sha1))
-
- rows = cursor.fetchall()
-
- if not rows:
- cursor.execute("""SELECT parent, child
- FROM changesets
- JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id)
- JOIN fileversions ON (fileversions.changeset=changesets.id)
- WHERE fileversions.file=%s
- AND fileversions.old_sha1=%s""",
- (file_id, new_sha1))
-
- rows = cursor.fetchall()
-
- parent = child = None
-
- for row_parent_id, row_child_id in rows:
- if row_child_id == child_id:
- parent = gitutils.Commit.fromId(db, review.repository, row_parent_id)
- child = gitutils.Commit.fromId(db, review.repository, row_child_id)
- break
- elif row_parent_id == child_id and parent is None:
- parent = gitutils.Commit.fromId(db, review.repository, row_parent_id)
- child = gitutils.Commit.fromId(db, review.repository, row_child_id)
-
- if parent and child:
- url = "/%s/%s..%s?review=%d&file=%d" % (review.repository.name, parent.sha1[:8], child.sha1[:8], review.id, file_id)
- link = ("<p>The link below goes to a diff that can be use to create the comment:</p>" +
- "<p style='padding-left: 2em'><a href='%s'>%s%s</a></p>") % (url, dbutils.getURLPrefix(db), url)
- else:
- link = ""
-
- raise OperationFailure(code="notsupported",
- title="File changed in review",
- message=("<p>Due to limitations in the code used to create comments, " +
- "it's only possible to create comments via a diff view if " +
- "the commented file has been changed in the review.</p>" +
- link),
- is_html=True)
-
- cursor.execute("""INSERT INTO commentchains (review, uid, type, file, first_commit, last_commit)
- VALUES (%s, %s, %s, %s, %s, %s)
- RETURNING id""",
- (review.id, user.id, chain_type, file_id, child_id, child_id))
- chain_id = cursor.fetchone()[0]
-
- cursor.execute("""INSERT INTO commentchainlines (chain, uid, commit, sha1, first_line, last_line)
- VALUES (%s, %s, %s, %s, %s, %s)""",
- (chain_id, user.id, child_id, new_sha1, offset, offset + count - 1))
- elif file_id is not None:
- parents_returned = set()
-
- def getFileParent(new_sha1):
- cursor.execute("""SELECT changesets.id, fileversions.old_sha1
- FROM changesets, reviewchangesets, fileversions
- WHERE reviewchangesets.review=%s
- AND reviewchangesets.changeset=changesets.id
- AND fileversions.changeset=changesets.id
- AND fileversions.file=%s
- AND fileversions.new_sha1=%s""",
- [review.id, file_id, new_sha1])
- try:
- changeset_id, old_sha1 = cursor.fetchone()
- if old_sha1 in parents_returned: return None, None
- parents_returned.add(old_sha1)
- return changeset_id, old_sha1
- except:
- return None, None
-
- children_returned = set()
-
- def getFileChild(old_sha1):
- cursor.execute("""SELECT changesets.id, fileversions.new_sha1
- FROM changesets, reviewchangesets, fileversions
- WHERE reviewchangesets.review=%s
- AND reviewchangesets.changeset=changesets.id
- AND fileversions.changeset=changesets.id
- AND fileversions.file=%s
- AND fileversions.old_sha1=%s""",
- [review.id, file_id, old_sha1])
- try:
- changeset_id, new_sha1 = cursor.fetchone()
- if new_sha1 in children_returned: return None, None
- children_returned.add(new_sha1)
- return changeset_id, new_sha1
- except:
- return None, None
-
- cursor.execute("""SELECT changesets.id
- FROM changesets, reviewchangesets, fileversions
- WHERE reviewchangesets.review=%s
- AND reviewchangesets.changeset=changesets.id
- AND changesets.child=%s
- AND fileversions.changeset=changesets.id
- AND fileversions.file=%s
- AND fileversions.old_sha1=%s
- AND fileversions.new_sha1=%s""",
- [review.id, child_id, file_id, old_sha1, new_sha1])
-
- row = cursor.fetchone()
-
- if not row:
- if origin == "old":
- cursor.execute("""SELECT changesets.id
- FROM changesets, reviewchangesets, fileversions
- WHERE reviewchangesets.review=%s
- AND reviewchangesets.changeset=changesets.id
- AND fileversions.changeset=changesets.id
- AND fileversions.file=%s
- AND fileversions.old_sha1=%s""",
- [review.id, file_id, old_sha1])
- else:
- cursor.execute("""SELECT changesets.id
- FROM changesets, reviewchangesets, fileversions
- WHERE reviewchangesets.review=%s
- AND reviewchangesets.changeset=changesets.id
- AND fileversions.changeset=changesets.id
- AND fileversions.file=%s
- AND fileversions.new_sha1=%s""",
- [review.id, file_id, new_sha1])
-
- row = cursor.fetchone()
-
- primary_changeset_id = row[0]
-
- sha1s_older = { }
- sha1s_newer = { old_sha1: (primary_changeset_id, new_sha1) }
-
- sha1 = new_sha1
- while True:
- changeset_id, next_sha1 = getFileParent(sha1)
- if changeset_id:
- sha1s_older[sha1] = changeset_id, next_sha1
- sha1s_newer[next_sha1] = changeset_id, sha1
- sha1 = next_sha1
- else:
- break
-
- sha1 = new_sha1
- while True:
- changeset_id, next_sha1 = getFileChild(sha1)
- if changeset_id:
- sha1s_newer[sha1] = changeset_id, next_sha1
- sha1 = next_sha1
- else:
- break
-
- commentchainlines_values = []
- processed = set()
-
- def searchOrigin(changeset_id, sha1, search_space, first_line, last_line):
- try:
- while sha1 not in processed:
- processed.add(sha1)
- changeset_id, next_sha1 = search_space[sha1]
- changeset = changeset_load.loadChangeset(db, review.repository, changeset_id, filtered_file_ids=set([file_id]))
- if len(changeset.child.parents) > 1: break
- verdict, next_first_line, next_last_line = updateCommentChain(first_line, last_line, changeset.files[0].chunks, forward)
- if verdict == "modified": break
- sha1 = next_sha1
- first_line = next_first_line
- last_line = next_last_line
- except:
- pass
- return changeset_id, sha1, first_line, last_line
-
- first_line = offset
- last_line = offset + count - 1
-
- if origin == 'old':
- changeset_id, sha1, first_line, last_line = searchOrigin(primary_changeset_id, old_sha1, sha1s_older, first_line, last_line)
- commit_id = diff.Changeset.fromId(db, review.repository, changeset_id).parent.id
+ if file_id is not None:
+ if origin == "old":
+ commit = gitutils.Commit.fromId(db, review.repository, parent_id)
else:
- changeset_id, sha1, first_line, last_line = searchOrigin(primary_changeset_id, new_sha1, sha1s_older, first_line, last_line)
- commit_id = diff.Changeset.fromId(db, review.repository, changeset_id).child.id
-
- commentchainlines_values.append((user.id, commit_id, sha1, first_line, last_line))
- processed = set()
- processed.add(sha1)
-
- while sha1 in sha1s_newer:
- changeset_id, sha1 = sha1s_newer[sha1]
+ commit = gitutils.Commit.fromId(db, review.repository, child_id)
- if sha1 in processed: break
- else: processed.add(sha1)
+ propagation = reviewing.comment.propagate.Propagation(db)
- changeset = changeset_load.loadChangeset(db, review.repository, changeset_id, filtered_file_ids=set([file_id]))
+ if not propagation.setCustom(review, commit, file_id, offset, offset + count - 1):
+ raise OperationFailure(code="invalidoperation",
+ title="Invalid operation",
+ message="It's not possible to create a comment here.")
- if len(changeset.child.parents) != 1:
- chunks = diff.parse.parseDifferences(review.repository, from_commit=changeset.parent, to_commit=changeset.child, selected_path=dbutils.describe_file(db, file_id)).chunks
- else:
- chunks = changeset.files[0].chunks
-
- verdict, first_line, last_line = updateCommentChain(first_line, last_line, chunks)
+ propagation.calculateInitialLines()
- if verdict == "transfer":
- commentchainlines_values.append((user.id, changeset.child.getId(db), sha1, first_line, last_line))
- else:
- break
+ cursor.execute("""INSERT INTO commentchains (review, uid, type, origin, file, first_commit, last_commit)
+ VALUES (%s, %s, %s, %s, %s, %s, %s)
+ RETURNING id""",
+ (review.id, user.id, chain_type, origin, file_id, parent_id, child_id))
- cursor.execute("INSERT INTO commentchains (review, uid, type, origin, file, first_commit, last_commit) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id", [review.id, user.id, chain_type, origin, file_id, parent_id, child_id])
chain_id = cursor.fetchone()[0]
+ commentchainlines_values = []
+
+ for sha1, (first_line, last_line) in propagation.new_lines.items():
+ commentchainlines_values.append((chain_id, user.id, sha1, first_line, last_line))
- try: cursor.executemany("INSERT INTO commentchainlines (chain, uid, commit, sha1, first_line, last_line) VALUES (%s, %s, %s, %s, %s, %s)", [(chain_id,) + values for values in commentchainlines_values])
- except: raise Exception, repr(commentchainlines_values)
+ cursor.executemany("""INSERT INTO commentchainlines (chain, uid, sha1, first_line, last_line)
+ VALUES (%s, %s, %s, %s, %s)""",
+ commentchainlines_values)
elif commit_id is not None:
commit = gitutils.Commit.fromId(db, review.repository, commit_id)
- cursor.execute("INSERT INTO commentchains (review, uid, type, first_commit, last_commit) VALUES (%s, %s, %s, %s, %s) RETURNING id", [review.id, user.id, chain_type, commit_id, commit_id])
+ cursor.execute("""INSERT INTO commentchains (review, uid, type, first_commit, last_commit)
+ VALUES (%s, %s, %s, %s, %s)
+ RETURNING id""",
+ (review.id, user.id, chain_type, commit_id, commit_id))
chain_id = cursor.fetchone()[0]
- cursor.execute("INSERT INTO commentchainlines (chain, uid, commit, sha1, first_line, last_line) VALUES (%s, %s, %s, %s, %s, %s)", (chain_id, user.id, commit_id, commit.sha1, offset, offset + count - 1))
+ cursor.execute("""INSERT INTO commentchainlines (chain, uid, sha1, first_line, last_line)
+ VALUES (%s, %s, %s, %s, %s)""",
+ (chain_id, user.id, commit.sha1, offset, offset + count - 1))
else:
- cursor.execute("INSERT INTO commentchains (review, uid, type) VALUES (%s, %s, %s) RETURNING id", [review.id, user.id, chain_type])
+ cursor.execute("""INSERT INTO commentchains (review, uid, type)
+ VALUES (%s, %s, %s)
+ RETURNING id""",
+ (review.id, user.id, chain_type))
chain_id = cursor.fetchone()[0]
commentchainusers = set([user.id] + map(int, review.owners))
@@ -623,141 +451,97 @@ def createComment(db, user, chain_id, comment, first=False):
return comment_id
-def updateCommentChain(first_line, last_line, chunks, forward=True):
- delta = 0
-
- for chunk in chunks:
- if forward:
- if chunk.delete_offset + chunk.delete_count <= first_line:
- # Chunk is before (and does not overlap) the comment chain.
- delta += chunk.insert_count - chunk.delete_count
- elif chunk.delete_offset <= last_line:
- # Chunk overlaps the comment chain.
- return ("modified", None, None)
- else:
- # Chunk is after comment chain, which thus was not overlapped by
- # any chunk. Copy the comment chain over to the new version of
- # the file with 'delta' added to its 'first_line'/'last_line'.
- return ("transfer", first_line + delta, last_line + delta)
+def validateCommentChain(db, review, origin, parent_id, child_id, file_id, offset, count):
+ """
+ Check whether the commented lines are changed by later commits in the
+ review.
+
+ If they are, a diff.Changeset object representing the first changeset that
+ modifies those lines is returned. If they are not, None is returned.
+ """
+
+ import reviewing.comment.propagate
+
+ if origin == "old":
+ commit = gitutils.Commit.fromId(db, review.repository, parent_id)
+ else:
+ commit = gitutils.Commit.fromId(db, review.repository, child_id)
+
+ propagation = reviewing.comment.propagate.Propagation(db)
+
+ if not propagation.setCustom(review, commit, file_id, offset, offset + count - 1):
+ return "invalid", {}
+
+ propagation.calculateInitialLines()
+
+ if propagation.active:
+ file_path = dbutils.describe_file(db, file_id)
+
+ if commit.getFileSHA1(file_path) != review.branch.head.getFileSHA1(file_path):
+ return "transferred", {}
else:
- if chunk.insert_offset + chunk.insert_count <= first_line:
- # Chunk is before (and does not overlap) the comment chain.
- delta += chunk.delete_count - chunk.insert_count
- elif chunk.insert_offset <= last_line:
- # Chunk overlaps the comment chain.
- return ("modified", None, None)
- else:
- # Chunk is after comment chain, which thus was not overlapped by
- # any chunk. Copy the comment chain over to the new version of
- # the file with 'delta' added to its 'first_line'/'last_line'.
- return ("transfer", first_line + delta, last_line + delta)
+ return "clean", {}
else:
- # Comment chain was after all the chunks. Copy it over to the new
- # version of the file with 'delta' added to its 'first_line' and
- # 'last_line'.
- return ("transfer", first_line + delta, last_line + delta)
+ addressed_by = propagation.addressed_by[0]
+
+ return "modified", { "parent_sha1": addressed_by.parent.sha1,
+ "child_sha1": addressed_by.child.sha1,
+ "offset": addressed_by.location.first_line }
+
+def propagateCommentChains(db, user, review, commits):
+ import reviewing.comment.propagate
-def updateCommentChains(db, user, review, changeset):
cursor = db.cursor()
+ cursor.execute("""SELECT id, uid, type, state, file
+ FROM commentchains
+ WHERE review=%s
+ AND file IS NOT NULL""",
+ (review.id,))
+
+ chains_by_file = {}
+
+ for chain_id, chain_user_id, chain_type, chain_state, file_id in cursor:
+ chains_by_file.setdefault(file_id, {})[chain_id] = (chain_user_id, chain_type, chain_state)
commentchainlines_values = []
- addressed = set()
-
- for file in changeset.files:
- cursor.execute("""SELECT id, commentchains.uid, type, commentchains.state, first_line, last_line
- FROM commentchains
- INNER JOIN commentchainlines ON (id=chain)
- WHERE commentchains.review=%s
- AND commentchains.state in ('draft', 'open')
- AND commentchains.file=%s
- AND commentchainlines.sha1=%s
- ORDER BY commentchainlines.first_line""",
- [review.id, file.id, file.old_sha1])
-
- rows = cursor.fetchall()
- if not rows: continue
-
- if len(changeset.child.parents) != 1:
- full_file = diff.parse.parseDifferences(review.repository, from_commit=changeset.parent, to_commit=changeset.child, selected_path=file.path)
- if not full_file: continue
- chunks = full_file.chunks
- else:
- chunks = file.chunks
-
- for chain_id, chain_user_id, chain_type, chain_state, first_line, last_line in rows:
- verdict, new_first_line, new_last_line = updateCommentChain(first_line, last_line, chunks)
-
- if verdict == "modified" and chain_type == "issue": addressed.add(chain_id)
- elif verdict == "transfer":
- cursor.execute("SELECT 1 FROM commentchainlines WHERE chain=%s AND sha1=%s", (chain_id, file.new_sha1))
- if not cursor.fetchone():
- if chain_state == 'open':
- lines_state = 'current'
- lines_user_id = user.id
- else:
- lines_state = 'draft'
- lines_user_id = chain_user_id
-
- commentchainlines_values.append([chain_id, lines_user_id, lines_state, changeset.child.getId(db), file.new_sha1, new_first_line, new_last_line])
-
- cursor.executemany("""INSERT INTO commentchainlines (chain, uid, state, commit, sha1, first_line, last_line)
- VALUES (%s, %s, %s, %s, %s, %s, %s)""",
+ addressed_values = []
+
+ for file_id, chains in chains_by_file.items():
+ file_path = dbutils.describe_file(db, file_id)
+ file_sha1 = review.branch.head.getFileSHA1(file_path)
+
+ cursor.execute("""SELECT chain, first_line, last_line
+ FROM commentchainlines
+ WHERE chain=ANY (%s)
+ AND sha1=%s""",
+ (chains.keys(), file_sha1))
+
+ for chain_id, first_line, last_line in cursor:
+ propagation = reviewing.comment.propagate.Propagation(db)
+ propagation.setExisting(review, chain_id, review.branch.head, file_id, first_line, last_line)
+ propagation.calculateAdditionalLines(commits)
+
+ chain_user_id, chain_type, chain_state = chains[chain_id]
+ lines_state = "draft" if chain_state == "draft" else "current"
+
+ for sha1, (first_line, last_line) in propagation.new_lines.items():
+ commentchainlines_values.append((chain_id, chain_user_id, lines_state, sha1, first_line, last_line))
+
+ if chain_type == "issue" and chain_state in ("open", "draft") and not propagation.active:
+ addressed_values.append((propagation.addressed_by[0].child.getId(db), chain_id))
+
+ cursor.executemany("""INSERT INTO commentchainlines (chain, uid, state, sha1, first_line, last_line)
+ VALUES (%s, %s, %s, %s, %s, %s)""",
commentchainlines_values)
- if addressed:
- cursor.executemany("UPDATE commentchains SET state='addressed', addressed_by=%s WHERE id=%s AND state='open'", [[changeset.child.id, chain_id] for chain_id in addressed])
- cursor.executemany("UPDATE commentchains SET addressed_by=%s WHERE id=%s AND state='draft'", [[changeset.child.id, chain_id] for chain_id in addressed])
+ if addressed_values:
+ cursor.executemany("UPDATE commentchains SET state='addressed', addressed_by=%s WHERE id=%s AND state='open'", addressed_values)
+ cursor.executemany("UPDATE commentchains SET addressed_by=%s WHERE id=%s AND state='draft'", addressed_values)
print "Addressed issues:"
- for chain_id in addressed:
+ for commit_id, chain_id in addressed_values:
chain = CommentChain.fromId(db, chain_id, user, review=review)
if chain.state == 'addressed':
chain.loadComments(db, user)
title = " %s: " % chain.title(False)
print "%s%s" % (title, chain.leader(max_length=80 - len(title), text=True))
-
-def validateCommentChain(db, review, file_id, sha1, offset, count):
- """Check whether the commented lines are changed by later commits in the review.
-If they are, a diff.Changeset object representing the first changeset that
-modifies those lines is returned. If they are not, None is returned."""
-
- cursor = db.cursor()
- cursor.execute("""SELECT old_sha1, new_sha1, reviewchangesets.changeset
- FROM reviewchangesets, fileversions
- WHERE reviewchangesets.review=%s
- AND fileversions.changeset=reviewchangesets.changeset
- AND fileversions.file=%s""",
- [review.id, file_id])
-
- sha1s = {}
-
- for old_sha1, new_sha1, changeset_id in cursor.fetchall():
- sha1s[old_sha1] = (new_sha1, changeset_id)
-
- commit_count = 0
- processed = set()
-
- while sha1 in sha1s and sha1 not in processed:
- processed.add(sha1)
-
- commit_count += 1
- sha1, changeset_id = sha1s[sha1]
-
- cursor.execute("""SELECT deleteOffset, deleteCount, insertOffset, insertCount
- FROM chunks
- WHERE changeset=%s
- AND file=%s
- ORDER BY deleteOffset ASC""",
- [changeset_id, file_id])
-
- for delete_offset, delete_count, insert_offset, insert_count in cursor.fetchall():
- if insert_offset + delete_count <= offset:
- offset += insert_count - delete_count
- elif offset + count <= insert_offset:
- break
- else:
- return "modified", { "sha1": diff.Changeset.fromId(db, review.repository, changeset_id).child.sha1,
- "offset": offset }
-
- if commit_count > 0: return "transferred", { "count": commit_count }
- else: return "clean", {}
View
327 reviewing/comment/propagate.py
@@ -0,0 +1,327 @@
+# -*- mode: python; encoding: utf-8 -*-
+#
+# Copyright 2012 Jens Lindström, Opera Software ASA
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import dbutils
+
+from changeset.utils import createChangeset
+
+FORWARD = 1
+BACKWARD = 2
+
+class Location:
+ def __init__(self, first_line, last_line, active=True):
+ self.first_line = first_line
+ self.last_line = last_line
+ self.active = active
+
+ def copy(self):
+ return Location(self.first_line, self.last_line, self.active)
+
+ def __iadd__(self, delta):
+ self.first_line += delta
+ self.last_line += delta
+ return self
+
+ def __len__(self):
+ return 2
+
+ def __getitem__(self, index):
+ if index == 0: return self.first_line
+ elif index == 1: return self.last_line
+ else: raise IndexError
+
+ def __eq__(self, other):
+ return tuple(self) == tuple(other)
+
+ def apply(self, changes, direction):
+ """
+ Apply a set of changes and adjust the location accordingly.
+
+ Process a set of changes in the form of a list of objects with the
+ attributes delete_offset, delete_count, insert_offset and insert_count
+ (such as diff.Chunk objects) sorted on ascending offsets. If any of the
+ changes overlap this location, the location's 'active' attribute is set
+ to False, otherwise the 'first_line' and 'last_line' attributes are
+ adjusted to keep the location referencing the same lines.
+
+ If the 'direction' argument is FORWARD, this location is interpreted as
+ a location in the old version (before the changes) and is adjusted to a
+ location in the new version (after the changes.) If the argument is
+ BACKWARD, this location is interpreted as a location in the new version
+ (after the changes) and is adjusted to a location in the old version
+ (before the changes.)
+
+ Returns True if the location is still active.
+ """
+
+ delta = 0
+
+ # The only difference between the two loops is that uses of
+ # delete_offset/delete_count and insert_offset/insert_count are
+ # mirrored.
+ if direction == FORWARD:
+ for change in changes:
+ if change.delete_offset + change.delete_count <= self.first_line:
+ # Change is before (and does not overlap) the location.
+ delta += change.insert_count - change.delete_count
+ elif change.delete_offset <= self.last_line:
+ # Change overlaps the location.
+ self.active = False
+ break
+ else:
+ # Change is after the location, meaning, since changes come
+ # in ascending offset order, that all other changes are also
+ # after the location.
+ break
+ else:
+ for change in changes:
+ if change.insert_offset + change.insert_count <= self.first_line:
+ # Change is before (and does not overlap) the location.
+ delta += change.delete_count - change.insert_count
+ elif change.insert_offset <= self.last_line:
+ # Change overlaps the comment chain.
+ self.active = False
+ break
+ else:
+ # Change is after the location, meaning, since changes come
+ # in ascending offset order, that all other changes are also
+ # after the location.
+ break
+
+ # Apply 'delta' to the location if it's still active.
+ if self.active: self += delta
+
+ return self.active
+
+class AddressedBy(object):
+ def __init__(self, parent, child, location):
+ self.parent = parent
+ self.child = child
+ self.location = location
+
+class Propagation:
+ def __init__(self, db):
+ self.db = db
+ self.review = None
+ self.rebases = None
+ self.initial_commit = None
+ self.file_path = None
+ self.file_id = None
+ self.location = None
+ self.active = None
+ self.all_lines = None
+ self.new_lines = None
+
+ def setCustom(self, review, commit, file_id, first_line, last_line):
+ """
+ Initialize for propagation of a custom location.
+
+ This mode of operation is used to propagate a new comment chain to all
+ relevant commits current part of the review.
+
+ Returns false if the creating a comment at the specified location is not
+ supported, typically because the commit is not being reviewed in the
+ review.
+ """
+
+ if not review.containsCommit(self.db, commit, True):
+ return False
+
+ self.review = review
+ self.rebases = review.getReviewRebases(self.db)
+ self.initial_commit = commit
+ self.addressed_by = []
+ self.file_path = dbutils.describe_file(self.db, file_id)
+ self.file_id = file_id
+ self.location = Location(first_line, last_line)
+ self.active = True
+
+ file_sha1 = commit.getFileSHA1(self.file_path)
+
+ self.all_lines = { file_sha1: (first_line, last_line) }
+ self.new_lines = { file_sha1: (first_line, last_line) }
+
+ return True
+
+ def setExisting(self, review, chain_id, commit, file_id, first_line, last_line, reopening=False):
+ """
+ Initialize for propagation of existing comment chain.
+
+ This initializes the location to where the comment chain is located in
+ the most recent commit in the review. If the comment chain is not
+ present in the most recent commit in the review, this function returns
+ False.
+
+ This mode of operation is used to update existing comment chains when
+ adding new commits to a review.
+ """
+
+ self.review = review
+ self.rebases = review.getReviewRebases(self.db)
+ self.initial_commit = commit
+ self.addressed_by = []
+ self.file_path = dbutils.describe_file(self.db, file_id)
+ self.file_id = file_id
+ self.location = Location(first_line, last_line)
+ self.active = True
+ self.all_lines = {}
+ self.new_lines = {}
+
+ cursor = self.db.cursor()
+ cursor.execute("""SELECT sha1, first_line, last_line
+ FROM commentchainlines
+ WHERE chain=%s""",
+ (chain_id,))
+
+ for file_sha1, first_line, last_line in cursor:
+ self.all_lines[file_sha1] = (first_line, last_line)
+
+ if reopening:
+ self.__setLines(commit.getFileSHA1(self.file_path), self.location)
+
+ return True
+
+ def calculateInitialLines(self):
+ """
+ Calculate the initial set of line mappings for a comment chain.
+
+ Propagates the initial location both backward and forward through all
+ current commits in the review. If, through forward propagation, the
+ location becomes inactive, the 'active' attribute is set to False. In
+ any case, the 'lines' attribute will map each file SHA-1 to a pair of
+ line numbers (first_line, last_line) for each location found during the
+ propagation.
+
+ Returns the value of the 'active' attribute.
+ """
+
+ self.__propagate(self.review.getCommitSet(self.db))
+ return self.active
+
+ def calculateAdditionalLines(self, commits):
+ """
+ Calculate additional set of line mappings when adding new commits.
+
+ If this propagation object is not active (because the comment chain
+ it represents is not present in the most recent commit in the review)
+ then nothing happens.
+
+ Returns the value of the 'active' attribute.
+ """
+
+ self.__propagate(commits)
+ return self.active
+
+ def __propagate(self, commits):
+ cursor = self.db.cursor()
+
+ def propagateBackward(commit, location, processed):
+ parents = commits.getParents(commit)
+ recurse = []
+
+ if not parents:
+ for parent_sha1 in commit.parents:
+ rebase = self.rebases.fromNewHead(parent_sha1)
+ if rebase:
+ parents.add(rebase.old_head)
+
+ for parent in parents - processed:
+ changes = self.__getChanges(parent, commit)
+ if changes:
+ parent_location = location.copy()
+ if parent_location.apply(changes, BACKWARD):
+ file_sha1 = parent.getFileSHA1(self.file_path)
+ assert file_sha1
+ self.__setLines(file_sha1, parent_location)
+ recurse.append((parent, parent_location))
+ else:
+ recurse.append((parent, location))
+
+ processed.add(commit)
+
+ for parent, parent_location in recurse:
+ propagateBackward(parent, parent_location, processed)
+
+ def propagateForward(commit, location, processed):
+ children = commits.getChildren(commit)
+ recurse = []
+
+ if not children:
+ rebase = self.rebases.fromOldHead(commit)
+ if rebase:
+ children.update(commits.getChildren(rebase.new_head))
+
+ if not children:
+ assert not commits or commit in commits.getHeads() or self.rebases.fromNewHead(commit)
+ self.active = True
+
+ for child in children - processed:
+ changes = self.__getChanges(commit, child)
+ if changes:
+ child_location = location.copy()
+ if child_location.apply(changes, FORWARD):
+ file_sha1 = child.getFileSHA1(self.file_path)
+ assert file_sha1
+ self.__setLines(file_sha1, child_location)
+ recurse.append((child, child_location))
+ else:
+ self.addressed_by.append(AddressedBy(commit, child, location))
+ else:
+ recurse.append((child, location))
+
+ processed.add(commit)
+
+ for child, child_location in recurse:
+ propagateForward(child, child_location, processed)
+
+ # If we started propagation in the middle of, or at the end of, the
+ # commit-set, this call does the main backward propagation. After
+ # that, it will do extra backward propagation via other parents of
+ # merge commits encountered during forward propagation.
+ #
+ # For non-merge commits, 'processed' will always contain the single
+ # parent of 'commit', and propagateBackward() will find no parent
+ # commits to process, leaving this call a no-op.
+ propagateBackward(commit, location, processed)
+
+ # Will be set to True again if propagation reaches the head of the
+ # commit-set.
+ self.active = False
+
+ propagateForward(self.initial_commit, self.location, set())
+
+ def __getChanges(self, from_commit, to_commit):
+ changesets = createChangeset(self.db,
+ user=None,
+ repository=self.review.repository,
+ from_commit=from_commit,
+ to_commit=to_commit,
+ filtered_file_ids=set([self.file_id]),
+ do_highlight=False)
+
+ assert len(changesets) == 1
+
+ if changesets[0].files:
+ assert changesets[0].files[0].id == self.file_id
+ return changesets[0].files[0].chunks
+ else:
+ return None
+
+ def __setLines(self, file_sha1, lines):
+ if file_sha1 not in self.all_lines:
+ self.all_lines[file_sha1] = self.new_lines[file_sha1] = tuple(lines)
+ else:
+ assert self.all_lines[file_sha1] == tuple(lines)
View
22 reviewing/html.py
@@ -25,6 +25,7 @@
import page.utils
import page.showcommit
import linkify
+import traceback
from time import strftime
@@ -108,8 +109,10 @@ def renderCodeCommentChain(db, target, user, review, chain, context_lines=3, com
file_path = dbutils.describe_file(db, file_id)
if (chain.state != "addressed" or original) and chain.first_commit == chain.last_commit:
- cursor.execute("SELECT sha1, first_line, last_line FROM commentchainlines WHERE chain=%s AND commit=%s", (chain.id, chain.first_commit.getId(db)))
- sha1, first_line, last_line = cursor.fetchone()
+ sha1 = chain.first_commit.getFileSHA1(file_path)
+
+ cursor.execute("SELECT first_line, last_line FROM commentchainlines WHERE chain=%s AND sha1=%s", (chain.id, sha1))
+ first_line, last_line = cursor.fetchone()
file = diff.File(file_id, file_path, sha1, sha1, review.repository, chunks=[])
file.loadNewLines(True)
@@ -365,20 +368,7 @@ def renderCommentChain(db, target, user, review, chain, context_lines=3, compact
target = target.div("comment-chain", id="c%d" % chain.id)
if chain.file_id:
- try: renderCodeCommentChain(db, target, user, review, chain, context_lines, compact, tabify, original, changeset, linkify)
- except:
- cursor = db.cursor()
- cursor.execute("SELECT first_line, last_line, commit FROM commentchainlines WHERE chain=%s ORDER BY time ASC LIMIT 1", (chain.id,))
- path = dbutils.describe_file(db, chain.file_id)
- first_line, last_line, commit_id = cursor.fetchone()
- commit = gitutils.Commit.fromId(db, review.repository, commit_id)
-
- if first_line == last_line: line = "line %d" % first_line
- else: line = "lines %d-%d" % (first_line, last_line)
-
- message = "<p><b>I'm terribly sorry, but this comment is broken in the database!</b></p><p>It was originally made against %s in some version of <code>%s</code>, in the commit <a href='%s/%s?review=%d&file=%d'>%s</a>.</p>" % (line, path, review.repository.name, commit.sha1, review.id, chain.file_id, commit.sha1)
-
- renderReviewCommentChain(db, target, user, review, chain, linkify, message)
+ renderCodeCommentChain(db, target, user, review, chain, context_lines, compact, tabify, original, changeset, linkify)
elif chain.first_commit:
renderCommitCommentChain(db, target, user, review, chain, linkify)
else:
View
43 reviewing/mail.py
@@ -64,35 +64,13 @@ def renderChainInMail(db, to_user, chain, focus_comment, new_state, new_type, li
if chain.file_id:
path = dbutils.describe_file(db, chain.file_id)
- if chain.first_commit == chain.last_commit:
- cursor.execute("""SELECT null, sha1
- FROM commentchainlines
- WHERE chain=%s
- AND commit=%s""",
- (chain.id, chain.first_commit.getId(db)))
- elif chain.origin == 'old':
- cursor.execute("""SELECT old_mode, old_sha1
- FROM fileversions, changesets, reviewchangesets
- WHERE fileversions.changeset=changesets.id
- AND fileversions.file=%s
- AND changesets.parent=%s
- AND reviewchangesets.changeset=changesets.id
- AND reviewchangesets.review=%s""",
- [chain.file_id, chain.first_commit.getId(db), chain.review.id])
+ if chain.first_commit == chain.last_commit or chain.origin == 'old':
+ entry = chain.first_commit.getFileEntry(path)
else:
- cursor.execute("""SELECT new_mode, new_sha1
- FROM fileversions, changesets, reviewchangesets
- WHERE fileversions.changeset=changesets.id
- AND fileversions.file=%s
- AND changesets.child=%s
- AND reviewchangesets.changeset=changesets.id
- AND reviewchangesets.review=%s""",
- [chain.file_id, chain.last_commit.getId(db), chain.review.id])
-
- try: mode, sha1 = cursor.fetchone()
- except:
- mode = None
- sha1 = chain.lines_by_sha1.keys()[0]
+ entry = chain.last_commit.getFileEntry(path)
+
+ sha1 = entry.sha1
+ mode = entry.mode
first_line, count = chain.lines_by_sha1[sha1]
@@ -100,12 +78,9 @@ def renderChainInMail(db, to_user, chain, focus_comment, new_state, new_type, li
if context: result += "%s in %s, %s:\n%s\n%s\n" % (chain.type.capitalize(), path, context, url, hr)
else: result += "%s in %s:\n%s\n%s\n" % (chain.type.capitalize(), path, url, hr)
- try:
- file = diff.File(id=chain.file_id, path=path, new_mode=mode, new_sha1=sha1, repository=chain.review.repository)
- file.loadNewLines()
- lines = file.newLines(False)
- except:
- raise Exception, repr((chain.id, file.id, mode, sha1))
+ file = diff.File(id=chain.file_id, path=path, new_mode=mode, new_sha1=sha1, repository=chain.review.repository)
+ file.loadNewLines()
+ lines = file.newLines(False)
last_line = first_line + count - 1
first_line = max(1, first_line - context_lines)
View
5 reviewing/utils.py
@@ -22,7 +22,7 @@
import mail
import changeset.utils as changeset_utils
-import reviewing.comment as review_comment
+import reviewing.comment
import log.commitset as log_commitset
from operation import OperationError, OperationFailure
@@ -395,8 +395,7 @@ def addCommitsToReview(db, user, review, commits, new_review=False, commitset=No
review.incrementSerial(db)
- for changeset in changesets:
- review_comment.updateCommentChains(db, user, review, changeset)
+ reviewing.comment.propagateCommentChains(db, user, review, new_commits)
if pending_mails is None: pending_mails = []
Please sign in to comment.
Something went wrong with that request. Please try again.