[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Parses commit messages (containing relatively 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 = id(new PhutilUTF8StringTruncator()) 145 ->setMaximumGlyphs(250) 146 ->setTerminator($terminal) 147 ->truncateString($title); 148 149 if ($short != $title) { 150 151 // If we shortened the title, split the rest into the summary, so 152 // we end up with a title like: 153 // 154 // Title title tile title title... 155 // 156 // ...and a summary like: 157 // 158 // ...title title title. 159 // 160 // Summary summary summary summary. 161 162 $summary = idx($fields, $key_summary, ''); 163 $offset = strlen($short) - strlen($terminal); 164 $remainder = ltrim(substr($fields[$key_title], $offset)); 165 $summary = '...'.$remainder."\n\n".$summary; 166 $summary = rtrim($summary, "\n"); 167 168 $fields[$key_title] = $short; 169 $fields[$key_summary] = $summary; 170 } 171 } 172 173 return $fields; 174 } 175 176 177 /** 178 * @task parse 179 */ 180 public function getErrors() { 181 return $this->errors; 182 } 183 184 185 /* -( Support Methods )---------------------------------------------------- */ 186 187 188 /** 189 * @task support 190 */ 191 public static function normalizeFieldLabel($label) { 192 return phutil_utf8_strtolower($label); 193 } 194 195 196 /* -( Internals )---------------------------------------------------------- */ 197 198 199 /** 200 * @task internal 201 */ 202 private function buildLabelRegexp(array $label_map) { 203 $field_labels = array_keys($label_map); 204 foreach ($field_labels as $key => $label) { 205 $field_labels[$key] = preg_quote($label, '/'); 206 } 207 $field_labels = implode('|', $field_labels); 208 209 $field_pattern = '/^(?P<field>'.$field_labels.'):(?P<text>.*)$/i'; 210 211 return $field_pattern; 212 } 213 214 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |