@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Move Differential commit message parsing to a separate, tested class

Summary: Ref T2222. We have a hunk of logic that purely does text parsing here; separate it and get coverage on it.

Test Plan:
- Ran new unit tests.
- Used `differential.parsecommitmessage`.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2222

Differential Revision: https://secure.phabricator.com/D8444

+376 -95
+3
src/__phutil_library_map__.php
··· 356 356 'DifferentialCommentPreviewController' => 'applications/differential/controller/DifferentialCommentPreviewController.php', 357 357 'DifferentialCommentQuery' => 'applications/differential/query/DifferentialCommentQuery.php', 358 358 'DifferentialCommentSaveController' => 'applications/differential/controller/DifferentialCommentSaveController.php', 359 + 'DifferentialCommitMessageParser' => 'applications/differential/parser/DifferentialCommitMessageParser.php', 360 + 'DifferentialCommitMessageParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php', 359 361 'DifferentialCommitsField' => 'applications/differential/customfield/DifferentialCommitsField.php', 360 362 'DifferentialCommitsFieldSpecification' => 'applications/differential/field/specification/DifferentialCommitsFieldSpecification.php', 361 363 'DifferentialConflictsFieldSpecification' => 'applications/differential/field/specification/DifferentialConflictsFieldSpecification.php', ··· 2919 2921 'DifferentialCommentPreviewController' => 'DifferentialController', 2920 2922 'DifferentialCommentQuery' => 'PhabricatorOffsetPagedQuery', 2921 2923 'DifferentialCommentSaveController' => 'DifferentialController', 2924 + 'DifferentialCommitMessageParserTestCase' => 'PhabricatorTestCase', 2922 2925 'DifferentialCommitsField' => 'DifferentialCustomField', 2923 2926 'DifferentialCommitsFieldSpecification' => 'DifferentialFieldSpecification', 2924 2927 'DifferentialConflictsFieldSpecification' => 'DifferentialFieldSpecification',
+10 -95
src/applications/differential/conduit/ConduitAPI_differential_parsecommitmessage_Method.php
··· 89 89 foreach ($aux_fields as $key => $aux_field) { 90 90 $labels = $aux_field->getSupportedCommitMessageLabels(); 91 91 foreach ($labels as $label) { 92 - $normal_label = strtolower($label); 92 + $normal_label = DifferentialCommitMessageParser::normalizeFieldLabel( 93 + $label); 93 94 if (!empty($label_map[$normal_label])) { 94 95 $previous = $label_map[$normal_label]; 95 96 throw new Exception( ··· 102 103 return $label_map; 103 104 } 104 105 105 - private function buildLabelRegexp(array $label_map) { 106 - $field_labels = array_keys($label_map); 107 - foreach ($field_labels as $key => $label) { 108 - $field_labels[$key] = preg_quote($label, '/'); 109 - } 110 - $field_labels = implode('|', $field_labels); 111 - 112 - $field_pattern = '/^(?P<field>'.$field_labels.'):(?P<text>.*)$/i'; 113 - 114 - return $field_pattern; 115 - } 116 106 117 107 private function parseCommitMessage($corpus, array $label_map) { 118 - $label_regexp = $this->buildLabelRegexp($label_map); 108 + $parser = id(new DifferentialCommitMessageParser()) 109 + ->setLabelMap($label_map) 110 + ->setTitleKey('title') 111 + ->setSummaryKey('summary'); 119 112 120 - // Note, deliberately not populating $seen with 'title' because it is 121 - // optional to include the 'Title:' label. We're doing a little special 122 - // casing to consume the first line as the title regardless of whether you 123 - // label it as such or not. 124 - $field = 'title'; 113 + $result = $parser->parseCorpus($corpus); 125 114 126 - $seen = array(); 127 - $lines = explode("\n", trim($corpus)); 128 - $field_map = array(); 129 - foreach ($lines as $key => $line) { 130 - $match = null; 131 - if (preg_match($label_regexp, $line, $match)) { 132 - $lines[$key] = trim($match['text']); 133 - $field = $label_map[strtolower($match['field'])]; 134 - if (!empty($seen[$field])) { 135 - $this->errors[] = "Field '{$field}' occurs twice in commit message!"; 136 - } 137 - $seen[$field] = true; 138 - } 139 - $field_map[$key] = $field; 115 + foreach ($parser->getErrors() as $error) { 116 + $this->errors[] = $error; 140 117 } 141 118 142 - $fields = array(); 143 - foreach ($lines as $key => $line) { 144 - $fields[$field_map[$key]][] = $line; 145 - } 146 - 147 - // This is a piece of special-cased magic which allows you to omit the 148 - // field labels for "title" and "summary". If the user enters a large block 149 - // of text at the beginning of the commit message with an empty line in it, 150 - // treat everything before the blank line as "title" and everything after 151 - // as "summary". 152 - if (isset($fields['title']) && empty($fields['summary'])) { 153 - $lines = $fields['title']; 154 - for ($ii = 0; $ii < count($lines); $ii++) { 155 - if (strlen(trim($lines[$ii])) == 0) { 156 - break; 157 - } 158 - } 159 - if ($ii != count($lines)) { 160 - $fields['title'] = array_slice($lines, 0, $ii); 161 - $fields['summary'] = array_slice($lines, $ii); 162 - } 163 - } 164 - 165 - // Implode all the lines back into chunks of text. 166 - foreach ($fields as $name => $lines) { 167 - $data = rtrim(implode("\n", $lines)); 168 - $data = ltrim($data, "\n"); 169 - $fields[$name] = $data; 170 - } 171 - 172 - // This is another piece of special-cased magic which allows you to 173 - // enter a ridiculously long title, or just type a big block of stream 174 - // of consciousness text, and have some sort of reasonable result conjured 175 - // from it. 176 - if (isset($fields['title'])) { 177 - $terminal = '...'; 178 - $title = $fields['title']; 179 - $short = phutil_utf8_shorten($title, 250, $terminal); 180 - if ($short != $title) { 181 - 182 - // If we shortened the title, split the rest into the summary, so 183 - // we end up with a title like: 184 - // 185 - // Title title tile title title... 186 - // 187 - // ...and a summary like: 188 - // 189 - // ...title title title. 190 - // 191 - // Summary summary summary summary. 192 - 193 - $summary = idx($fields, 'summary', ''); 194 - $offset = strlen($short) - strlen($terminal); 195 - $remainder = ltrim(substr($fields['title'], $offset)); 196 - $summary = '...'.$remainder."\n\n".$summary; 197 - $summary = rtrim($summary, "\n"); 198 - 199 - $fields['title'] = $short; 200 - $fields['summary'] = $summary; 201 - } 202 - } 203 - 204 - return $fields; 119 + return $result; 205 120 } 206 121 207 122
+210
src/applications/differential/parser/DifferentialCommitMessageParser.php
··· 1 + <?php 2 + 3 + /** 4 + * Parses commit messages (containing relaively freeform text with textual 5 + * field labels) into a dictionary of fields. 6 + * 7 + * $parser = id(new DifferentialCommitMessageParser()) 8 + * ->setLabelMap($label_map) 9 + * ->setTitleKey($key_title) 10 + * ->setSummaryKey($key_summary); 11 + * 12 + * $fields = $parser->parseCorpus($corpus); 13 + * $errors = $parser->getErrors(); 14 + * 15 + * This is used by Differential to parse messages entered from the command line. 16 + * 17 + * @task config Configuring the Parser 18 + * @task parse Parsing Messages 19 + * @task support Support Methods 20 + * @task internal Internals 21 + */ 22 + final class DifferentialCommitMessageParser { 23 + 24 + private $labelMap; 25 + private $titleKey; 26 + private $summaryKey; 27 + private $errors; 28 + 29 + 30 + /* -( Configuring the Parser )--------------------------------------------- */ 31 + 32 + 33 + /** 34 + * @task config 35 + */ 36 + public function setLabelMap(array $label_map) { 37 + $this->labelMap = $label_map; 38 + return $this; 39 + } 40 + 41 + 42 + /** 43 + * @task config 44 + */ 45 + public function setTitleKey($title_key) { 46 + $this->titleKey = $title_key; 47 + return $this; 48 + } 49 + 50 + 51 + /** 52 + * @task config 53 + */ 54 + public function setSummaryKey($summary_key) { 55 + $this->summaryKey = $summary_key; 56 + return $this; 57 + } 58 + 59 + 60 + /* -( Parsing Messages )--------------------------------------------------- */ 61 + 62 + 63 + /** 64 + * @task parse 65 + */ 66 + public function parseCorpus($corpus) { 67 + $this->errors = array(); 68 + 69 + $label_map = $this->labelMap; 70 + $key_title = $this->titleKey; 71 + $key_summary = $this->summaryKey; 72 + 73 + if (!$key_title || !$key_summary || ($label_map === null)) { 74 + throw new Exception( 75 + pht( 76 + 'Expected labelMap, summaryKey and titleKey to be set before '. 77 + 'parsing a corpus.')); 78 + } 79 + 80 + $label_regexp = $this->buildLabelRegexp($label_map); 81 + 82 + // NOTE: We're special casing things here to make the "Title:" label 83 + // optional in the message. 84 + $field = $key_title; 85 + 86 + $seen = array(); 87 + $lines = explode("\n", trim($corpus)); 88 + $field_map = array(); 89 + foreach ($lines as $key => $line) { 90 + $match = null; 91 + if (preg_match($label_regexp, $line, $match)) { 92 + $lines[$key] = trim($match['text']); 93 + $field = $label_map[self::normalizeFieldLabel($match['field'])]; 94 + if (!empty($seen[$field])) { 95 + $this->errors[] = pht( 96 + 'Field "%s" occurs twice in commit message!', 97 + $field); 98 + } 99 + $seen[$field] = true; 100 + } 101 + $field_map[$key] = $field; 102 + } 103 + 104 + $fields = array(); 105 + foreach ($lines as $key => $line) { 106 + $fields[$field_map[$key]][] = $line; 107 + } 108 + 109 + // This is a piece of special-cased magic which allows you to omit the 110 + // field labels for "title" and "summary". If the user enters a large block 111 + // of text at the beginning of the commit message with an empty line in it, 112 + // treat everything before the blank line as "title" and everything after 113 + // as "summary". 114 + if (isset($fields[$key_title]) && empty($fields[$key_summary])) { 115 + $lines = $fields[$key_title]; 116 + for ($ii = 0; $ii < count($lines); $ii++) { 117 + if (strlen(trim($lines[$ii])) == 0) { 118 + break; 119 + } 120 + } 121 + if ($ii != count($lines)) { 122 + $fields[$key_title] = array_slice($lines, 0, $ii); 123 + $summary = array_slice($lines, $ii); 124 + if (strlen(trim(implode("\n", $summary)))) { 125 + $fields[$key_summary] = $summary; 126 + } 127 + } 128 + } 129 + 130 + // Implode all the lines back into chunks of text. 131 + foreach ($fields as $name => $lines) { 132 + $data = rtrim(implode("\n", $lines)); 133 + $data = ltrim($data, "\n"); 134 + $fields[$name] = $data; 135 + } 136 + 137 + // This is another piece of special-cased magic which allows you to 138 + // enter a ridiculously long title, or just type a big block of stream 139 + // of consciousness text, and have some sort of reasonable result conjured 140 + // from it. 141 + if (isset($fields[$key_title])) { 142 + $terminal = '...'; 143 + $title = $fields[$key_title]; 144 + $short = phutil_utf8_shorten($title, 250, $terminal); 145 + if ($short != $title) { 146 + 147 + // If we shortened the title, split the rest into the summary, so 148 + // we end up with a title like: 149 + // 150 + // Title title tile title title... 151 + // 152 + // ...and a summary like: 153 + // 154 + // ...title title title. 155 + // 156 + // Summary summary summary summary. 157 + 158 + $summary = idx($fields, $key_summary, ''); 159 + $offset = strlen($short) - strlen($terminal); 160 + $remainder = ltrim(substr($fields[$key_title], $offset)); 161 + $summary = '...'.$remainder."\n\n".$summary; 162 + $summary = rtrim($summary, "\n"); 163 + 164 + $fields[$key_title] = $short; 165 + $fields[$key_summary] = $summary; 166 + } 167 + } 168 + 169 + return $fields; 170 + } 171 + 172 + 173 + /** 174 + * @task parse 175 + */ 176 + public function getErrors() { 177 + return $this->errors; 178 + } 179 + 180 + 181 + /* -( Support Methods )---------------------------------------------------- */ 182 + 183 + 184 + /** 185 + * @task support 186 + */ 187 + public static function normalizeFieldLabel($label) { 188 + return phutil_utf8_strtolower($label); 189 + } 190 + 191 + 192 + /* -( Internals )---------------------------------------------------------- */ 193 + 194 + 195 + /** 196 + * @task internal 197 + */ 198 + private function buildLabelRegexp(array $label_map) { 199 + $field_labels = array_keys($label_map); 200 + foreach ($field_labels as $key => $label) { 201 + $field_labels[$key] = preg_quote($label, '/'); 202 + } 203 + $field_labels = implode('|', $field_labels); 204 + 205 + $field_pattern = '/^(?P<field>'.$field_labels.'):(?P<text>.*)$/i'; 206 + 207 + return $field_pattern; 208 + } 209 + 210 + }
+65
src/applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php
··· 1 + <?php 2 + 3 + final class DifferentialCommitMessageParserTestCase 4 + extends PhabricatorTestCase { 5 + 6 + public function testDifferentialCommitMessageParser() { 7 + $dir = dirname(__FILE__).'/messages/'; 8 + $list = Filesystem::listDirectory($dir, $include_hidden = false); 9 + foreach ($list as $file) { 10 + if (!preg_match('/.txt$/', $file)) { 11 + continue; 12 + } 13 + 14 + $data = Filesystem::readFile($dir.$file); 15 + $divider = "~~~~~~~~~~\n"; 16 + $parts = explode($divider, $data); 17 + if (count($parts) !== 4) { 18 + throw new Exception( 19 + pht( 20 + 'Expected test file "%s" to contain four parts (message, fields, '. 21 + 'output, errors) divided by "~~~~~~~~~~".', 22 + $file)); 23 + } 24 + 25 + list($message, $fields, $output, $errors) = $parts; 26 + $fields = phutil_json_decode($fields, null); 27 + $output = phutil_json_decode($output, null); 28 + $errors = phutil_json_decode($errors, null); 29 + 30 + if ($fields === null || $output === null || $errors === null) { 31 + throw new Exception( 32 + pht( 33 + 'Expected test file "%s" to contain valid JSON in its sections.', 34 + $file)); 35 + } 36 + 37 + $parser = id(new DifferentialCommitMessageParser()) 38 + ->setLabelMap($fields) 39 + ->setTitleKey('title') 40 + ->setSummaryKey('summary'); 41 + 42 + $result_output = $parser->parseCorpus($message); 43 + $result_errors = $parser->getErrors(); 44 + 45 + $this->assertEqual($output, $result_output); 46 + $this->assertEqual($errors, $result_errors); 47 + } 48 + } 49 + 50 + public function testDifferentialCommitMessageParserNormalization() { 51 + $map = array( 52 + 'Test Plan' => 'test plan', 53 + 'REVIEWERS' => 'reviewers', 54 + 'sUmmArY' => 'summary', 55 + ); 56 + 57 + foreach ($map as $input => $expect) { 58 + $this->assertEqual( 59 + $expect, 60 + DifferentialCommitMessageParser::normalizeFieldLabel($input), 61 + pht('Field normalization of label "%s".', $input)); 62 + } 63 + } 64 + 65 + }
+17
src/applications/differential/parser/__tests__/messages/double-field.txt
··· 1 + fix bug 2 + 3 + color: red 4 + color: blue 5 + ~~~~~~~~~~ 6 + { 7 + "color": "color" 8 + } 9 + ~~~~~~~~~~ 10 + { 11 + "title": "fix bug", 12 + "color": "red\nblue" 13 + } 14 + ~~~~~~~~~~ 15 + [ 16 + "Field \"color\" occurs twice in commit message!" 17 + ]
+11
src/applications/differential/parser/__tests__/messages/long-title.txt
··· 1 + 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 2 + 3 + ~~~~~~~~~~ 4 + {} 5 + ~~~~~~~~~~ 6 + { 7 + "title": "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567...", 8 + "summary": "...89012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" 9 + } 10 + ~~~~~~~~~~ 11 + []
+15
src/applications/differential/parser/__tests__/messages/multi-label.txt
··· 1 + fix bug 2 + 3 + hue: black 4 + ~~~~~~~~~~ 5 + { 6 + "color": "color", 7 + "hue": "color" 8 + } 9 + ~~~~~~~~~~ 10 + { 11 + "title": "fix bug", 12 + "color": "black" 13 + } 14 + ~~~~~~~~~~ 15 + []
+24
src/applications/differential/parser/__tests__/messages/normal.txt
··· 1 + Title 2 + 3 + Summary: This is the summary. 4 + 5 + Test Plan: Tested things. 6 + 7 + Reviewers: alincoln 8 + 9 + ~~~~~~~~~~ 10 + { 11 + "title": "title", 12 + "summary": "summary", 13 + "test plan": "test plan", 14 + "reviewers": "reviewers" 15 + } 16 + ~~~~~~~~~~ 17 + { 18 + "title": "Title", 19 + "summary": "This is the summary.", 20 + "test plan": "Tested things.", 21 + "reviewers": "alincoln" 22 + } 23 + ~~~~~~~~~~ 24 + []
+12
src/applications/differential/parser/__tests__/messages/simple.txt
··· 1 + fix bug 2 + 3 + it had a bug but I fixed it 4 + ~~~~~~~~~~ 5 + {} 6 + ~~~~~~~~~~ 7 + { 8 + "title": "fix bug", 9 + "summary": "it had a bug but I fixed it" 10 + } 11 + ~~~~~~~~~~ 12 + []
+9
src/applications/differential/parser/__tests__/messages/trivial.txt
··· 1 + fix bug 2 + ~~~~~~~~~~ 3 + {} 4 + ~~~~~~~~~~ 5 + { 6 + "title": "fix bug" 7 + } 8 + ~~~~~~~~~~ 9 + []