diff --git a/src/applications/differential/conduit/ConduitAPI_differential_parsecommitmessage_Method.php b/src/applications/differential/conduit/ConduitAPI_differential_parsecommitmessage_Method.php index 10e2b35468..234a0fdf68 100644 --- a/src/applications/differential/conduit/ConduitAPI_differential_parsecommitmessage_Method.php +++ b/src/applications/differential/conduit/ConduitAPI_differential_parsecommitmessage_Method.php @@ -1,176 +1,208 @@ 'required string', 'partial' => 'optional bool', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $corpus = $request->getValue('corpus'); $is_partial = $request->getValue('partial'); $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($aux_fields as $key => $aux_field) { $aux_field->setUser($request->getUser()); if (!$aux_field->shouldAppearOnCommitMessage()) { unset($aux_fields[$key]); } } $aux_fields = mpull($aux_fields, null, 'getCommitMessageKey'); $this->errors = array(); // Build a map from labels (like "Test Plan") to field keys // (like "testPlan"). $label_map = $this->buildLabelMap($aux_fields); $field_map = $this->parseCommitMessage($corpus, $label_map); $fields = array(); foreach ($field_map as $field_key => $field_value) { $field = $aux_fields[$field_key]; try { $fields[$field_key] = $field->parseValueFromCommitMessage($field_value); $field->setValueFromParsedCommitMessage($fields[$field_key]); } catch (DifferentialFieldParseException $ex) { $field_label = $field->renderLabelForCommitMessage(); $this->errors[] = "Error parsing field '{$field_label}': ".$ex->getMessage(); } } if (!$is_partial) { foreach ($aux_fields as $field_key => $aux_field) { try { $aux_field->validateField(); } catch (DifferentialFieldValidationException $ex) { $field_label = $aux_field->renderLabelForCommitMessage(); $this->errors[] = "Invalid or missing field '{$field_label}': ". $ex->getMessage(); } } } return array( 'errors' => $this->errors, 'fields' => $fields, ); } private function buildLabelMap(array $aux_fields) { assert_instances_of($aux_fields, 'DifferentialFieldSpecification'); $label_map = array(); foreach ($aux_fields as $key => $aux_field) { $labels = $aux_field->getSupportedCommitMessageLabels(); foreach ($labels as $label) { $normal_label = strtolower($label); if (!empty($label_map[$normal_label])) { $previous = $label_map[$normal_label]; throw new Exception( "Field label '{$label}' is parsed by two fields: '{$key}' and ". "'{$previous}'. Each label must be parsed by only one field."); } $label_map[$normal_label] = $key; } } return $label_map; } private function buildLabelRegexp(array $label_map) { $field_labels = array_keys($label_map); foreach ($field_labels as $key => $label) { $field_labels[$key] = preg_quote($label, '/'); } $field_labels = implode('|', $field_labels); $field_pattern = '/^(?P'.$field_labels.'):(?P.*)$/i'; return $field_pattern; } private function parseCommitMessage($corpus, array $label_map) { $label_regexp = $this->buildLabelRegexp($label_map); // Note, deliberately not populating $seen with 'title' because it is // optional to include the 'Title:' label. We're doing a little special // casing to consume the first line as the title regardless of whether you // label it as such or not. $field = 'title'; $seen = array(); $lines = explode("\n", trim($corpus)); $field_map = array(); foreach ($lines as $key => $line) { $match = null; if (preg_match($label_regexp, $line, $match)) { $lines[$key] = trim($match['text']); $field = $label_map[strtolower($match['field'])]; if (!empty($seen[$field])) { $this->errors[] = "Field '{$field}' occurs twice in commit message!"; } $seen[$field] = true; } $field_map[$key] = $field; } $fields = array(); foreach ($lines as $key => $line) { $fields[$field_map[$key]][] = $line; } // This is a piece of special-cased magic which allows you to omit the // field labels for "title" and "summary". If the user enters a large block // of text at the beginning of the commit message with an empty line in it, // treat everything before the blank line as "title" and everything after // as "summary". if (isset($fields['title']) && empty($fields['summary'])) { $lines = $fields['title']; for ($ii = 0; $ii < count($lines); $ii++) { if (strlen(trim($lines[$ii])) == 0) { break; } } if ($ii != count($lines)) { $fields['title'] = array_slice($lines, 0, $ii); $fields['summary'] = array_slice($lines, $ii); } } // Implode all the lines back into chunks of text. foreach ($fields as $name => $lines) { $data = rtrim(implode("\n", $lines)); $data = ltrim($data, "\n"); $fields[$name] = $data; } + // This is another piece of special-cased magic which allows you to + // enter a ridiculously long title, or just type a big block of stream + // of consciousness text, and have some sort of reasonable result conjured + // from it. + if (isset($fields['title'])) { + $terminal = '...'; + $title = $fields['title']; + $short = phutil_utf8_shorten($title, 250, $terminal); + if ($short != $title) { + + // If we shortened the title, split the rest into the summary, so + // we end up with a title like: + // + // Title title tile title title... + // + // ...and a summary like: + // + // ...title title title. + // + // Summary summary summary summary. + + $summary = idx($fields, 'summary', ''); + $offset = strlen($short) - strlen($terminal); + $remainder = ltrim(substr($fields['title'], $offset)); + $summary = '...'.$remainder."\n\n".$summary; + $summary = rtrim($summary, "\n"); + + $fields['title'] = $short; + $fields['summary'] = $summary; + } + } + return $fields; } }