protected function recordCommit($commit_identifier, $epoch) { $repository = $this->getRepository(); $commit = new PhabricatorRepositoryCommit(); $commit->setRepositoryID($repository->getID()); $commit->setCommitIdentifier($commit_identifier); $commit->setEpoch($epoch); try { $commit->save(); $event = new PhabricatorTimelineEvent('cmit', array('id' => $commit->getID())); $event->recordEvent(); queryfx($repository->establishConnection('w'), 'INSERT INTO %T (repositoryID, size, lastCommitID, epoch) VALUES (%d, 1, %d, %d) ON DUPLICATE KEY UPDATE size = size + 1, lastCommitID = IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID), epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)', PhabricatorRepository::TABLE_SUMMARY, $repository->getID(), $commit->getID(), $epoch); $this->commitCache[$commit_identifier] = true; } catch (AphrontQueryDuplicateKeyException $ex) { // Ignore. This can happen because we discover the same new commit // more than once when looking at history, or because of races or // data inconsistency or cosmic radiation; in any case, we're still // in a good state if we ignore the failure. $this->commitCache[$commit_identifier] = true; } $this->stillWorking(); }
private function checkAuditReasons(PhabricatorRepositoryCommit $commit, PhabricatorOwnersPackage $package) { $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID()); $reasons = array(); if ($data->getCommitDetail('vsDiff')) { $reasons[] = pht('Changed After Revision Was Accepted'); } $commit_author_phid = $data->getCommitDetail('authorPHID'); if (!$commit_author_phid) { $reasons[] = pht('Commit Author Not Recognized'); } $revision_id = $data->getCommitDetail('differential.revisionID'); $revision_author_phid = null; $commit_reviewedby_phid = null; if ($revision_id) { $revision = id(new DifferentialRevisionQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withIDs(array($revision_id))->executeOne(); if ($revision) { $revision_author_phid = $revision->getAuthorPHID(); $commit_reviewedby_phid = $data->getCommitDetail('reviewerPHID'); if ($revision_author_phid !== $commit_author_phid) { $reasons[] = pht('Author Not Matching with Revision'); } } else { $reasons[] = pht('Revision Not Found'); } } else { $reasons[] = pht('No Revision Specified'); } $owners_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(array($package->getID())); if (!($commit_author_phid && in_array($commit_author_phid, $owners_phids) || $commit_reviewedby_phid && in_array($commit_reviewedby_phid, $owners_phids))) { $reasons[] = pht('Owners Not Involved'); } return $reasons; }
public static function indexCommit(PhabricatorRepositoryCommit $commit) { $commit_data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID()); $date_created = $commit->getEpoch(); $commit_message = $commit_data->getCommitMessage(); $author_phid = $commit_data->getCommitDetail('authorPHID'); $repository = id(new PhabricatorRepository())->loadOneWhere('id = %d', $commit->getRepositoryID()); if (!$repository) { return; } $title = 'r' . $repository->getCallsign() . $commit->getCommitIdentifier() . " " . $commit_data->getSummary(); $doc = new PhabricatorSearchAbstractDocument(); $doc->setPHID($commit->getPHID()); $doc->setDocumentType(PhabricatorPHIDConstants::PHID_TYPE_CMIT); $doc->setDocumentCreated($date_created); $doc->setDocumentModified($date_created); $doc->setDocumentTitle($title); $doc->addField(PhabricatorSearchField::FIELD_BODY, $commit_message); if ($author_phid) { $doc->addRelationship(PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, $author_phid, PhabricatorPHIDConstants::PHID_TYPE_USER, $date_created); } $doc->addRelationship(PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY, $repository->getPHID(), PhabricatorPHIDConstants::PHID_TYPE_REPO, $date_created); $comments = id(new PhabricatorAuditComment())->loadAllWhere('targetPHID = %s', $commit->getPHID()); foreach ($comments as $comment) { if (strlen($comment->getContent())) { $doc->addField(PhabricatorSearchField::FIELD_COMMENT, $comment->getContent()); } } self::reindexAbstractDocument($doc); }
protected function parseCommitWithRef(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, DiffusionCommitRef $ref) { $this->updateCommitData($ref); if ($this->shouldQueueFollowupTasks()) { $this->queueTask('PhabricatorRepositoryMercurialCommitChangeParserWorker', array('commitID' => $commit->getID())); } }
public function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $ref = id(new DiffusionLowLevelCommitQuery())->setRepository($repository)->withIdentifier($commit->getCommitIdentifier())->execute(); $this->updateCommitData($ref); if ($this->shouldQueueFollowupTasks()) { $this->queueTask('PhabricatorRepositorySvnCommitChangeParserWorker', array('commitID' => $commit->getID())); } }
public function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { // NOTE: %B was introduced somewhat recently in git's history, so pull // commit message information with %s and %b instead. // Even though we pass --encoding here, git doesn't always succeed, so // we try a little harder, since git *does* tell us what the actual encoding // is correctly (unless it doesn't; encoding is sometimes empty). list($info) = $repository->execxLocalCommand('log -n 1 --encoding=%s --format=%s %s --', 'UTF-8', implode('%x00', array('%e', '%cn', '%ce', '%an', '%ae', '%s%n%n%b')), $commit->getCommitIdentifier()); $parts = explode("", $info); $encoding = array_shift($parts); // See note above - git doesn't always convert the encoding correctly. $do_convert = false; if (strlen($encoding) && strtoupper($encoding) != 'UTF-8') { if (function_exists('mb_convert_encoding')) { $do_convert = true; } } foreach ($parts as $key => $part) { if ($do_convert) { $parts[$key] = mb_convert_encoding($part, 'UTF-8', $encoding); } $parts[$key] = phutil_utf8ize($part); } $committer_name = $parts[0]; $committer_email = $parts[1]; $author_name = $parts[2]; $author_email = $parts[3]; $message = $parts[4]; if (strlen($author_email)) { $author = "{$author_name} <{$author_email}>"; } else { $author = "{$author_name}"; } if (strlen($committer_email)) { $committer = "{$committer_name} <{$committer_email}>"; } else { $committer = "{$committer_name}"; } if ($committer == $author) { $committer = null; } $this->updateCommitData($author, $message, $committer); if ($this->shouldQueueFollowupTasks()) { $task = new PhabricatorWorkerTask(); $task->setTaskClass('PhabricatorRepositoryGitCommitChangeParserWorker'); $task->setData(array('commitID' => $commit->getID())); $task->save(); } }
public function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { list($stdout) = $repository->execxLocalCommand('log --template %s --rev %s', '{author}\\n{desc}', $commit->getCommitIdentifier()); list($author, $message) = explode("\n", $stdout, 2); $author = phutil_utf8ize($author); $message = phutil_utf8ize($message); $message = trim($message); $this->updateCommitData($author, $message); if ($this->shouldQueueFollowupTasks()) { $task = new PhabricatorWorkerTask(); $task->setTaskClass('PhabricatorRepositoryMercurialCommitChangeParserWorker'); $task->setData(array('commitID' => $commit->getID())); $task->save(); } }
protected final function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { if (!$this->shouldSkipImportStep()) { $viewer = PhabricatorUser::getOmnipotentUser(); $refs_raw = DiffusionQuery::callConduitWithDiffusionRequest($viewer, DiffusionRequest::newFromDictionary(array('repository' => $repository, 'user' => $viewer)), 'diffusion.querycommits', array('repositoryPHID' => $repository->getPHID(), 'phids' => array($commit->getPHID()), 'bypassCache' => true, 'needMessages' => true)); if (empty($refs_raw['data'])) { throw new Exception(pht('Unable to retrieve details for commit "%s"!', $commit->getPHID())); } $ref = DiffusionCommitRef::newFromConduitResult(head($refs_raw['data'])); $this->updateCommitData($ref); } if ($this->shouldQueueFollowupTasks()) { $this->queueTask($this->getFollowupTaskClass(), array('commitID' => $commit->getID()), array('priority' => PhabricatorWorker::PRIORITY_DEFAULT)); } }
public function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $uri = $repository->getDetail('remote-uri'); $log = $this->getSVNLogXMLObject($uri, $commit->getCommitIdentifier(), $verbose = false); $entry = $log->logentry[0]; $author = (string) $entry->author; $message = (string) $entry->msg; $this->updateCommitData($author, $message); if ($this->shouldQueueFollowupTasks()) { $task = new PhabricatorWorkerTask(); $task->setTaskClass('PhabricatorRepositorySvnCommitChangeParserWorker'); $task->setData(array('commitID' => $commit->getID())); $task->save(); } }
public function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { // NOTE: %B was introduced somewhat recently in git's history, so pull // commit message information with %s and %b instead. list($info) = $repository->execxLocalCommand("log -n 1 --encoding='UTF-8' --pretty=format:%%an%%x00%%s%%n%%n%%b %s", $commit->getCommitIdentifier()); list($author, $message) = explode("", $info); // Make sure these are valid UTF-8. $author = phutil_utf8ize($author); $message = phutil_utf8ize($message); $message = trim($message); $this->updateCommitData($author, $message); if ($this->shouldQueueFollowupTasks()) { $task = new PhabricatorWorkerTask(); $task->setTaskClass('PhabricatorRepositoryGitCommitChangeParserWorker'); $task->setData(array('commitID' => $commit->getID())); $task->save(); } }
private function checkAuditReasons(PhabricatorRepositoryCommit $commit, PhabricatorOwnersPackage $package) { $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID()); $reasons = array(); if ($data->getCommitDetail('vsDiff')) { $reasons[] = "Changed After Revision Was Accepted"; } $commit_author_phid = $data->getCommitDetail('authorPHID'); if (!$commit_author_phid) { $reasons[] = "Commit Author Not Recognized"; } $revision_id = $data->getCommitDetail('differential.revisionID'); $revision_author_phid = null; $commit_reviewedby_phid = null; $commit_author_phid = null; if ($revision_id) { $revision = id(new DifferentialRevision())->load($revision_id); if ($revision) { $revision->loadRelationships(); $revision_author_phid = $revision->getAuthorPHID(); $revision_reviewedby_phid = $revision->loadReviewedBy(); $commit_reviewedby_phid = $data->getCommitDetail('reviewerPHID'); $commit_author_phid = $data->getCommitDetail('authorPHID'); if ($revision_author_phid !== $commit_author_phid) { $reasons[] = "Author Not Matching with Revision"; } if ($revision_reviewedby_phid !== $commit_reviewedby_phid) { $reasons[] = "ReviewedBy Not Matching with Revision"; } } else { $reasons[] = "Revision Not Found"; } } else { $reasons[] = "No Revision Specified"; } $owners_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(array($package->getID())); if (!($commit_author_phid && in_array($commit_author_phid, $owners_phids) || $commit_reviewedby_phid && in_array($commit_reviewedby_phid, $owners_phids))) { $reasons[] = "Owners Not Involved"; } return $reasons; }
private function writeCommitChanges(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, array $changes) { $repository_id = (int) $repository->getID(); $commit_id = (int) $commit->getID(); // NOTE: This SQL is being built manually instead of with qsprintf() // because some SVN changes affect an enormous number of paths (millions) // and this showed up as significantly slow on a profile at some point. $changes_sql = array(); foreach ($changes as $change) { $values = array($repository_id, (int) $change->getPathID(), $commit_id, nonempty((int) $change->getTargetPathID(), 'null'), nonempty((int) $change->getTargetCommitID(), 'null'), (int) $change->getChangeType(), (int) $change->getFileType(), (int) $change->getIsDirect(), (int) $change->getCommitSequence()); $changes_sql[] = '(' . implode(', ', $values) . ')'; } $conn_w = $repository->establishConnection('w'); queryfx($conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit_id); foreach (PhabricatorLiskDAO::chunkSQL($changes_sql) as $chunk) { queryfx($conn_w, 'INSERT INTO %T (repositoryID, pathID, commitID, targetPathID, targetCommitID, changeType, fileType, isDirect, commitSequence) VALUES %Q', PhabricatorRepository::TABLE_PATHCHANGE, $chunk); } }
private function checkAuditReasons(PhabricatorRepositoryCommit $commit, PhabricatorOwnersPackage $package) { $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID()); $reasons = array(); if ($data->getCommitDetail('vsDiff')) { $reasons[] = 'Changed After Revision Was Accepted'; } $commit_author_phid = $data->getCommitDetail('authorPHID'); if (!$commit_author_phid) { $reasons[] = 'Commit Author Not Recognized'; } $revision_id = $data->getCommitDetail('differential.revisionID'); $revision_author_phid = null; $commit_reviewedby_phid = null; if ($revision_id) { // TODO: (T603) This is probably safe to use an omnipotent user on, // but check things more closely. $revision = id(new DifferentialRevision())->load($revision_id); if ($revision) { $revision_author_phid = $revision->getAuthorPHID(); $commit_reviewedby_phid = $data->getCommitDetail('reviewerPHID'); if ($revision_author_phid !== $commit_author_phid) { $reasons[] = 'Author Not Matching with Revision'; } } else { $reasons[] = 'Revision Not Found'; } } else { $reasons[] = 'No Revision Specified'; } $owners_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(array($package->getID())); if (!($commit_author_phid && in_array($commit_author_phid, $owners_phids) || $commit_reviewedby_phid && in_array($commit_reviewedby_phid, $owners_phids))) { $reasons[] = 'Owners Not Involved'; } return $reasons; }
private function insertTask(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, $data = array()) { $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $class = 'PhabricatorRepositoryGitCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $class = 'PhabricatorRepositorySvnCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker'; break; default: throw new Exception("Unknown repository type '{$vcs}'!"); } $task = new PhabricatorWorkerTask(); $task->setTaskClass($class); $data['commitID'] = $commit->getID(); $task->setData($data); $task->save(); }
private function insertTask(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, $data = array()) { $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $class = 'PhabricatorRepositoryGitCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $class = 'PhabricatorRepositorySvnCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker'; break; default: throw new Exception(pht("Unknown repository type '%s'!", $vcs)); } $data['commitID'] = $commit->getID(); PhabricatorWorker::scheduleTask($class, $data); }
private function lookupSvnCommits(PhabricatorRepository $repository, array $commits) { if (!$commits) { return array(); } $commit_table = new PhabricatorRepositoryCommit(); $commit_data = queryfx_all($commit_table->establishConnection('w'), 'SELECT id, commitIdentifier FROM %T WHERE repositoryID = %d AND commitIdentifier in (%Ls)', $commit_table->getTableName(), $repository->getID(), $commits); $commit_map = ipull($commit_data, 'id', 'commitIdentifier'); $need = array(); foreach ($commits as $commit) { if (empty($commit_map[$commit])) { $need[] = $commit; } } // If we are parsing a Subversion repository and have been configured to // import only some subdirectory of it, we may find commits which reference // other foreign commits outside of the directory (for instance, because of // a move or copy). Rather than trying to execute full parses on them, just // create stub commits and identify the stubs as foreign commits. if ($need) { $subpath = $repository->getDetail('svn-subpath'); if (!$subpath) { $commits = implode(', ', $need); throw new Exception("Missing commits ({$need}) in a SVN repository which is not " . "configured for subdirectory-only parsing!"); } foreach ($need as $foreign_commit) { $commit = new PhabricatorRepositoryCommit(); $commit->setRepositoryID($repository->getID()); $commit->setCommitIdentifier($foreign_commit); $commit->setEpoch(0); // Mark this commit as imported so it doesn't prevent the repository // from transitioning into the "Imported" state. $commit->setImportStatus(PhabricatorRepositoryCommit::IMPORTED_ALL); $commit->save(); $data = new PhabricatorRepositoryCommitData(); $data->setCommitID($commit->getID()); $data->setAuthorName(''); $data->setCommitMessage(''); $data->setCommitDetails(array('foreign-svn-stub' => true, 'svn-subpath' => $subpath)); $data->save(); $commit_map[$foreign_commit] = $commit->getID(); } } return $commit_map; }
public function setCommit(PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $repository = id(new PhabricatorRepositoryQuery())->setViewer($viewer)->withIDs(array($commit->getRepositoryID()))->needProjectPHIDs(true)->executeOne(); if (!$repository) { throw new Exception(pht('Unable to load repository!')); } $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID()); if (!$data) { throw new Exception(pht('Unable to load commit data!')); } $this->commit = clone $commit; $this->commit->attachRepository($repository); $this->commit->attachCommitData($data); $this->repository = $repository; $this->commitData = $data; return $this; }
private function insertTask(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, $data = array()) { $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $class = 'PhabricatorRepositoryGitCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $class = 'PhabricatorRepositorySvnCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker'; break; default: throw new Exception(pht("Unknown repository type '%s'!", $vcs)); } $data['commitID'] = $commit->getID(); // If the repository is importing for the first time, we schedule tasks // at IMPORT priority, which is very low. Making progress on importing a // new repository for the first time is less important than any other // daemon task. // If the repostitory has finished importing and we're just catching up // on recent commits, we schedule discovery at COMMIT priority, which is // slightly below the default priority. // Note that followup tasks and triggered tasks (like those generated by // Herald or Harbormaster) will queue at DEFAULT priority, so that each // commit tends to fully import before we start the next one. This tends // to give imports fairly predictable progress. See T11677 for some // discussion. if ($repository->isImporting()) { $task_priority = PhabricatorWorker::PRIORITY_IMPORT; } else { $task_priority = PhabricatorWorker::PRIORITY_COMMIT; } $options = array('priority' => $task_priority); PhabricatorWorker::scheduleTask($class, $data, $options); }
private function applyHeraldRules(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $commit->attachRepository($repository); // Don't take any actions on an importing repository. Principally, this // avoids generating thousands of audits or emails when you import an // established repository on an existing install. if ($repository->isImporting()) { return; } if ($repository->getDetail('herald-disabled')) { return; } $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID()); if (!$data) { throw new PhabricatorWorkerPermanentFailureException(pht('Unable to load commit data. The data for this task is invalid ' . 'or no longer exists.')); } $adapter = id(new HeraldCommitAdapter())->setCommit($commit); $rules = id(new HeraldRuleQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withContentTypes(array($adapter->getAdapterContentType()))->withDisabled(false)->needConditionsAndActions(true)->needAppliedToPHIDs(array($adapter->getPHID()))->needValidateAuthors(true)->execute(); $engine = new HeraldEngine(); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); $xscript = $engine->getTranscript(); $audit_phids = $adapter->getAuditMap(); $cc_phids = $adapter->getAddCCMap(); if ($audit_phids || $cc_phids) { $this->createAudits($commit, $audit_phids, $cc_phids, $rules); } HarbormasterBuildable::applyBuildPlans($commit->getPHID(), $repository->getPHID(), $adapter->getBuildPlans()); $explicit_auditors = $this->createAuditsFromCommitMessage($commit, $data); $this->publishFeedStory($repository, $commit, $data); $herald_targets = $adapter->getEmailPHIDs(); $email_phids = array_unique(array_merge($explicit_auditors, array_keys($cc_phids), $herald_targets)); if (!$email_phids) { return; } $revision = $adapter->loadDifferentialRevision(); if ($revision) { $name = $revision->getTitle(); } else { $name = $data->getSummary(); } $author_phid = $data->getCommitDetail('authorPHID'); $reviewer_phid = $data->getCommitDetail('reviewerPHID'); $phids = array_filter(array($author_phid, $reviewer_phid, $commit->getPHID())); $handles = id(new PhabricatorHandleQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withPHIDs($phids)->execute(); $commit_handle = $handles[$commit->getPHID()]; $commit_name = $commit_handle->getName(); if ($author_phid) { $author_name = $handles[$author_phid]->getName(); } else { $author_name = $data->getAuthorName(); } if ($reviewer_phid) { $reviewer_name = $handles[$reviewer_phid]->getName(); } else { $reviewer_name = null; } $who = implode(', ', array_filter(array($author_name, $reviewer_name))); $description = $data->getCommitMessage(); $commit_uri = PhabricatorEnv::getProductionURI($commit_handle->getURI()); $differential = $revision ? PhabricatorEnv::getProductionURI('/D' . $revision->getID()) : 'No revision.'; $limit = self::MAX_FILES_SHOWN_IN_EMAIL; $files = $adapter->loadAffectedPaths(); sort($files); if (count($files) > $limit) { array_splice($files, $limit); $files[] = '(This commit affected more than ' . $limit . ' files. ' . 'Only ' . $limit . ' are shown here and additional ones are truncated.)'; } $files = implode("\n", $files); $xscript_id = $xscript->getID(); $why_uri = '/herald/transcript/' . $xscript_id . '/'; $reply_handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit($commit); $template = new PhabricatorMetaMTAMail(); $inline_patch_text = $this->buildPatch($template, $repository, $commit); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection($description); $body->addTextSection(pht('DETAILS'), $commit_uri); // TODO: This should be integrated properly once we move to // ApplicationTransactions. $field_list = PhabricatorCustomField::getObjectFields($commit, PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS); $field_list->setViewer(PhabricatorUser::getOmnipotentUser())->readFieldsFromStorage($commit); foreach ($field_list->getFields() as $field) { try { $field->buildApplicationTransactionMailBody(new DifferentialTransaction(), $body); } catch (Exception $ex) { // Log the exception and continue. phlog($ex); } } $body->addTextSection(pht('DIFFERENTIAL REVISION'), $differential); $body->addTextSection(pht('AFFECTED FILES'), $files); $body->addReplySection($reply_handler->getReplyHandlerInstructions()); $body->addHeraldSection($why_uri); $body->addRawSection($inline_patch_text); $body = $body->render(); $prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); $threading = PhabricatorAuditCommentEditor::getMailThreading($repository, $commit); list($thread_id, $thread_topic) = $threading; $template->setRelatedPHID($commit->getPHID()); $template->setSubject("{$commit_name}: {$name}"); $template->setSubjectPrefix($prefix); $template->setVarySubjectPrefix('[Commit]'); $template->setBody($body); $template->setThreadID($thread_id, $is_new = true); $template->addHeader('Thread-Topic', $thread_topic); $template->setIsBulk(true); $template->addHeader('X-Herald-Rules', $xscript->getXHeraldRulesHeader()); if ($author_phid) { $template->setFrom($author_phid); } // TODO: We should verify that each recipient can actually see the // commit before sending them email (T603). $mails = $reply_handler->multiplexMail($template, id(new PhabricatorHandleQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withPHIDs($email_phids)->execute(), array()); foreach ($mails as $mail) { $mail->saveAndSend(); } }
private function renderAddCommentPanel(PhabricatorRepositoryCommit $commit, array $audit_requests, $pane_id = null) { assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); $user = $this->getRequest()->getUser(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); Javelin::initBehavior('differential-keyboard-navigation', array('haunt' => $pane_id)); $draft = id(new PhabricatorDraft())->loadOneWhere('authorPHID = %s AND draftKey = %s', $user->getPHID(), 'diffusion-audit-' . $commit->getID()); if ($draft) { $draft = $draft->getDraft(); } else { $draft = null; } $actions = $this->getAuditActions($commit, $audit_requests); $form = id(new AphrontFormView())->setUser($user)->setAction('/audit/addcomment/')->addHiddenInput('commit', $commit->getPHID())->appendChild(id(new AphrontFormSelectControl())->setLabel('Action')->setName('action')->setID('audit-action')->setOptions($actions))->appendChild(id(new AphrontFormTokenizerControl())->setLabel('Add Auditors')->setName('auditors')->setControlID('add-auditors')->setControlStyle('display: none')->setID('add-auditors-tokenizer')->setDisableBehavior(true))->appendChild(id(new AphrontFormTokenizerControl())->setLabel('Add CCs')->setName('ccs')->setControlID('add-ccs')->setControlStyle('display: none')->setID('add-ccs-tokenizer')->setDisableBehavior(true))->appendChild(id(new AphrontFormTextAreaControl())->setLabel('Comments')->setName('content')->setValue($draft)->setID('audit-content')->setCaption(phutil_render_tag('a', array('href' => PhabricatorEnv::getDoclink('article/Remarkup_Reference.html'), 'tabindex' => '-1', 'target' => '_blank'), 'Formatting Reference')))->appendChild(id(new AphrontFormSubmitControl())->setValue($is_serious ? 'Submit' : 'Cook the Books')); $panel = new AphrontPanelView(); $panel->setHeader($is_serious ? 'Audit Commit' : 'Creative Accounting'); $panel->appendChild($form); $panel->addClass('aphront-panel-accent'); $panel->addClass('aphront-panel-flush'); require_celerity_resource('phabricator-transaction-view-css'); Javelin::initBehavior('differential-add-reviewers-and-ccs', array('dynamic' => array('add-auditors-tokenizer' => array('actions' => array('add_auditors' => 1), 'src' => '/typeahead/common/users/', 'row' => 'add-auditors', 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'placeholder' => 'Type a user name...'), 'add-ccs-tokenizer' => array('actions' => array('add_ccs' => 1), 'src' => '/typeahead/common/mailable/', 'row' => 'add-ccs', 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'placeholder' => 'Type a user or mailing list...')), 'select' => 'audit-action')); Javelin::initBehavior('differential-feedback-preview', array('uri' => '/audit/preview/' . $commit->getID() . '/', 'preview' => 'audit-preview', 'content' => 'audit-content', 'action' => 'audit-action', 'previewTokenizers' => array('auditors' => 'add-auditors-tokenizer', 'ccs' => 'add-ccs-tokenizer'), 'inline' => 'inline-comment-preview', 'inlineuri' => '/diffusion/inline/preview/' . $commit->getPHID() . '/')); $preview_panel = '<div class="aphront-panel-preview aphront-panel-flush"> <div id="audit-preview"> <div class="aphront-panel-preview-loading-text"> Loading preview... </div> </div> <div id="inline-comment-preview"> </div> </div>'; return phutil_render_tag('div', array('class' => 'differential-add-comment-panel'), $panel->render() . $preview_panel); }
private function writeChanges(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, array $effects, array $path_map, array $commit_map) { $conn_w = $repository->establishConnection('w'); $sql = array(); foreach ($effects as $effect) { $sql[] = qsprintf($conn_w, '(%d, %d, %d, %nd, %nd, %d, %d, %d, %d)', $repository->getID(), $path_map[$effect['rawPath']], $commit->getID(), $effect['rawTargetPath'] ? $path_map[$effect['rawTargetPath']] : null, $effect['rawTargetCommit'] ? $commit_map[$effect['rawTargetCommit']] : null, $effect['changeType'], $effect['fileType'], $effect['rawDirect'] ? 1 : 0, $commit->getCommitIdentifier()); } queryfx($conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit->getID()); foreach (array_chunk($sql, 512) as $sql_chunk) { queryfx($conn_w, 'INSERT INTO %T (repositoryID, pathID, commitID, targetPathID, targetCommitID, changeType, fileType, isDirect, commitSequence) VALUES %Q', PhabricatorRepository::TABLE_PATHCHANGE, implode(', ', $sql_chunk)); } }
protected function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $full_name = 'r' . $repository->getCallsign() . $commit->getCommitIdentifier(); echo "Parsing {$full_name}...\n"; if ($this->isBadCommit($full_name)) { echo "This commit is marked bad!\n"; return; } $local_path = $repository->getDetail('local-path'); list($raw) = execx('(cd %s && git log -n1 -M -C -B --find-copies-harder --raw -t ' . '--abbrev=40 --pretty=format: %s)', $local_path, $commit->getCommitIdentifier()); $changes = array(); $move_away = array(); $copy_away = array(); $lines = explode("\n", $raw); foreach ($lines as $line) { if (!strlen(trim($line))) { continue; } list($old_mode, $new_mode, $old_hash, $new_hash, $more_stuff) = preg_split('/ +/', $line); // We may only have two pieces here. list($action, $src_path, $dst_path) = array_merge(explode("\t", $more_stuff), array(null)); // Normalize the paths for consistency with the SVN workflow. $src_path = '/' . $src_path; if ($dst_path) { $dst_path = '/' . $dst_path; } $old_mode = intval($old_mode, 8); $new_mode = intval($new_mode, 8); $file_type = DifferentialChangeType::FILE_NORMAL; if ($new_mode & 040000) { $file_type = DifferentialChangeType::FILE_DIRECTORY; } else { if ($new_mode & 0120000) { $file_type = DifferentialChangeType::FILE_SYMLINK; } } // TODO: We can detect binary changes as git does, through a combination // of running 'git check-attr' for stuff like 'binary', 'merge' or 'diff', // and by falling back to inspecting the first 8,000 characters of the // buffer for null bytes (this is seriously git's algorithm, see // buffer_is_binary() in xdiff-interface.c). $change_type = null; $change_path = $src_path; $change_target = null; $is_direct = true; switch ($action[0]) { case 'A': $change_type = DifferentialChangeType::TYPE_ADD; break; case 'D': $change_type = DifferentialChangeType::TYPE_DELETE; break; case 'C': $change_type = DifferentialChangeType::TYPE_COPY_HERE; $change_path = $dst_path; $change_target = $src_path; $copy_away[$change_target][] = $change_path; break; case 'R': $change_type = DifferentialChangeType::TYPE_MOVE_HERE; $change_path = $dst_path; $change_target = $src_path; $move_away[$change_target][] = $change_path; break; case 'T': // Type of the file changed, fall through and treat it as a // modification. Not 100% sure this is the right thing to do but it // seems reasonable. // Type of the file changed, fall through and treat it as a // modification. Not 100% sure this is the right thing to do but it // seems reasonable. case 'M': if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { $change_type = DifferentialChangeType::TYPE_CHILD; $is_direct = false; } else { $change_type = DifferentialChangeType::TYPE_CHANGE; } break; // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible // in theory but shouldn't appear here. // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible // in theory but shouldn't appear here. default: throw new Exception("Failed to parse line '{$line}'."); } $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $file_type, 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => $change_target, 'targetCommitID' => $change_target ? $commit->getID() : null); } // Add a change to '/' since git doesn't mention it. $changes['/'] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => '/', 'changeType' => DifferentialChangeType::TYPE_CHILD, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, 'isDirect' => false, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null); foreach ($copy_away as $change_path => $destinations) { if (isset($move_away[$change_path])) { $change_type = DifferentialChangeType::TYPE_MULTICOPY; $is_direct = true; unset($move_away[$change_path]); } else { $change_type = DifferentialChangeType::TYPE_COPY_AWAY; $is_direct = false; } $reference = $changes[reset($destinations)]; $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $reference['fileType'], 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null); } foreach ($move_away as $change_path => $destinations) { $reference = $changes[reset($destinations)]; $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => DifferentialChangeType::TYPE_MOVE_AWAY, 'fileType' => $reference['fileType'], 'isDirect' => true, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null); } $paths = array(); foreach ($changes as $change) { $paths[$change['path']] = true; if ($change['targetPath']) { $paths[$change['targetPath']] = true; } } $path_map = $this->lookupOrCreatePaths(array_keys($paths)); foreach ($changes as $key => $change) { $changes[$key]['pathID'] = $path_map[$change['path']]; if ($change['targetPath']) { $changes[$key]['targetPathID'] = $path_map[$change['targetPath']]; } else { $changes[$key]['targetPathID'] = null; } } $conn_w = $repository->establishConnection('w'); $changes_sql = array(); foreach ($changes as $change) { $values = array((int) $change['repositoryID'], (int) $change['pathID'], (int) $change['commitID'], $change['targetPathID'] ? (int) $change['targetPathID'] : 'null', $change['targetCommitID'] ? (int) $change['targetCommitID'] : 'null', (int) $change['changeType'], (int) $change['fileType'], (int) $change['isDirect'], (int) $change['commitSequence']); $changes_sql[] = '(' . implode(', ', $values) . ')'; } queryfx($conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit->getID()); foreach (array_chunk($changes_sql, 256) as $sql_chunk) { queryfx($conn_w, 'INSERT INTO %T (repositoryID, pathID, commitID, targetPathID, targetCommitID, changeType, fileType, isDirect, commitSequence) VALUES %Q', PhabricatorRepository::TABLE_PATHCHANGE, implode(', ', $sql_chunk)); } $this->finishParse(); }
protected function parseCommitChanges(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $raw = DiffusionQuery::callConduitWithDiffusionRequest($viewer, DiffusionRequest::newFromDictionary(array('repository' => $repository, 'user' => $viewer)), 'diffusion.internal.gitrawdiffquery', array('commit' => $commit->getCommitIdentifier())); $changes = array(); $move_away = array(); $copy_away = array(); $lines = explode("\n", $raw); foreach ($lines as $line) { if (!strlen(trim($line))) { continue; } list($old_mode, $new_mode, $old_hash, $new_hash, $more_stuff) = preg_split('/ +/', $line, 5); // We may only have two pieces here. list($action, $src_path, $dst_path) = array_merge(explode("\t", $more_stuff), array(null)); // Normalize the paths for consistency with the SVN workflow. $src_path = '/' . $src_path; if ($dst_path) { $dst_path = '/' . $dst_path; } $old_mode = intval($old_mode, 8); $new_mode = intval($new_mode, 8); switch ($new_mode & 0160000) { case 0160000: $file_type = DifferentialChangeType::FILE_SUBMODULE; break; case 0120000: $file_type = DifferentialChangeType::FILE_SYMLINK; break; case 040000: $file_type = DifferentialChangeType::FILE_DIRECTORY; break; default: $file_type = DifferentialChangeType::FILE_NORMAL; break; } // TODO: We can detect binary changes as git does, through a combination // of running 'git check-attr' for stuff like 'binary', 'merge' or 'diff', // and by falling back to inspecting the first 8,000 characters of the // buffer for null bytes (this is seriously git's algorithm, see // buffer_is_binary() in xdiff-interface.c). $change_type = null; $change_path = $src_path; $change_target = null; $is_direct = true; switch ($action[0]) { case 'A': $change_type = DifferentialChangeType::TYPE_ADD; break; case 'D': $change_type = DifferentialChangeType::TYPE_DELETE; break; case 'C': $change_type = DifferentialChangeType::TYPE_COPY_HERE; $change_path = $dst_path; $change_target = $src_path; $copy_away[$change_target][] = $change_path; break; case 'R': $change_type = DifferentialChangeType::TYPE_MOVE_HERE; $change_path = $dst_path; $change_target = $src_path; $move_away[$change_target][] = $change_path; break; case 'T': // Type of the file changed, fall through and treat it as a // modification. Not 100% sure this is the right thing to do but it // seems reasonable. // Type of the file changed, fall through and treat it as a // modification. Not 100% sure this is the right thing to do but it // seems reasonable. case 'M': if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { $change_type = DifferentialChangeType::TYPE_CHILD; $is_direct = false; } else { $change_type = DifferentialChangeType::TYPE_CHANGE; } break; // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible // in theory but shouldn't appear here. // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible // in theory but shouldn't appear here. default: throw new Exception(pht("Failed to parse line '%s'.", $line)); } $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $file_type, 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => $change_target, 'targetCommitID' => $change_target ? $commit->getID() : null); } // Add a change to '/' since git doesn't mention it. $changes['/'] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => '/', 'changeType' => DifferentialChangeType::TYPE_CHILD, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, 'isDirect' => false, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null); foreach ($copy_away as $change_path => $destinations) { if (isset($move_away[$change_path])) { $change_type = DifferentialChangeType::TYPE_MULTICOPY; $is_direct = true; unset($move_away[$change_path]); } else { $change_type = DifferentialChangeType::TYPE_COPY_AWAY; // This change is direct if we picked up a modification above (i.e., // the original copy source was also edited). Otherwise the original // wasn't touched, so leave it as an indirect change. $is_direct = isset($changes[$change_path]); } $reference = $changes[reset($destinations)]; $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $reference['fileType'], 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null); } foreach ($move_away as $change_path => $destinations) { $reference = $changes[reset($destinations)]; $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => DifferentialChangeType::TYPE_MOVE_AWAY, 'fileType' => $reference['fileType'], 'isDirect' => true, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null); } $paths = array(); foreach ($changes as $change) { $paths[$change['path']] = true; if ($change['targetPath']) { $paths[$change['targetPath']] = true; } } $path_map = $this->lookupOrCreatePaths(array_keys($paths)); foreach ($changes as $key => $change) { $changes[$key]['pathID'] = $path_map[$change['path']]; if ($change['targetPath']) { $changes[$key]['targetPathID'] = $path_map[$change['targetPath']]; } else { $changes[$key]['targetPathID'] = null; } } $results = array(); foreach ($changes as $change) { $result = id(new PhabricatorRepositoryParsedChange())->setPathID($change['pathID'])->setTargetPathID($change['targetPathID'])->setTargetCommitID($change['targetCommitID'])->setChangeType($change['changeType'])->setFileType($change['fileType'])->setIsDirect($change['isDirect'])->setCommitSequence($change['commitSequence']); $results[] = $result; } return $results; }
private function renderAddCommentPanel(PhabricatorRepositoryCommit $commit, array $audit_requests) { assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); $request = $this->getRequest(); $user = $request->getUser(); if (!$user->isLoggedIn()) { return id(new PhabricatorApplicationTransactionCommentView())->setUser($user)->setRequestURI($request->getRequestURI()); } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $pane_id = celerity_generate_unique_node_id(); Javelin::initBehavior('differential-keyboard-navigation', array('haunt' => $pane_id)); $draft = id(new PhabricatorDraft())->loadOneWhere('authorPHID = %s AND draftKey = %s', $user->getPHID(), 'diffusion-audit-' . $commit->getID()); if ($draft) { $draft = $draft->getDraft(); } else { $draft = null; } $actions = $this->getAuditActions($commit, $audit_requests); $mailable_source = new PhabricatorMetaMTAMailableDatasource(); $auditor_source = new DiffusionAuditorDatasource(); $form = id(new AphrontFormView())->setUser($user)->setAction('/audit/addcomment/')->addHiddenInput('commit', $commit->getPHID())->appendChild(id(new AphrontFormSelectControl())->setLabel(pht('Action'))->setName('action')->setID('audit-action')->setOptions($actions))->appendControl(id(new AphrontFormTokenizerControl())->setLabel(pht('Add Auditors'))->setName('auditors')->setControlID('add-auditors')->setControlStyle('display: none')->setID('add-auditors-tokenizer')->setDisableBehavior(true)->setDatasource($auditor_source))->appendControl(id(new AphrontFormTokenizerControl())->setLabel(pht('Add CCs'))->setName('ccs')->setControlID('add-ccs')->setControlStyle('display: none')->setID('add-ccs-tokenizer')->setDisableBehavior(true)->setDatasource($mailable_source))->appendChild(id(new PhabricatorRemarkupControl())->setLabel(pht('Comments'))->setName('content')->setValue($draft)->setID('audit-content')->setUser($user))->appendChild(id(new AphrontFormSubmitControl())->setValue(pht('Submit'))); $header = new PHUIHeaderView(); $header->setHeader($is_serious ? pht('Audit Commit') : pht('Creative Accounting')); Javelin::initBehavior('differential-add-reviewers-and-ccs', array('dynamic' => array('add-auditors-tokenizer' => array('actions' => array('add_auditors' => 1), 'src' => $auditor_source->getDatasourceURI(), 'row' => 'add-auditors', 'placeholder' => $auditor_source->getPlaceholderText()), 'add-ccs-tokenizer' => array('actions' => array('add_ccs' => 1), 'src' => $mailable_source->getDatasourceURI(), 'row' => 'add-ccs', 'placeholder' => $mailable_source->getPlaceholderText())), 'select' => 'audit-action')); Javelin::initBehavior('differential-feedback-preview', array('uri' => '/audit/preview/' . $commit->getID() . '/', 'preview' => 'audit-preview', 'content' => 'audit-content', 'action' => 'audit-action', 'previewTokenizers' => array('auditors' => 'add-auditors-tokenizer', 'ccs' => 'add-ccs-tokenizer'), 'inline' => 'inline-comment-preview', 'inlineuri' => '/diffusion/inline/preview/' . $commit->getPHID() . '/')); $loading = phutil_tag_div('aphront-panel-preview-loading-text', pht('Loading preview...')); $preview_panel = phutil_tag_div('aphront-panel-preview aphront-panel-flush', array(phutil_tag('div', array('id' => 'audit-preview'), $loading), phutil_tag('div', array('id' => 'inline-comment-preview')))); // TODO: This is pretty awkward, unify the CSS between Diffusion and // Differential better. require_celerity_resource('differential-core-view-css'); $anchor = id(new PhabricatorAnchorView())->setAnchorName('comment')->setNavigationMarker(true)->render(); $comment_box = id(new PHUIObjectBoxView())->setHeader($header)->appendChild($form); return phutil_tag('div', array('id' => $pane_id), phutil_tag_div('differential-add-comment-panel', array($anchor, $comment_box, $preview_panel))); }
private function buildAddCommentView(PhabricatorRepositoryCommit $commit, array $audit_requests) { assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); $user = $this->getRequest()->getUser(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); Javelin::initBehavior('differential-keyboard-navigation', array('haunt' => null)); $draft = id(new PhabricatorDraft())->loadOneWhere('authorPHID = %s AND draftKey = %s', $user->getPHID(), 'diffusion-audit-' . $commit->getID()); if ($draft) { $draft = $draft->getDraft(); } else { $draft = null; } $actions = $this->getAuditActions($commit, $audit_requests); $form = id(new AphrontFormView())->setUser($user)->setAction('/audit/addcomment/')->addHiddenInput('commit', $commit->getPHID())->appendChild(id(new AphrontFormSelectControl())->setLabel('Action')->setName('action')->setID('audit-action')->setOptions($actions))->appendChild(id(new AphrontFormTextAreaControl())->setLabel('Comments')->setName('content')->setValue($draft)->setID('audit-content')->setCaption(phutil_render_tag('a', array('href' => PhabricatorEnv::getDoclink('article/Remarkup_Reference.html'), 'tabindex' => '-1', 'target' => '_blank'), 'Formatting Reference')))->appendChild(id(new AphrontFormSubmitControl())->setValue($is_serious ? 'Submit' : 'Cook the Books')); $panel = new AphrontPanelView(); $panel->setHeader($is_serious ? 'Audit Commit' : 'Creative Accounting'); $panel->appendChild($form); require_celerity_resource('phabricator-transaction-view-css'); Javelin::initBehavior('audit-preview', array('uri' => '/audit/preview/' . $commit->getID() . '/', 'preview' => 'audit-preview', 'content' => 'audit-content', 'action' => 'audit-action')); $preview_panel = '<div class="aphront-panel-preview"> <div id="audit-preview"> <div class="aphront-panel-preview-loading-text"> Loading preview... </div> </div> </div>'; $view = new AphrontNullView(); $view->appendChild($panel); $view->appendChild($preview_panel); return $view; }
protected function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $full_name = 'r' . $repository->getCallsign() . $commit->getCommitIdentifier(); echo "Parsing {$full_name}...\n"; if ($this->isBadCommit($full_name)) { echo "This commit is marked bad!\n"; return; } list($stdout) = $repository->execxLocalCommand('status -C --change %s', $commit->getCommitIdentifier()); $status = ArcanistMercurialParser::parseMercurialStatusDetails($stdout); $common_attributes = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'commitSequence' => $commit->getEpoch()); $changes = array(); // Like Git, Mercurial doesn't track directories directly. We need to infer // directory creation and removal by observing file creation and removal // and testing if the directories in question are previously empty (thus, // created) or subsequently empty (thus, removed). $maybe_new_directories = array(); $maybe_del_directories = array(); $all_directories = array(); // Parse the basic information from "hg status", which shows files that // were directly affected by the change. foreach ($status as $path => $path_info) { $path = '/' . $path; $flags = $path_info['flags']; $change_target = $path_info['from'] ? '/' . $path_info['from'] : null; $changes[$path] = array('path' => $path, 'isDirect' => true, 'targetPath' => $change_target, 'targetCommitID' => $change_target ? $commit->getID() : null, 'changeType' => null, 'fileType' => null, 'flags' => $flags) + $common_attributes; if ($flags & ArcanistRepositoryAPI::FLAG_ADDED) { $maybe_new_directories[] = dirname($path); } else { if ($flags & ArcanistRepositoryAPI::FLAG_DELETED) { $maybe_del_directories[] = dirname($path); } } $all_directories[] = dirname($path); } // Add change information for each source path which doesn't appear in the // status. These files were copied, but were not modified. We also know they // must exist. foreach ($changes as $path => $change) { $from = $change['targetPath']; if ($from && empty($changes[$from])) { $changes[$from] = array('path' => $from, 'isDirect' => false, 'targetPath' => null, 'targetCommitID' => null, 'changeType' => DifferentialChangeType::TYPE_COPY_AWAY, 'fileType' => null, 'flags' => 0) + $common_attributes; } } $away = array(); foreach ($changes as $path => $change) { $target_path = $change['targetPath']; if ($target_path) { $away[$target_path][] = $path; } } // Now that we have all the direct changes, figure out change types. foreach ($changes as $path => $change) { $flags = $change['flags']; $from = $change['targetPath']; if ($from) { $target = $changes[$from]; } else { $target = null; } if ($flags & ArcanistRepositoryAPI::FLAG_ADDED) { if ($target) { if ($target['flags'] & ArcanistRepositoryAPI::FLAG_DELETED) { $change_type = DifferentialChangeType::TYPE_MOVE_HERE; } else { $change_type = DifferentialChangeType::TYPE_COPY_HERE; } } else { $change_type = DifferentialChangeType::TYPE_ADD; } } else { if ($flags & ArcanistRepositoryAPI::FLAG_DELETED) { if (isset($away[$path])) { if (count($away[$path]) > 1) { $change_type = DifferentialChangeType::TYPE_MULTICOPY; } else { $change_type = DifferentialChangeType::TYPE_MOVE_AWAY; } } else { $change_type = DifferentialChangeType::TYPE_DELETE; } } else { if (isset($away[$path])) { $change_type = DifferentialChangeType::TYPE_COPY_AWAY; } else { $change_type = DifferentialChangeType::TYPE_CHANGE; } } } $changes[$path]['changeType'] = $change_type; } // Go through all the affected directories and identify any which were // actually added or deleted. $dir_status = array(); foreach ($maybe_del_directories as $dir) { $exists = false; foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) { if (isset($dir_status[$path])) { break; } // If we know some child exists, we know this path exists. If we don't // know that a child exists, test if this directory still exists. if (!$exists) { $exists = $this->mercurialPathExists($repository, $path, $commit->getCommitIdentifier()); } if ($exists) { $dir_status[$path] = DifferentialChangeType::TYPE_CHILD; } else { $dir_status[$path] = DifferentialChangeType::TYPE_DELETE; } } } list($stdout) = $repository->execxLocalCommand('parents --rev %s --style default', $commit->getCommitIdentifier()); $parents = ArcanistMercurialParser::parseMercurialLog($stdout); $parent = reset($parents); if ($parent) { // TODO: We should expand this to a full 40-character hash using "hg id". $parent = $parent['rev']; } foreach ($maybe_new_directories as $dir) { $exists = false; foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) { if (isset($dir_status[$path])) { break; } if (!$exists) { if ($parent) { $exists = $this->mercurialPathExists($repository, $path, $parent); } else { $exists = false; } } if ($exists) { $dir_status[$path] = DifferentialChangeType::TYPE_CHILD; } else { $dir_status[$path] = DifferentialChangeType::TYPE_ADD; } } } foreach ($all_directories as $dir) { foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) { if (isset($dir_status[$path])) { break; } $dir_status[$path] = DifferentialChangeType::TYPE_CHILD; } } // Merge all the directory statuses into the path statuses. foreach ($dir_status as $path => $status) { if (isset($changes[$path])) { // TODO: The UI probably doesn't handle any of these cases with // terrible elegance, but they are exceedingly rare. $existing_type = $changes[$path]['changeType']; if ($existing_type == DifferentialChangeType::TYPE_DELETE) { // This change removes a file, replaces it with a directory, and then // adds children of that directory. Mark it as a "change" instead, // and make the type a directory. $changes[$path]['fileType'] = DifferentialChangeType::FILE_DIRECTORY; $changes[$path]['changeType'] = DifferentialChangeType::TYPE_CHANGE; } else { if ($existing_type == DifferentialChangeType::TYPE_MOVE_AWAY || $existing_type == DifferentialChangeType::TYPE_MULTICOPY) { // This change moves or copies a file, replaces it with a directory, // and then adds children to that directory. Mark it as "copy away" // instead of whatever it was, and make the type a directory. $changes[$path]['fileType'] = DifferentialChangeType::FILE_DIRECTORY; $changes[$path]['changeType'] = DifferentialChangeType::TYPE_COPY_AWAY; } else { if ($existing_type == DifferentialChangeType::TYPE_ADD) { // This change removes a diretory and replaces it with a file. Mark // it as "change" instead of "add". $changes[$path]['changeType'] = DifferentialChangeType::TYPE_CHANGE; } } } continue; } $changes[$path] = array('path' => $path, 'isDirect' => $status == DifferentialChangeType::TYPE_CHILD ? false : true, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, 'changeType' => $status, 'targetPath' => null, 'targetCommitID' => null) + $common_attributes; } // TODO: use "hg diff --git" to figure out which files are symlinks. foreach ($changes as $path => $change) { if (empty($change['fileType'])) { $changes[$path]['fileType'] = DifferentialChangeType::FILE_NORMAL; } } $all_paths = array(); foreach ($changes as $path => $change) { $all_paths[$path] = true; if ($change['targetPath']) { $all_paths[$change['targetPath']] = true; } } $path_map = $this->lookupOrCreatePaths(array_keys($all_paths)); foreach ($changes as $key => $change) { $changes[$key]['pathID'] = $path_map[$change['path']]; if ($change['targetPath']) { $changes[$key]['targetPathID'] = $path_map[$change['targetPath']]; } else { $changes[$key]['targetPathID'] = null; } } $conn_w = $repository->establishConnection('w'); $changes_sql = array(); foreach ($changes as $change) { $values = array((int) $change['repositoryID'], (int) $change['pathID'], (int) $change['commitID'], $change['targetPathID'] ? (int) $change['targetPathID'] : 'null', $change['targetCommitID'] ? (int) $change['targetCommitID'] : 'null', (int) $change['changeType'], (int) $change['fileType'], (int) $change['isDirect'], (int) $change['commitSequence']); $changes_sql[] = '(' . implode(', ', $values) . ')'; } queryfx($conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit->getID()); foreach (array_chunk($changes_sql, 256) as $sql_chunk) { queryfx($conn_w, 'INSERT INTO %T (repositoryID, pathID, commitID, targetPathID, targetCommitID, changeType, fileType, isDirect, commitSequence) VALUES %Q', PhabricatorRepository::TABLE_PATHCHANGE, implode(', ', $sql_chunk)); } $this->finishParse(); }
public function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { if ($repository->getDetail('herald-disabled')) { return; } $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID()); $rules = HeraldRule::loadAllByContentTypeWithFullData(HeraldContentTypeConfig::CONTENT_TYPE_COMMIT); $adapter = new HeraldCommitAdapter($repository, $commit, $data); $engine = new HeraldEngine(); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter); $email_phids = $adapter->getEmailPHIDs(); if (!$email_phids) { return; } $xscript = $engine->getTranscript(); $commit_name = $adapter->getHeraldName(); $revision = $adapter->loadDifferentialRevision(); $name = null; if ($revision) { $name = ' ' . $revision->getTitle(); } $author_phid = $data->getCommitDetail('authorPHID'); $reviewer_phid = $data->getCommitDetail('reviewerPHID'); $phids = array_filter(array($author_phid, $reviewer_phid)); $handles = array(); if ($phids) { $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); } if ($author_phid) { $author_name = $handles[$author_phid]->getName(); } else { $author_name = $data->getAuthorName(); } if ($reviewer_phid) { $reviewer_name = $handles[$reviewer_phid]->getName(); } else { $reviewer_name = null; } $who = implode(', ', array_filter(array($author_name, $reviewer_name))); $description = $data->getCommitMessage(); $details = PhabricatorEnv::getProductionURI('/' . $commit_name); $differential = $revision ? PhabricatorEnv::getProductionURI('/D' . $revision->getID()) : 'No revision.'; $files = $adapter->loadAffectedPaths(); sort($files); $files = implode("\n ", $files); $xscript_id = $xscript->getID(); $manage_uri = PhabricatorEnv::getProductionURI('/herald/view/commits/'); $why_uri = PhabricatorEnv::getProductionURI('/herald/transcript/' . $xscript_id . '/'); $body = <<<EOBODY DESCRIPTION {$description} DETAILS {$details} DIFFERENTIAL REVISION {$differential} AFFECTED FILES {$files} MANAGE HERALD COMMIT RULES {$manage_uri} WHY DID I GET THIS EMAIL? {$why_uri} EOBODY; $subject = "[Herald/Commit] {$commit_name} ({$who}){$name}"; $mailer = new PhabricatorMetaMTAMail(); $mailer->setRelatedPHID($commit->getPHID()); $mailer->addTos($email_phids); $mailer->setSubject($subject); $mailer->setBody($body); $mailer->addHeader('X-Herald-Rules', $xscript->getXHeraldRulesHeader()); if ($author_phid) { $mailer->setFrom($author_phid); } $mailer->saveAndSend(); }
public function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID()); if (!$data) { // TODO: Permanent failure. return; } $rules = HeraldRule::loadAllByContentTypeWithFullData(HeraldContentTypeConfig::CONTENT_TYPE_COMMIT, $commit->getPHID()); $adapter = new HeraldCommitAdapter($repository, $commit, $data); $engine = new HeraldEngine(); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); $audit_phids = $adapter->getAuditMap(); if ($audit_phids) { $this->createAudits($commit, $audit_phids, $rules); } $explicit_auditors = $this->createAuditsFromCommitMessage($commit, $data); if ($repository->getDetail('herald-disabled')) { // This just means "disable email"; audits are (mostly) idempotent. return; } $this->publishFeedStory($repository, $commit, $data); $herald_targets = $adapter->getEmailPHIDs(); $email_phids = array_unique(array_merge($explicit_auditors, $herald_targets)); if (!$email_phids) { return; } $xscript = $engine->getTranscript(); $revision = $adapter->loadDifferentialRevision(); if ($revision) { $name = $revision->getTitle(); } else { $name = $data->getSummary(); } $author_phid = $data->getCommitDetail('authorPHID'); $reviewer_phid = $data->getCommitDetail('reviewerPHID'); $phids = array_filter(array($author_phid, $reviewer_phid, $commit->getPHID())); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $commit_handle = $handles[$commit->getPHID()]; $commit_name = $commit_handle->getName(); if ($author_phid) { $author_name = $handles[$author_phid]->getName(); } else { $author_name = $data->getAuthorName(); } if ($reviewer_phid) { $reviewer_name = $handles[$reviewer_phid]->getName(); } else { $reviewer_name = null; } $who = implode(', ', array_filter(array($author_name, $reviewer_name))); $description = $data->getCommitMessage(); $commit_uri = PhabricatorEnv::getProductionURI($commit_handle->getURI()); $differential = $revision ? PhabricatorEnv::getProductionURI('/D' . $revision->getID()) : 'No revision.'; $files = $adapter->loadAffectedPaths(); sort($files); $files = implode("\n", $files); $xscript_id = $xscript->getID(); $manage_uri = '/herald/view/commits/'; $why_uri = '/herald/transcript/' . $xscript_id . '/'; $reply_handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit($commit); $template = new PhabricatorMetaMTAMail(); $inline_patch_text = $this->buildPatch($template, $repository, $commit); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection($description); $body->addTextSection(pht('DETAILS'), $commit_uri); $body->addTextSection(pht('DIFFERENTIAL REVISION'), $differential); $body->addTextSection(pht('AFFECTED FILES'), $files); $body->addReplySection($reply_handler->getReplyHandlerInstructions()); $body->addHeraldSection($manage_uri, $why_uri); $body->addRawSection($inline_patch_text); $body = $body->render(); $prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); $threading = PhabricatorAuditCommentEditor::getMailThreading($repository, $commit); list($thread_id, $thread_topic) = $threading; $template->setRelatedPHID($commit->getPHID()); $template->setSubject("{$commit_name}: {$name}"); $template->setSubjectPrefix($prefix); $template->setVarySubjectPrefix("[Commit]"); $template->setBody($body); $template->setThreadID($thread_id, $is_new = true); $template->addHeader('Thread-Topic', $thread_topic); $template->setIsBulk(true); $template->addHeader('X-Herald-Rules', $xscript->getXHeraldRulesHeader()); if ($author_phid) { $template->setFrom($author_phid); } $mails = $reply_handler->multiplexMail($template, id(new PhabricatorObjectHandleData($email_phids))->loadHandles(), array()); foreach ($mails as $mail) { $mail->saveAndSend(); } }
protected function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $full_name = 'r' . $repository->getCallsign() . $commit->getCommitIdentifier(); echo "Parsing {$full_name}...\n"; if ($this->isBadCommit($full_name)) { echo "This commit is marked bad!\n"; return; } // Check if the commit has parents. We're testing to see whether it is the // first commit in history (in which case we must use "git log") or some // other commit (in which case we can use "git diff"). We'd rather use // "git diff" because it has the right behavior for merge commits, but // it requires the commit to have a parent that we can diff against. The // first commit doesn't, so "commit^" is not a valid ref. list($parents) = $repository->execxLocalCommand('log -n1 --format=%s %s', '%P', $commit->getCommitIdentifier()); $use_log = !strlen(trim($parents)); if ($use_log) { // This is the first commit so we need to use "log". We know it's not a // merge commit because it couldn't be merging anything, so this is safe. // NOTE: "--pretty=format: " is to disable diff output, we only want the // part we get from "--raw". list($raw) = $repository->execxLocalCommand('log -n1 -M -C -B --find-copies-harder --raw -t ' . '--pretty=format: --abbrev=40 %s', $commit->getCommitIdentifier()); } else { // Otherwise, we can use "diff", which will give us output for merges. // We diff against the first parent, as this is generally the expectation // and results in sensible behavior. list($raw) = $repository->execxLocalCommand('diff -n1 -M -C -B --find-copies-harder --raw -t ' . '--abbrev=40 %s^1 %s', $commit->getCommitIdentifier(), $commit->getCommitIdentifier()); } $changes = array(); $move_away = array(); $copy_away = array(); $lines = explode("\n", $raw); foreach ($lines as $line) { if (!strlen(trim($line))) { continue; } list($old_mode, $new_mode, $old_hash, $new_hash, $more_stuff) = preg_split('/ +/', $line, 5); // We may only have two pieces here. list($action, $src_path, $dst_path) = array_merge(explode("\t", $more_stuff), array(null)); // Normalize the paths for consistency with the SVN workflow. $src_path = '/' . $src_path; if ($dst_path) { $dst_path = '/' . $dst_path; } $old_mode = intval($old_mode, 8); $new_mode = intval($new_mode, 8); switch ($new_mode & 0160000) { case 0160000: $file_type = DifferentialChangeType::FILE_SUBMODULE; break; case 0120000: $file_type = DifferentialChangeType::FILE_SYMLINK; break; case 040000: $file_type = DifferentialChangeType::FILE_DIRECTORY; break; default: $file_type = DifferentialChangeType::FILE_NORMAL; break; } // TODO: We can detect binary changes as git does, through a combination // of running 'git check-attr' for stuff like 'binary', 'merge' or 'diff', // and by falling back to inspecting the first 8,000 characters of the // buffer for null bytes (this is seriously git's algorithm, see // buffer_is_binary() in xdiff-interface.c). $change_type = null; $change_path = $src_path; $change_target = null; $is_direct = true; switch ($action[0]) { case 'A': $change_type = DifferentialChangeType::TYPE_ADD; break; case 'D': $change_type = DifferentialChangeType::TYPE_DELETE; break; case 'C': $change_type = DifferentialChangeType::TYPE_COPY_HERE; $change_path = $dst_path; $change_target = $src_path; $copy_away[$change_target][] = $change_path; break; case 'R': $change_type = DifferentialChangeType::TYPE_MOVE_HERE; $change_path = $dst_path; $change_target = $src_path; $move_away[$change_target][] = $change_path; break; case 'T': // Type of the file changed, fall through and treat it as a // modification. Not 100% sure this is the right thing to do but it // seems reasonable. // Type of the file changed, fall through and treat it as a // modification. Not 100% sure this is the right thing to do but it // seems reasonable. case 'M': if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { $change_type = DifferentialChangeType::TYPE_CHILD; $is_direct = false; } else { $change_type = DifferentialChangeType::TYPE_CHANGE; } break; // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible // in theory but shouldn't appear here. // NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible // in theory but shouldn't appear here. default: throw new Exception("Failed to parse line '{$line}'."); } $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $file_type, 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => $change_target, 'targetCommitID' => $change_target ? $commit->getID() : null); } // Add a change to '/' since git doesn't mention it. $changes['/'] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => '/', 'changeType' => DifferentialChangeType::TYPE_CHILD, 'fileType' => DifferentialChangeType::FILE_DIRECTORY, 'isDirect' => false, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null); foreach ($copy_away as $change_path => $destinations) { if (isset($move_away[$change_path])) { $change_type = DifferentialChangeType::TYPE_MULTICOPY; $is_direct = true; unset($move_away[$change_path]); } else { $change_type = DifferentialChangeType::TYPE_COPY_AWAY; $is_direct = false; } $reference = $changes[reset($destinations)]; $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => $change_type, 'fileType' => $reference['fileType'], 'isDirect' => $is_direct, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null); } foreach ($move_away as $change_path => $destinations) { $reference = $changes[reset($destinations)]; $changes[$change_path] = array('repositoryID' => $repository->getID(), 'commitID' => $commit->getID(), 'path' => $change_path, 'changeType' => DifferentialChangeType::TYPE_MOVE_AWAY, 'fileType' => $reference['fileType'], 'isDirect' => true, 'commitSequence' => $commit->getEpoch(), 'targetPath' => null, 'targetCommitID' => null); } $paths = array(); foreach ($changes as $change) { $paths[$change['path']] = true; if ($change['targetPath']) { $paths[$change['targetPath']] = true; } } $path_map = $this->lookupOrCreatePaths(array_keys($paths)); foreach ($changes as $key => $change) { $changes[$key]['pathID'] = $path_map[$change['path']]; if ($change['targetPath']) { $changes[$key]['targetPathID'] = $path_map[$change['targetPath']]; } else { $changes[$key]['targetPathID'] = null; } } $conn_w = $repository->establishConnection('w'); $changes_sql = array(); foreach ($changes as $change) { $values = array((int) $change['repositoryID'], (int) $change['pathID'], (int) $change['commitID'], $change['targetPathID'] ? (int) $change['targetPathID'] : 'null', $change['targetCommitID'] ? (int) $change['targetCommitID'] : 'null', (int) $change['changeType'], (int) $change['fileType'], (int) $change['isDirect'], (int) $change['commitSequence']); $changes_sql[] = '(' . implode(', ', $values) . ')'; } queryfx($conn_w, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit->getID()); foreach (array_chunk($changes_sql, 256) as $sql_chunk) { queryfx($conn_w, 'INSERT INTO %T (repositoryID, pathID, commitID, targetPathID, targetCommitID, changeType, fileType, isDirect, commitSequence) VALUES %Q', PhabricatorRepository::TABLE_PATHCHANGE, implode(', ', $sql_chunk)); } $this->finishParse(); }
public function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID()); if (!$data) { // TODO: Permanent failure. return; } $rules = HeraldRule::loadAllByContentTypeWithFullData(HeraldContentTypeConfig::CONTENT_TYPE_COMMIT, $commit->getPHID()); $adapter = new HeraldCommitAdapter($repository, $commit, $data); $engine = new HeraldEngine(); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); $audit_phids = $adapter->getAuditMap(); if ($audit_phids) { $this->createAudits($commit, $audit_phids, $rules); } $this->createAuditsFromCommitMessage($commit, $data); $email_phids = $adapter->getEmailPHIDs(); if (!$email_phids) { return; } if ($repository->getDetail('herald-disabled')) { // This just means "disable email"; audits are (mostly) idempotent. return; } $xscript = $engine->getTranscript(); $revision = $adapter->loadDifferentialRevision(); if ($revision) { $name = $revision->getTitle(); } else { $name = $data->getSummary(); } $author_phid = $data->getCommitDetail('authorPHID'); $reviewer_phid = $data->getCommitDetail('reviewerPHID'); $phids = array_filter(array($author_phid, $reviewer_phid, $commit->getPHID())); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $commit_handle = $handles[$commit->getPHID()]; $commit_name = $commit_handle->getName(); if ($author_phid) { $author_name = $handles[$author_phid]->getName(); } else { $author_name = $data->getAuthorName(); } if ($reviewer_phid) { $reviewer_name = $handles[$reviewer_phid]->getName(); } else { $reviewer_name = null; } $who = implode(', ', array_filter(array($author_name, $reviewer_name))); $description = $data->getCommitMessage(); $commit_uri = PhabricatorEnv::getProductionURI($commit_handle->getURI()); $differential = $revision ? PhabricatorEnv::getProductionURI('/D' . $revision->getID()) : 'No revision.'; $files = $adapter->loadAffectedPaths(); sort($files); $files = implode("\n ", $files); $xscript_id = $xscript->getID(); $manage_uri = PhabricatorEnv::getProductionURI('/herald/view/commits/'); $why_uri = PhabricatorEnv::getProductionURI('/herald/transcript/' . $xscript_id . '/'); $reply_handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit($commit); $reply_instructions = $reply_handler->getReplyHandlerInstructions(); if ($reply_instructions) { $reply_instructions = "\n" . "REPLY HANDLER ACTIONS\n" . " " . $reply_instructions . "\n"; } $body = <<<EOBODY DESCRIPTION {$description} DETAILS {$commit_uri} DIFFERENTIAL REVISION {$differential} AFFECTED FILES {$files} {$reply_instructions} MANAGE HERALD COMMIT RULES {$manage_uri} WHY DID I GET THIS EMAIL? {$why_uri} EOBODY; $prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); $subject = trim("{$prefix} {$commit_name}: {$name}"); $vary_subject = trim("{$prefix} [Commit] {$commit_name}: {$name}"); $threading = PhabricatorAuditCommentEditor::getMailThreading($commit->getPHID()); list($thread_id, $thread_topic) = $threading; $template = new PhabricatorMetaMTAMail(); $template->setRelatedPHID($commit->getPHID()); $template->setSubject($subject); $template->setVarySubject($subject); $template->setBody($body); $template->setThreadID($thread_id, $is_new = true); $template->addHeader('Thread-Topic', $thread_topic); $template->setIsBulk(true); $template->addHeader('X-Herald-Rules', $xscript->getXHeraldRulesHeader()); if ($author_phid) { $template->setFrom($author_phid); } $mails = $reply_handler->multiplexMail($template, id(new PhabricatorObjectHandleData($email_phids))->loadHandles(), array()); foreach ($mails as $mail) { $mail->saveAndSend(); } }