Index: robots.txt
===================================================================
RCS file: /cvs/drupal/drupal/robots.txt,v
retrieving revision 1.13
diff -u -p -r1.13 robots.txt
--- robots.txt	29 Nov 2008 09:33:50 -0000	1.13
+++ robots.txt	15 Jun 2009 16:27:13 -0000
@@ -41,7 +41,7 @@ Disallow: /UPGRADE.txt
 Disallow: /xmlrpc.php
 # Paths (clean URLs)
 Disallow: /admin/
-Disallow: /comment/reply/
+Disallow: /comment/
 Disallow: /contact/
 Disallow: /node/add/
 Disallow: /search/
@@ -51,7 +51,7 @@ Disallow: /user/login/
 Disallow: /user/logout/
 # Paths (no clean URLs)
 Disallow: /?q=admin/
-Disallow: /?q=comment/reply/
+Disallow: /?q=comment/
 Disallow: /?q=contact/
 Disallow: /?q=node/add/
 Disallow: /?q=search/
Index: modules/comment/comment.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.admin.inc,v
retrieving revision 1.23
diff -u -p -r1.23 comment.admin.inc
--- modules/comment/comment.admin.inc	6 Jun 2009 10:27:42 -0000	1.23
+++ modules/comment/comment.admin.inc	15 Jun 2009 16:27:14 -0000
@@ -86,7 +86,7 @@ function comment_admin_overview($type = 
 
   foreach ($result as $comment) {
     $options[$comment->cid] = array(
-      'subject' => l($comment->subject, 'node/' . $comment->nid, array('attributes' => array('title' => truncate_utf8($comment->comment, 128)), 'fragment' => 'comment-' . $comment->cid)),
+      'subject' => l($comment->subject, 'comment/' . $comment->cid, array('attributes' => array('title' => truncate_utf8($comment->comment, 128)), 'fragment' => 'comment-' . $comment->cid)),
       'author' => theme('username', $comment),
       'posted_in' => l($comment->node_title, 'node/' . $comment->nid),
       'time' => format_date($comment->timestamp, 'small'),
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.722
diff -u -p -r1.722 comment.module
--- modules/comment/comment.module	11 Jun 2009 15:17:15 -0000	1.722
+++ modules/comment/comment.module	15 Jun 2009 16:27:15 -0000
@@ -204,11 +204,35 @@ function comment_menu() {
     'access arguments' => array('administer comments'),
     'type' => MENU_CALLBACK,
   );
+  $items['comment/%comment'] = array(
+    'title' => 'Comment permalink',
+    'page callback' => 'comment_permalink',
+    'access arguments' => array('access comments'),
+    'type' => MENU_CALLBACK,
+  );
 
   return $items;
 }
 
 /**
+ * Redirects comment links to the correct page depending on comment settings.
+ */
+function comment_permalink() {
+  $comment = menu_get_object('comment');
+  $node = node_load($comment->nid);
+  if ($node && $comment) {
+    $page = comment_get_display_page($comment->cid, $node->type);
+    $_GET['q'] = 'node/' . $node->nid;
+    $_GET['page'] = $page;
+    return menu_execute_active_handler('node/' . $node->nid);
+  }
+  else {
+    return drupal_not_found();
+  }
+}
+
+
+/**
  * Implement hook_node_type().
  */
 function comment_node_type($op, $info) {
@@ -404,7 +428,7 @@ function theme_comment_block() {
   $items = array();
   $number = variable_get('comment_block_count', 10);
   foreach (comment_get_recent($number) as $comment) {
-    $items[] = l($comment->subject, 'node/' . $comment->nid, array('fragment' => 'comment-' . $comment->cid)) . '<br />' . t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->timestamp)));
+    $items[] = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)) . '<br />' . t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->timestamp)));
   }
 
   if ($items) {
@@ -1348,6 +1372,60 @@ function comment_num_new($nid, $timestam
 }
 
 /**
+ * Get the display ordinal for a comment, starting from 0.
+ *
+ * @param $cid
+ *   The comment ID.
+ * @param $node_type
+ *   The node type of the comment's parent.
+ * @return
+ *   The display ordinal for the comment.
+ */
+function comment_get_display_ordinal($cid, $node_type) {
+  // Count how many comments (c) are before $cid (d) in display order. This is
+  // the 0-based display ordinal.
+  $query = db_select('comment', 'c');
+  $query->innerJoin('comment', 'd', 'd.nid = c.nid');
+  $query->addExpression('COUNT(*)', 'count');
+  $query->condition('d.cid', $cid);
+  if (!user_access('administer comments')) {
+    $query->condition('c.status', COMMENT_PUBLISHED);
+  }
+  $mode = variable_get('comment_default_mode_' . $node_type, COMMENT_MODE_THREADED_EXPANDED);
+
+  if ($mode == COMMENT_MODE_FLAT_EXPANDED || $mode == COMMENT_MODE_FLAT_COLLAPSED) {
+    // For flat comments, cid is used for ordering comments due to
+    // unpredicatable behavior with timestamp, so we make the same assumption
+    // here.
+    $query->condition('c.cid', 'd.cid', '<');
+  }
+  else {
+    // For threaded comments, the c.thread column is used for ordering. We can
+    // use the vancode for comparison, but must remove the trailing slash.
+    // @see comment_render().
+    $query->where('SUBSTRING(c.thread, 1, (LENGTH(c.thread) -1)) < SUBSTRING(d.thread, 1, (LENGTH(d.thread) -1))');
+  }
+
+  return $query->execute()->fetchField();
+}
+
+/**
+ * Return the page number for a comment.
+ *
+ * @param $cid
+ *   The comment ID.
+ * @param $node_type
+ *   The node type the comment is attached to.
+ * @return
+ *   The page number.
+ */
+function comment_get_display_page($cid, $node_type) {
+  $ordinal = comment_get_display_ordinal($cid, $node_type);
+  $comments_per_page = variable_get('comment_default_per_page_' . $node_type, 50);
+  return floor($ordinal / $comments_per_page);
+}
+
+/**
  * Validate comment data.
  *
  * @param $edit
@@ -1801,11 +1879,7 @@ function _comment_form_submit(&$comment_
 function comment_form_submit($form, &$form_state) {
   _comment_form_submit($form_state['values']);
   if ($cid = comment_save($form_state['values'])) {
-    $node = node_load($form_state['values']['nid']);
-    // Add 1 to existing $node->comment count to include new comment.
-    $comment_count = $node->comment_count + 1;
-    $page = comment_new_page_count($comment_count, 1, $node);
-    $form_state['redirect'] = array('node/' . $node->nid, $page, "comment-$cid");
+    $form_state['redirect'] = array('comment/' . $cid, array(), "comment-$cid");
     return;
   }
 }
@@ -1868,7 +1942,7 @@ function template_preprocess_comment(&$v
   $variables['picture']   = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', $comment) : '';
   $variables['signature'] = $comment->signature;
   $variables['submitted'] = theme('comment_submitted', $comment);
-  $variables['title']     = l($comment->subject, $_GET['q'], array('fragment' => "comment-$comment->cid"));
+  $variables['title']     = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => "comment-$comment->cid"));
   $variables['template_files'][] = 'comment-' . $node->type;
   // Set status to a string representation of comment->status.
   if (isset($comment->preview)) {
@@ -1909,7 +1983,7 @@ function template_preprocess_comment_fol
   $variables['author'] = theme('username', $comment);
   $variables['date']   = format_date($comment->timestamp);
   $variables['new']    = $comment->new ? t('new') : '';
-  $variables['title']  = l($comment->subject, comment_node_url() . '/' . $comment->cid, array('fragment' => "comment-$comment->cid"));
+  $variables['title']  = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => "comment-$comment->cid"));
   // Gather comment classes.
   if ($comment->uid === 0) {
     $variables['classes_array'][] = 'comment-by-anonymous';
Index: modules/comment/comment.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.test,v
retrieving revision 1.31
diff -u -p -r1.31 comment.test
--- modules/comment/comment.test	3 Jun 2009 06:52:29 -0000	1.31
+++ modules/comment/comment.test	15 Jun 2009 16:27:16 -0000
@@ -123,7 +123,9 @@ class CommentHelperCase extends DrupalWe
    *   Form value.
    */
   function setCommentForm($enabled) {
-    $this->setCommentSettings('comment_form_location', ($enabled ? '1' : '3'), 'Comment controls ' . ($enabled ? 'enabled' : 'disabled') . '.');
+    $this->setCommentSettings('comment_form_location',
+      ($enabled ? COMMENT_FORM_BELOW : COMMENT_FORM_SEPARATE_PAGE),
+      'Comment controls ' . ($enabled ? 'enabled' : 'disabled') . '.');
   }
 
   /**
@@ -143,7 +145,7 @@ class CommentHelperCase extends DrupalWe
    *   Comments per page value.
    */
   function setCommentsPerPage($number) {
-    $this->setCommentSettings('comment_default_per_page_article', $number, 'Number of comments per page set to ' . $number . '.');
+    $this->setCommentSettings('comment_default_per_page', $number, 'Number of comments per page set to ' . $number . '.');
   }
 
   /**
@@ -244,7 +246,9 @@ class CommentInterfaceTest extends Comme
     // Set comments to not have subject.
     $this->drupalLogin($this->admin_user);
     $this->setCommentPreview(TRUE);
+    $this->setCommentForm(TRUE);
     $this->setCommentSubject(FALSE);
+    $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED_EXPANDED, t('Comment paging changed.'));
     $this->drupalLogout();
 
     // Post comment without subject.
@@ -300,11 +304,10 @@ class CommentInterfaceTest extends Comme
     $this->drupalGet('node');
     $this->assertRaw('3 comments', t('Link to the 3 comments exist.'));
 
-    // Pager
+    // Confirm a new comment is posted to the correct page.
     $this->setCommentsPerPage(2);
     $comment_new_page = $this->postComment($this->node, $this->randomName(), $this->randomName());
-    $this->drupalGet('node/' . $this->node->nid);
-    $this->assertTrue($this->commentExists($comment) && $this->commentExists($comment_new_page), t('Page one exists. %s'));
+    $this->assertTrue($this->commentExists($comment_new_page), t('Page one exists. %s'));
     $this->drupalGet('node/' . $this->node->nid, array('query' => 'page=1'));
     $this->assertTrue($this->commentExists($reply, TRUE), t('Page two exists. %s'));
     $this->setCommentsPerPage(50);
@@ -454,6 +457,78 @@ class CommentAnonymous extends CommentHe
   }
 }
 
+/**
+ * Verify pagination of comments
+ */
+class CommentPagerTest extends CommentHelperCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => t('Comment paging settings'),
+      'description' => t('Test paging of comments and their settings.'),
+      'group' => t('Comment'),
+    );
+  }
+
+  function testCommentPaging() {
+    $this->drupalLogin($this->admin_user);
+    $this->setCommentForm(TRUE);
+    $this->setCommentSubject(TRUE);
+    $this->setCommentPreview(FALSE);
+    $node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
+    $comments = array();
+    $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), FALSE, TRUE);
+    $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), FALSE, TRUE);
+    $comments[] = $this->postComment($node, $this->randomName(), $this->randomName(), FALSE, TRUE);
+
+    $this->setCommentSettings('comment_default_mode', COMMENT_MODE_FLAT_EXPANDED, t('Comment paging changed.'));
+    $this->setCommentsPerPage(1);
+    $this->drupalGet('node/' . $node->nid);
+    $this->assertRaw(t('next'), t('Paging links found.'));
+    $this->assertTrue($this->commentExists($comments[0]), t('Comment 1 appears on page 1.'));
+    $this->assertFalse($this->commentExists($comments[1]), t('Comment 2 does not appear on page 1.'));
+    $this->assertFalse($this->commentExists($comments[2]), t('Comment 3 does not appear on page 1.'));
+
+    $this->drupalGet('node/' . $node->nid, array('query' => 'page=1'));
+    $this->assertTrue($this->commentExists($comments[1]), t('Comment 2 appears on page 2.'));
+    $this->assertFalse($this->commentExists($comments[0]), t('Comment 1 does not appear on page 2.'));
+    $this->assertFalse($this->commentExists($comments[2]), t('Comment 3 does not appear on page 2.'));
+
+    $this->drupalGet('node/' . $node->nid, array('query' => 'page=2'));
+    $this->assertTrue($this->commentExists($comments[2]), t('Comment 3 appears on page 3.'));
+    $this->assertFalse($this->commentExists($comments[0]), t('Comment 1 does not appear on page 3.'));
+    $this->assertFalse($this->commentExists($comments[1]), t('Comment 2 does not appear on page 3.'));
+
+    // Post a reply to the oldest comment and test again.
+    $replies = array();
+    $oldest_comment = reset($comments);
+    $this->drupalGet('comment/reply/' . $node->nid . '/' . $oldest_comment->id);
+    $reply = $this->postComment(null, $this->randomName(), $this->randomName(), FALSE, TRUE);
+
+    $this->setCommentsPerPage(2);
+    // We are still in flat view - the replies should not be on the first page,
+    // even though they are replies to the oldest comment.
+    $this->drupalGet('node/' . $node->nid, array('query' => 'page=0'));
+    $this->assertFalse($this->commentExists($reply, TRUE), t('In flat mode, reply does not appear on page 1.'));
+
+    // If we switch to threaded mode, the replies on the oldest comment
+    // should be bumped to the first page and comment 6 should be bumped
+    // to the second page.
+    $this->setCommentSettings('comment_default_mode', COMMENT_MODE_THREADED_EXPANDED, t('Switched to threaded mode.'));
+    $this->drupalGet('node/' . $node->nid, array('query' => 'page=0'));
+    $this->assertTrue($this->commentExists($reply, TRUE), t('In threaded mode, reply appears on page 1.'));
+    $this->assertFalse($this->commentExists($comments[1]), t('In threaded mode, comment 2 has been bumped off of page 1.'));
+
+    // If (# replies > # comments per page) in threaded expanded view,
+    // the overage should be bumped.
+    $reply2 = $this->postComment(NULL, $this->randomName(), $this->randomName(), FALSE, TRUE);
+    $this->drupalGet('node/' . $node->nid, array('query' => 'page=0'));
+    $this->assertFalse($this->commentExists($reply2, TRUE), t('In threaded mode where # replies > # comments per page, the newest reply does not appear on page 1.'));
+
+    $this->drupalLogout();
+  }
+}
+
 class CommentApprovalTest extends CommentHelperCase {
   public static function getInfo() {
     return array(
@@ -595,10 +670,25 @@ class CommentBlockFunctionalTest extends
     $this->drupalPost('admin/build/block/configure/comment/recent', $block, t('Save block'));
     $this->assertText(t('The block configuration has been saved.'), t('Block saved.'));
 
-    // Test that all three comments are shown.
+    // Post an additional comment.
+    $comment4 = $this->postComment($this->node, $this->randomName(), $this->randomName());
+
+    // Test that all four comments are shown.
     $this->assertText($comment1->subject, t('Comment found in block.'));
     $this->assertText($comment2->subject, t('Comment found in block.'));
     $this->assertText($comment3->comment, t('Comment found in block.'));
+    $this->assertText($comment4->subject, t('Comment found in block.'));
+
+    // Test that links to comments work when comments are across pages.
+    $this->setCommentsPerPage(1);
+    $this->drupalGet('');
+    $this->clickLink($comment1->subject);
+    $this->assertText($comment1->subject, t('Comment link goes to correct page.'));
+    $this->drupalGet('');
+    $this->clickLink($comment2->subject);
+    $this->assertText($comment2->subject, t('Comment link goes to correct page.'));
+    $this->clickLink($comment4->subject);
+    $this->assertText($comment4->subject, t('Comment link goes to correct page.'));
   }
 }
 
