1<?php
2
3/**
4 * Represents a parsed commit message.
5 */
6final class ArcanistDifferentialCommitMessage extends Phobject {
7
8  private $rawCorpus;
9  private $revisionID;
10  private $fields = array();
11  private $xactions = null;
12
13  private $gitSVNBaseRevision;
14  private $gitSVNBasePath;
15  private $gitSVNUUID;
16
17  public static function newFromRawCorpus($corpus) {
18    $obj = new ArcanistDifferentialCommitMessage();
19    $obj->rawCorpus = $corpus;
20    $obj->revisionID = $obj->parseRevisionIDFromRawCorpus($corpus);
21
22    $pattern = '/^git-svn-id:\s*([^@]+)@(\d+)\s+(.*)$/m';
23    $match = null;
24    if (preg_match($pattern, $corpus, $match)) {
25      $obj->gitSVNBaseRevision = $match[1].'@'.$match[2];
26      $obj->gitSVNBasePath     = $match[1];
27      $obj->gitSVNUUID         = $match[3];
28    }
29
30    return $obj;
31  }
32
33  public function getRawCorpus() {
34    return $this->rawCorpus;
35  }
36
37  public function getRevisionID() {
38    return $this->revisionID;
39  }
40
41  public function getRevisionMonogram() {
42    if ($this->revisionID) {
43      return 'D'.$this->revisionID;
44    }
45    return null;
46  }
47
48  public function pullDataFromConduit(
49    ConduitClient $conduit,
50    $partial = false) {
51
52    $result = $conduit->callMethodSynchronous(
53      'differential.parsecommitmessage',
54      array(
55        'corpus'  => $this->rawCorpus,
56        'partial' => $partial,
57      ));
58
59    $this->fields = $result['fields'];
60
61    // NOTE: This does not exist prior to late October 2017.
62    $this->xactions = idx($result, 'transactions');
63
64    if (!empty($result['errors'])) {
65      throw new ArcanistDifferentialCommitMessageParserException(
66        $result['errors']);
67    }
68
69    return $this;
70  }
71
72  public function getFieldValue($key) {
73    if (array_key_exists($key, $this->fields)) {
74      return $this->fields[$key];
75    }
76    return null;
77  }
78
79  public function setFieldValue($key, $value) {
80    $this->fields[$key] = $value;
81    return $this;
82  }
83
84  public function getFields() {
85    return $this->fields;
86  }
87
88  public function getGitSVNBaseRevision() {
89    return $this->gitSVNBaseRevision;
90  }
91
92  public function getGitSVNBasePath() {
93    return $this->gitSVNBasePath;
94  }
95
96  public function getGitSVNUUID() {
97    return $this->gitSVNUUID;
98  }
99
100  public function getChecksum() {
101    $fields = array_filter($this->fields);
102    ksort($fields);
103    $fields = json_encode($fields);
104    return md5($fields);
105  }
106
107  public function getTransactions() {
108    return $this->xactions;
109  }
110
111  /**
112   * Extract the revision ID from a commit message.
113   *
114   * @param string Raw commit message.
115   * @return int|null Revision ID, if the commit message contains one.
116   */
117  private function parseRevisionIDFromRawCorpus($corpus) {
118    $match = null;
119    if (!preg_match('/^Differential Revision:\s*(.+)/im', $corpus, $match)) {
120      return null;
121    }
122
123    $revision_value = trim($match[1]);
124    $revision_pattern = '/^[dD]([1-9]\d*)\z/';
125
126    // Accept a bare revision ID like "D123".
127    if (preg_match($revision_pattern, $revision_value, $match)) {
128      return (int)$match[1];
129    }
130
131    // Otherwise, try to find a full URI.
132    $uri = new PhutilURI($revision_value);
133    $path = $uri->getPath();
134    $path = trim($path, '/');
135    if (preg_match($revision_pattern, $path, $match)) {
136      return (int)$match[1];
137    }
138
139    throw new ArcanistUsageException(
140      pht(
141        'Invalid "Differential Revision" field in commit message. This field '.
142        'should have a revision identifier like "%s" or a Phabricator URI '.
143        'like "%s", but has "%s".',
144        'D123',
145        'https://phabricator.example.com/D123',
146        $revision_value));
147  }
148
149}
150