3 final class DifferentialHunk
4 extends DifferentialDAO
6 PhabricatorPolicyInterface
,
7 PhabricatorDestructibleInterface
{
9 protected $changesetID;
15 protected $dataEncoding;
16 protected $dataFormat;
21 private $structuredLines;
22 private $structuredFiles = array();
25 private $forcedEncoding;
28 const FLAG_LINES_ADDED
= 1;
29 const FLAG_LINES_REMOVED
= 2;
30 const FLAG_LINES_STABLE
= 4;
32 const DATATYPE_TEXT
= 'text';
33 const DATATYPE_FILE
= 'file';
35 const DATAFORMAT_RAW
= 'byte';
36 const DATAFORMAT_DEFLATED
= 'gzde';
38 protected function getConfiguration() {
40 self
::CONFIG_BINARY
=> array(
43 self
::CONFIG_COLUMN_SCHEMA
=> array(
44 'dataType' => 'bytes4',
45 'dataEncoding' => 'text16?',
46 'dataFormat' => 'bytes4',
47 'oldOffset' => 'uint32',
49 'newOffset' => 'uint32',
52 self
::CONFIG_KEY_SCHEMA
=> array(
53 'key_changeset' => array(
54 'columns' => array('changesetID'),
56 'key_created' => array(
57 'columns' => array('dateCreated'),
60 ) + parent
::getConfiguration();
63 public function getAddedLines() {
64 return $this->makeContent($include = '+');
67 public function getRemovedLines() {
68 return $this->makeContent($include = '-');
71 public function makeNewFile() {
72 return implode('', $this->makeContent($include = ' +'));
75 public function makeOldFile() {
76 return implode('', $this->makeContent($include = ' -'));
79 public function makeChanges() {
80 return implode('', $this->makeContent($include = '-+'));
83 public function getStructuredOldFile() {
84 return $this->getStructuredFile('-');
87 public function getStructuredNewFile() {
88 return $this->getStructuredFile('+');
91 private function getStructuredFile($kind) {
92 if ($kind !== '+' && $kind !== '-') {
95 'Structured file kind should be "+" or "-", got "%s".',
99 if (!isset($this->structuredFiles
[$kind])) {
101 $number = $this->newOffset
;
103 $number = $this->oldOffset
;
106 $lines = $this->getStructuredLines();
108 // NOTE: We keep the "\ No newline at end of file" line if it appears
109 // after a line which is not excluded. For example, if we're constructing
110 // the "+" side of the diff, we want to ignore this one since it's
111 // relevant only to the "-" side of the diff:
114 // \ No newline at end of file
117 // ...but we want to keep this one:
121 // \ No newline at end of file
125 foreach ($lines as $line) {
126 switch ($line['type']) {
129 $file[$number++
] = $line;
134 // Strip the actual newline off the line's text.
135 $text = $file[$number - 1]['text'];
136 $text = rtrim($text, "\r\n");
137 $file[$number - 1]['text'] = $text;
139 $file[$number++
] = $line;
149 $this->structuredFiles
[$kind] = $file;
152 return $this->structuredFiles
[$kind];
155 public function getSplitLines() {
156 if ($this->splitLines
=== null) {
157 $this->splitLines
= phutil_split_lines($this->getChanges());
159 return $this->splitLines
;
162 public function getStructuredLines() {
163 if ($this->structuredLines
=== null) {
164 $lines = $this->getSplitLines();
166 $structured = array();
167 foreach ($lines as $line) {
168 if (empty($line[0])) {
169 // TODO: Can we just get rid of this?
173 $structured[] = array(
175 'text' => substr($line, 1),
179 $this->structuredLines
= $structured;
182 return $this->structuredLines
;
186 public function getContentWithMask($mask) {
189 if (($mask & self
::FLAG_LINES_ADDED
)) {
193 if (($mask & self
::FLAG_LINES_REMOVED
)) {
197 if (($mask & self
::FLAG_LINES_STABLE
)) {
201 $include = implode('', $include);
203 return implode('', $this->makeContent($include));
206 private function makeContent($include) {
207 $lines = $this->getSplitLines();
210 $include_map = array();
211 for ($ii = 0; $ii < strlen($include); $ii++
) {
212 $include_map[$include[$ii]] = true;
215 if (isset($include_map['+'])) {
216 $n = $this->newOffset
;
218 $n = $this->oldOffset
;
221 $use_next_newline = false;
222 foreach ($lines as $line) {
223 if (!isset($line[0])) {
227 if ($line[0] == '\\') {
228 if ($use_next_newline) {
229 $results[last_key($results)] = rtrim(end($results), "\n");
231 } else if (empty($include_map[$line[0]])) {
232 $use_next_newline = false;
234 $use_next_newline = true;
235 $results[$n] = substr($line, 1);
238 if ($line[0] == ' ' ||
isset($include_map[$line[0]])) {
246 public function getChangeset() {
247 return $this->assertAttached($this->changeset
);
250 public function attachChangeset(DifferentialChangeset
$changeset) {
251 $this->changeset
= $changeset;
256 /* -( Storage )------------------------------------------------------------ */
259 public function setChanges($text) {
260 $this->rawData
= $text;
262 $this->dataEncoding
= $this->detectEncodingForStorage($text);
263 $this->dataType
= self
::DATATYPE_TEXT
;
265 list($format, $data) = $this->formatDataForStorage($text);
267 $this->dataFormat
= $format;
273 public function getChanges() {
274 return $this->getUTF8StringFromStorage(
276 nonempty($this->forcedEncoding
, $this->getDataEncoding()));
279 public function forceEncoding($encoding) {
280 $this->forcedEncoding
= $encoding;
284 private function formatDataForStorage($data) {
285 $deflated = PhabricatorCaches
::maybeDeflateData($data);
286 if ($deflated !== null) {
287 return array(self
::DATAFORMAT_DEFLATED
, $deflated);
290 return array(self
::DATAFORMAT_RAW
, $data);
293 public function getAutomaticDataFormat() {
294 // If the hunk is already stored deflated, just keep it deflated. This is
295 // mostly a performance improvement for "bin/differential migrate-hunk" so
296 // that we don't have to recompress all the stored hunks when looking for
297 // stray uncompressed hunks.
298 if ($this->dataFormat
=== self
::DATAFORMAT_DEFLATED
) {
299 return self
::DATAFORMAT_DEFLATED
;
302 list($format) = $this->formatDataForStorage($this->getRawData());
307 public function saveAsText() {
308 $old_type = $this->getDataType();
309 $old_data = $this->getData();
311 $raw_data = $this->getRawData();
313 $this->setDataType(self
::DATATYPE_TEXT
);
315 list($format, $data) = $this->formatDataForStorage($raw_data);
316 $this->setDataFormat($format);
317 $this->setData($data);
319 $result = $this->save();
321 $this->destroyData($old_type, $old_data);
326 public function saveAsFile() {
327 $old_type = $this->getDataType();
328 $old_data = $this->getData();
330 $raw_data = $this->getRawData();
332 list($format, $data) = $this->formatDataForStorage($raw_data);
333 $this->setDataFormat($format);
335 $file = PhabricatorFile
::newFromFileData(
338 'name' => 'differential-hunk',
339 'mime-type' => 'application/octet-stream',
340 'viewPolicy' => PhabricatorPolicies
::POLICY_NOONE
,
343 $this->setDataType(self
::DATATYPE_FILE
);
344 $this->setData($file->getPHID());
346 // NOTE: Because hunks don't have a PHID and we just load hunk data with
347 // the omnipotent viewer, we do not need to attach the file to anything.
349 $result = $this->save();
351 $this->destroyData($old_type, $old_data);
356 private function getRawData() {
357 if ($this->rawData
=== null) {
358 $type = $this->getDataType();
359 $data = $this->getData();
362 case self
::DATATYPE_TEXT
:
363 // In this storage type, the changes are stored on the object.
366 case self
::DATATYPE_FILE
:
367 $data = $this->loadFileData();
371 pht('Hunk has unsupported data type "%s"!', $type));
374 $format = $this->getDataFormat();
376 case self
::DATAFORMAT_RAW
:
377 // In this format, the changes are stored as-is.
380 case self
::DATAFORMAT_DEFLATED
:
381 $data = PhabricatorCaches
::inflateData($data);
385 pht('Hunk has unsupported data encoding "%s"!', $type));
388 $this->rawData
= $data;
391 return $this->rawData
;
394 private function loadFileData() {
395 if ($this->fileData
=== null) {
396 $type = $this->getDataType();
397 if ($type !== self
::DATATYPE_FILE
) {
400 'Unable to load file data for hunk with wrong data type ("%s").',
404 $file_phid = $this->getData();
406 $file = $this->loadRawFile($file_phid);
407 $data = $file->loadFileData();
409 $this->fileData
= $data;
412 return $this->fileData
;
415 private function loadRawFile($file_phid) {
416 $viewer = PhabricatorUser
::getOmnipotentUser();
419 $files = id(new PhabricatorFileQuery())
421 ->withPHIDs(array($file_phid))
426 'Failed to load file ("%s") with hunk data.',
430 $file = head($files);
435 private function destroyData(
438 PhabricatorDestructionEngine
$engine = null) {
441 $engine = new PhabricatorDestructionEngine();
445 case self
::DATATYPE_FILE
:
446 $file = $this->loadRawFile($data);
447 $engine->destroyObject($file);
453 /* -( PhabricatorPolicyInterface )----------------------------------------- */
456 public function getCapabilities() {
458 PhabricatorPolicyCapability
::CAN_VIEW
,
462 public function getPolicy($capability) {
463 return $this->getChangeset()->getPolicy($capability);
466 public function hasAutomaticCapability($capability, PhabricatorUser
$viewer) {
467 return $this->getChangeset()->hasAutomaticCapability($capability, $viewer);
471 /* -( PhabricatorDestructibleInterface )----------------------------------- */
474 public function destroyObjectPermanently(
475 PhabricatorDestructionEngine
$engine) {
477 $type = $this->getDataType();
478 $data = $this->getData();
480 $this->destroyData($type, $data, $engine);