PageRenderTime 44ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/src/applications/differential/editor/comment/DifferentialCommentEditor.php

http://github.com/facebook/phabricator
PHP | 553 lines | 470 code | 59 blank | 24 comment | 37 complexity | 38d96bace661b64a1e2c808f2631584c MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0
  1. <?php
  2. /*
  3. * Copyright 2012 Facebook, Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. class DifferentialCommentEditor {
  18. protected $revision;
  19. protected $actorPHID;
  20. protected $action;
  21. protected $attachInlineComments;
  22. protected $message;
  23. protected $changedByCommit;
  24. protected $addedReviewers = array();
  25. private $addedCCs = array();
  26. private $parentMessageID;
  27. private $contentSource;
  28. public function __construct(
  29. DifferentialRevision $revision,
  30. $actor_phid,
  31. $action) {
  32. $this->revision = $revision;
  33. $this->actorPHID = $actor_phid;
  34. $this->action = $action;
  35. }
  36. public function setParentMessageID($parent_message_id) {
  37. $this->parentMessageID = $parent_message_id;
  38. return $this;
  39. }
  40. public function setMessage($message) {
  41. $this->message = $message;
  42. return $this;
  43. }
  44. public function setAttachInlineComments($attach) {
  45. $this->attachInlineComments = $attach;
  46. return $this;
  47. }
  48. public function setChangedByCommit($changed_by_commit) {
  49. $this->changedByCommit = $changed_by_commit;
  50. return $this;
  51. }
  52. public function getChangedByCommit() {
  53. return $this->changedByCommit;
  54. }
  55. public function setAddedReviewers($added_reviewers) {
  56. $this->addedReviewers = $added_reviewers;
  57. return $this;
  58. }
  59. public function getAddedReviewers() {
  60. return $this->addedReviewers;
  61. }
  62. public function setAddedCCs($added_ccs) {
  63. $this->addedCCs = $added_ccs;
  64. return $this;
  65. }
  66. public function getAddedCCs() {
  67. return $this->addedCCs;
  68. }
  69. public function setContentSource(PhabricatorContentSource $content_source) {
  70. $this->contentSource = $content_source;
  71. return $this;
  72. }
  73. public function save() {
  74. $revision = $this->revision;
  75. $action = $this->action;
  76. $actor_phid = $this->actorPHID;
  77. $actor = id(new PhabricatorUser())->loadOneWhere('PHID = %s', $actor_phid);
  78. $actor_is_author = ($actor_phid == $revision->getAuthorPHID());
  79. $actor_is_admin = $actor->getIsAdmin();
  80. $revision_status = $revision->getStatus();
  81. $revision->loadRelationships();
  82. $reviewer_phids = $revision->getReviewers();
  83. if ($reviewer_phids) {
  84. $reviewer_phids = array_combine($reviewer_phids, $reviewer_phids);
  85. }
  86. $metadata = array();
  87. $inline_comments = array();
  88. if ($this->attachInlineComments) {
  89. $inline_comments = id(new DifferentialInlineComment())->loadAllWhere(
  90. 'authorPHID = %s AND revisionID = %d AND commentID IS NULL',
  91. $this->actorPHID,
  92. $revision->getID());
  93. }
  94. switch ($action) {
  95. case DifferentialAction::ACTION_COMMENT:
  96. if (!$this->message && !$inline_comments) {
  97. throw new DifferentialActionHasNoEffectException(
  98. "You are submitting an empty comment with no action: ".
  99. "you must act on the revision or post a comment.");
  100. }
  101. break;
  102. case DifferentialAction::ACTION_RESIGN:
  103. if ($actor_is_author) {
  104. throw new Exception('You can not resign from your own revision!');
  105. }
  106. if (empty($reviewer_phids[$actor_phid])) {
  107. throw new DifferentialActionHasNoEffectException(
  108. "You can not resign from this revision because you are not ".
  109. "a reviewer.");
  110. }
  111. DifferentialRevisionEditor::alterReviewers(
  112. $revision,
  113. $reviewer_phids,
  114. $rem = array($actor_phid),
  115. $add = array(),
  116. $actor_phid);
  117. break;
  118. case DifferentialAction::ACTION_ABANDON:
  119. if (!($actor_is_author || $actor_is_admin)) {
  120. throw new Exception('You can only abandon your own revisions.');
  121. }
  122. if ($revision_status == ArcanistDifferentialRevisionStatus::COMMITTED) {
  123. throw new DifferentialActionHasNoEffectException(
  124. "You can not abandon this revision because it has already ".
  125. "been committed.");
  126. }
  127. if ($revision_status == ArcanistDifferentialRevisionStatus::ABANDONED) {
  128. throw new DifferentialActionHasNoEffectException(
  129. "You can not abandon this revision because it has already ".
  130. "been abandoned.");
  131. }
  132. $revision->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED);
  133. break;
  134. case DifferentialAction::ACTION_ACCEPT:
  135. if ($actor_is_author) {
  136. throw new Exception('You can not accept your own revision.');
  137. }
  138. if (($revision_status !=
  139. ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) &&
  140. ($revision_status !=
  141. ArcanistDifferentialRevisionStatus::NEEDS_REVISION)) {
  142. switch ($revision_status) {
  143. case ArcanistDifferentialRevisionStatus::ACCEPTED:
  144. throw new DifferentialActionHasNoEffectException(
  145. "You can not accept this revision because someone else ".
  146. "already accepted it.");
  147. case ArcanistDifferentialRevisionStatus::ABANDONED:
  148. throw new DifferentialActionHasNoEffectException(
  149. "You can not accept this revision because it has been ".
  150. "abandoned.");
  151. case ArcanistDifferentialRevisionStatus::COMMITTED:
  152. throw new DifferentialActionHasNoEffectException(
  153. "You can not accept this revision because it has already ".
  154. "been committed.");
  155. default:
  156. throw new Exception(
  157. "Unexpected revision state '{$revision_status}'!");
  158. }
  159. }
  160. $revision
  161. ->setStatus(ArcanistDifferentialRevisionStatus::ACCEPTED);
  162. if (!isset($reviewer_phids[$actor_phid])) {
  163. DifferentialRevisionEditor::alterReviewers(
  164. $revision,
  165. $reviewer_phids,
  166. $rem = array(),
  167. $add = array($actor_phid),
  168. $actor_phid);
  169. }
  170. break;
  171. case DifferentialAction::ACTION_REQUEST:
  172. if (!$actor_is_author) {
  173. throw new Exception('You must own a revision to request review.');
  174. }
  175. switch ($revision_status) {
  176. case ArcanistDifferentialRevisionStatus::ACCEPTED:
  177. case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
  178. $revision->setStatus(
  179. ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
  180. break;
  181. case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
  182. throw new DifferentialActionHasNoEffectException(
  183. "You can not request review of this revision because it has ".
  184. "been abandoned.");
  185. case ArcanistDifferentialRevisionStatus::ABANDONED:
  186. throw new DifferentialActionHasNoEffectException(
  187. "You can not request review of this revision because it has ".
  188. "been abandoned.");
  189. case ArcanistDifferentialRevisionStatus::COMMITTED:
  190. throw new DifferentialActionHasNoEffectException(
  191. "You can not request review of this revision because it has ".
  192. "already been committed.");
  193. default:
  194. throw new Exception(
  195. "Unexpected revision state '{$revision_status}'!");
  196. }
  197. break;
  198. case DifferentialAction::ACTION_REJECT:
  199. if ($actor_is_author) {
  200. throw new Exception(
  201. 'You can not request changes to your own revision.');
  202. }
  203. switch ($revision_status) {
  204. case ArcanistDifferentialRevisionStatus::ACCEPTED:
  205. case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
  206. case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
  207. // NOTE: We allow you to reject an already-rejected revision
  208. // because it doesn't create any ambiguity and avoids a rather
  209. // needless dialog.
  210. break;
  211. case ArcanistDifferentialRevisionStatus::ABANDONED:
  212. throw new DifferentialActionHasNoEffectException(
  213. "You can not request changes to this revision because it has ".
  214. "been abandoned.");
  215. case ArcanistDifferentialRevisionStatus::COMMITTED:
  216. throw new DifferentialActionHasNoEffectException(
  217. "You can not request changes to this revision because it has ".
  218. "already been committed.");
  219. default:
  220. throw new Exception(
  221. "Unexpected revision state '{$revision_status}'!");
  222. }
  223. if (!isset($reviewer_phids[$actor_phid])) {
  224. DifferentialRevisionEditor::alterReviewers(
  225. $revision,
  226. $reviewer_phids,
  227. $rem = array(),
  228. $add = array($actor_phid),
  229. $actor_phid);
  230. }
  231. $revision
  232. ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVISION);
  233. break;
  234. case DifferentialAction::ACTION_RETHINK:
  235. if (!$actor_is_author) {
  236. throw new Exception(
  237. "You can not plan changes to somebody else's revision");
  238. }
  239. switch ($revision_status) {
  240. case ArcanistDifferentialRevisionStatus::ACCEPTED:
  241. case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
  242. case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
  243. break;
  244. case ArcanistDifferentialRevisionStatus::ABANDONED:
  245. throw new DifferentialActionHasNoEffectException(
  246. "You can not plan changes to this revision because it has ".
  247. "been abandoned.");
  248. case ArcanistDifferentialRevisionStatus::COMMITTED:
  249. throw new DifferentialActionHasNoEffectException(
  250. "You can not plan changes to this revision because it has ".
  251. "already been committed.");
  252. default:
  253. throw new Exception(
  254. "Unexpected revision state '{$revision_status}'!");
  255. }
  256. $revision
  257. ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVISION);
  258. break;
  259. case DifferentialAction::ACTION_RECLAIM:
  260. if (!$actor_is_author) {
  261. throw new Exception('You can not reclaim a revision you do not own.');
  262. }
  263. if ($revision_status != ArcanistDifferentialRevisionStatus::ABANDONED) {
  264. throw new DifferentialActionHasNoEffectException(
  265. "You can not reclaim this revision because it is not abandoned.");
  266. }
  267. $revision
  268. ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
  269. break;
  270. case DifferentialAction::ACTION_COMMIT:
  271. $revision
  272. ->setStatus(ArcanistDifferentialRevisionStatus::COMMITTED);
  273. break;
  274. case DifferentialAction::ACTION_ADDREVIEWERS:
  275. $added_reviewers = $this->getAddedReviewers();
  276. $user_tried_to_add = count($added_reviewers);
  277. foreach ($added_reviewers as $k => $user_phid) {
  278. if ($user_phid == $revision->getAuthorPHID()) {
  279. unset($added_reviewers[$k]);
  280. }
  281. if (!empty($reviewer_phids[$user_phid])) {
  282. unset($added_reviewers[$k]);
  283. }
  284. }
  285. $added_reviewers = array_unique($added_reviewers);
  286. if ($added_reviewers) {
  287. DifferentialRevisionEditor::alterReviewers(
  288. $revision,
  289. $reviewer_phids,
  290. $rem = array(),
  291. $add = $added_reviewers,
  292. $actor_phid);
  293. $key = DifferentialComment::METADATA_ADDED_REVIEWERS;
  294. $metadata[$key] = $added_reviewers;
  295. } else {
  296. if ($user_tried_to_add == 0) {
  297. throw new DifferentialActionHasNoEffectException(
  298. "You can not add reviewers, because you did not specify any ".
  299. "reviewers.");
  300. } else if ($user_tried_to_add == 1) {
  301. throw new DifferentialActionHasNoEffectException(
  302. "You can not add that reviewer, because they are already an ".
  303. "author or reviewer.");
  304. } else {
  305. throw new DifferentialActionHasNoEffectException(
  306. "You can not add those reviewers, because they are all already ".
  307. "authors or reviewers.");
  308. }
  309. }
  310. break;
  311. case DifferentialAction::ACTION_ADDCCS:
  312. $added_ccs = $this->getAddedCCs();
  313. $user_tried_to_add = count($added_ccs);
  314. $added_ccs = $this->filterAddedCCs($added_ccs);
  315. if ($added_ccs) {
  316. foreach ($added_ccs as $cc) {
  317. DifferentialRevisionEditor::addCC(
  318. $revision,
  319. $cc,
  320. $this->actorPHID);
  321. }
  322. $key = DifferentialComment::METADATA_ADDED_CCS;
  323. $metadata[$key] = $added_ccs;
  324. } else {
  325. if ($user_tried_to_add == 0) {
  326. throw new DifferentialActionHasNoEffectException(
  327. "You can not add CCs, because you did not specify any ".
  328. "CCs.");
  329. } else if ($user_tried_to_add == 1) {
  330. throw new DifferentialActionHasNoEffectException(
  331. "You can not add that CC, because they are already an ".
  332. "author, reviewer or CC.");
  333. } else {
  334. throw new DifferentialActionHasNoEffectException(
  335. "You can not add those CCs, because they are all already ".
  336. "authors, reviewers or CCs.");
  337. }
  338. }
  339. break;
  340. default:
  341. throw new Exception('Unsupported action.');
  342. }
  343. // Always save the revision (even if we didn't actually change any of its
  344. // properties) so that it jumps to the top of the revision list when sorted
  345. // by "updated". Notably, this allows "ping" comments to push it to the
  346. // top of the action list.
  347. $revision->save();
  348. if ($action != DifferentialAction::ACTION_RESIGN &&
  349. $this->actorPHID != $revision->getAuthorPHID() &&
  350. !in_array($this->actorPHID, $revision->getReviewers())) {
  351. DifferentialRevisionEditor::addCC(
  352. $revision,
  353. $this->actorPHID,
  354. $this->actorPHID);
  355. }
  356. $comment = id(new DifferentialComment())
  357. ->setAuthorPHID($this->actorPHID)
  358. ->setRevisionID($revision->getID())
  359. ->setAction($action)
  360. ->setContent((string)$this->message)
  361. ->setMetadata($metadata);
  362. if ($this->contentSource) {
  363. $comment->setContentSource($this->contentSource);
  364. }
  365. $comment->save();
  366. $changesets = array();
  367. if ($inline_comments) {
  368. $load_ids = mpull($inline_comments, 'getChangesetID');
  369. if ($load_ids) {
  370. $load_ids = array_unique($load_ids);
  371. $changesets = id(new DifferentialChangeset())->loadAllWhere(
  372. 'id in (%Ld)',
  373. $load_ids);
  374. }
  375. foreach ($inline_comments as $inline) {
  376. $inline->setCommentID($comment->getID());
  377. $inline->save();
  378. }
  379. }
  380. // Find any "@mentions" in the comment blocks.
  381. $content_blocks = array($comment->getContent());
  382. foreach ($inline_comments as $inline) {
  383. $content_blocks[] = $inline->getContent();
  384. }
  385. $mention_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions(
  386. $content_blocks);
  387. if ($mention_ccs) {
  388. $mention_ccs = $this->filterAddedCCs($mention_ccs);
  389. if ($mention_ccs) {
  390. $metadata = $comment->getMetadata();
  391. $metacc = idx(
  392. $metadata,
  393. DifferentialComment::METADATA_ADDED_CCS,
  394. array());
  395. foreach ($mention_ccs as $cc_phid) {
  396. DifferentialRevisionEditor::addCC(
  397. $revision,
  398. $cc_phid,
  399. $this->actorPHID);
  400. $metacc[] = $cc_phid;
  401. }
  402. $metadata[DifferentialComment::METADATA_ADDED_CCS] = $metacc;
  403. $comment->setMetadata($metadata);
  404. $comment->save();
  405. }
  406. }
  407. $phids = array($this->actorPHID);
  408. $handles = id(new PhabricatorObjectHandleData($phids))
  409. ->loadHandles();
  410. $actor_handle = $handles[$this->actorPHID];
  411. $xherald_header = HeraldTranscript::loadXHeraldRulesHeader(
  412. $revision->getPHID());
  413. id(new DifferentialCommentMail(
  414. $revision,
  415. $actor_handle,
  416. $comment,
  417. $changesets,
  418. $inline_comments))
  419. ->setToPHIDs(
  420. array_merge(
  421. $revision->getReviewers(),
  422. array($revision->getAuthorPHID())))
  423. ->setCCPHIDs($revision->getCCPHIDs())
  424. ->setChangedByCommit($this->getChangedByCommit())
  425. ->setXHeraldRulesHeader($xherald_header)
  426. ->setParentMessageID($this->parentMessageID)
  427. ->send();
  428. $event_data = array(
  429. 'revision_id' => $revision->getID(),
  430. 'revision_phid' => $revision->getPHID(),
  431. 'revision_name' => $revision->getTitle(),
  432. 'revision_author_phid' => $revision->getAuthorPHID(),
  433. 'action' => $comment->getAction(),
  434. 'feedback_content' => $comment->getContent(),
  435. 'actor_phid' => $this->actorPHID,
  436. );
  437. id(new PhabricatorTimelineEvent('difx', $event_data))
  438. ->recordEvent();
  439. // TODO: Move to a daemon?
  440. id(new PhabricatorFeedStoryPublisher())
  441. ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_DIFFERENTIAL)
  442. ->setStoryData($event_data)
  443. ->setStoryTime(time())
  444. ->setStoryAuthorPHID($this->actorPHID)
  445. ->setRelatedPHIDs(
  446. array(
  447. $revision->getPHID(),
  448. $this->actorPHID,
  449. $revision->getAuthorPHID(),
  450. ))
  451. ->publish();
  452. // TODO: Move to a daemon?
  453. PhabricatorSearchDifferentialIndexer::indexRevision($revision);
  454. return $comment;
  455. }
  456. private function filterAddedCCs(array $ccs) {
  457. $revision = $this->revision;
  458. $current_ccs = $revision->getCCPHIDs();
  459. $current_ccs = array_fill_keys($current_ccs, true);
  460. $reviewer_phids = $revision->getReviewers();
  461. $reviewer_phids = array_fill_keys($reviewer_phids, true);
  462. foreach ($ccs as $key => $cc) {
  463. if (isset($current_ccs[$cc])) {
  464. unset($ccs[$key]);
  465. }
  466. if (isset($reviewer_phids[$cc])) {
  467. unset($ccs[$key]);
  468. }
  469. if ($cc == $revision->getAuthorPHID()) {
  470. unset($ccs[$key]);
  471. }
  472. }
  473. return $ccs;
  474. }
  475. }