public function toGitPatch()

in src/parser/ArcanistBundle.php [313:485]


  public function toGitPatch() {
    $this->reservedBytes = 0;

    $eol = $this->getEOL('git');

    $result = array();
    $changes = $this->getChanges();

    $binary_sources = array();
    foreach ($changes as $change) {
      if (!$this->isGitBinaryChange($change)) {
        continue;
      }

      $type = $change->getType();
      if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY ||
          $type == ArcanistDiffChangeType::TYPE_COPY_AWAY ||
          $type == ArcanistDiffChangeType::TYPE_MULTICOPY) {
        foreach ($change->getAwayPaths() as $path) {
          $binary_sources[$path] = $change;
        }
      }
    }

    foreach (array_keys($changes) as $multicopy_key) {
      $multicopy_change = $changes[$multicopy_key];

      $type = $multicopy_change->getType();
      if ($type != ArcanistDiffChangeType::TYPE_MULTICOPY) {
        continue;
      }

      // Decompose MULTICOPY into one MOVE_HERE and several COPY_HERE because
      // we need more information than we have in order to build a delete patch
      // and represent it as a bunch of COPY_HERE plus a delete. For details,
      // see T419.

      // Basically, MULTICOPY means there are 2 or more corresponding COPY_HERE
      // changes, so find one of them arbitrarily and turn it into a MOVE_HERE.

      // TODO: We might be able to do this more cleanly after T230 is resolved.

      $decompose_okay = false;
      foreach ($changes as $change_key => $change) {
        if ($change->getType() != ArcanistDiffChangeType::TYPE_COPY_HERE) {
          continue;
        }
        if ($change->getOldPath() != $multicopy_change->getCurrentPath()) {
          continue;
        }
        $decompose_okay = true;
        $change = clone $change;
        $change->setType(ArcanistDiffChangeType::TYPE_MOVE_HERE);
        $changes[$change_key] = $change;

        // The multicopy is now fully represented by MOVE_HERE plus one or more
        // COPY_HERE, so throw it away.
        unset($changes[$multicopy_key]);
        break;
      }

      if (!$decompose_okay) {
        throw new Exception(
          pht(
            'Failed to decompose multicopy changeset in '.
            'order to generate diff.'));
      }
    }

    foreach ($changes as $change) {
      $type = $change->getType();
      $file_type = $change->getFileType();

      if ($file_type == ArcanistDiffChangeType::FILE_DIRECTORY) {
        // TODO: We should raise a FYI about this, so the user is aware
        // that we omitted it, if the directory is empty or has permissions
        // which git can't represent.

        // Git doesn't support empty directories, so we simply ignore them. If
        // the directory is nonempty, 'git apply' will create it when processing
        // the changesets for files inside it.
        continue;
      }

      if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY) {
        // Git will apply this in the corresponding MOVE_HERE.
        continue;
      }

      $old_mode = idx($change->getOldProperties(), 'unix:filemode', '100644');
      $new_mode = idx($change->getNewProperties(), 'unix:filemode', '100644');

      $is_binary = $this->isGitBinaryChange($change);

      if ($is_binary) {
        $old_binary = idx($binary_sources, $this->getCurrentPath($change));
        $change_body = $this->buildBinaryChange($change, $old_binary);
      } else {
        $change_body = $this->buildHunkChanges($change->getHunks(), $eol);
      }
      if ($type == ArcanistDiffChangeType::TYPE_COPY_AWAY) {
        // TODO: This is only relevant when patching old Differential diffs
        // which were created prior to arc pruning TYPE_COPY_AWAY for files
        // with no modifications.
        if (!strlen($change_body) && ($old_mode == $new_mode)) {
          continue;
        }
      }

      $old_path = $this->getOldPath($change);
      $cur_path = $this->getCurrentPath($change);

      if ($old_path === null) {
        $old_index = 'a/'.$cur_path;
        $old_target  = '/dev/null';
      } else {
        $old_index = 'a/'.$old_path;
        $old_target  = 'a/'.$old_path;
      }

      if ($cur_path === null) {
        $cur_index = 'b/'.$old_path;
        $cur_target  = '/dev/null';
      } else {
        $cur_index = 'b/'.$cur_path;
        $cur_target  = 'b/'.$cur_path;
      }

      $old_target = $this->encodeGitTargetPath($old_target);
      $cur_target = $this->encodeGitTargetPath($cur_target);

      $result[] = "diff --git {$old_index} {$cur_index}".$eol;

      if ($type == ArcanistDiffChangeType::TYPE_ADD) {
        $result[] = "new file mode {$new_mode}".$eol;
      }

      if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE ||
          $type == ArcanistDiffChangeType::TYPE_MOVE_HERE ||
          $type == ArcanistDiffChangeType::TYPE_COPY_AWAY ||
          $type == ArcanistDiffChangeType::TYPE_CHANGE) {
        if ($old_mode !== $new_mode) {
          $result[] = "old mode {$old_mode}".$eol;
          $result[] = "new mode {$new_mode}".$eol;
        }
      }

      if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE) {
        $result[] = "copy from {$old_path}".$eol;
        $result[] = "copy to {$cur_path}".$eol;
      } else if ($type == ArcanistDiffChangeType::TYPE_MOVE_HERE) {
        $result[] = "rename from {$old_path}".$eol;
        $result[] = "rename to {$cur_path}".$eol;
      } else if ($type == ArcanistDiffChangeType::TYPE_DELETE ||
                 $type == ArcanistDiffChangeType::TYPE_MULTICOPY) {
        $old_mode = idx($change->getOldProperties(), 'unix:filemode');
        if ($old_mode) {
          $result[] = "deleted file mode {$old_mode}".$eol;
        }
      }

      if ($change_body) {
        if (!$is_binary) {
          $result[] = "--- {$old_target}".$eol;
          $result[] = "+++ {$cur_target}".$eol;
        }
        $result[] = $change_body;
      }
    }

    $diff = implode('', $result).$eol;
    return $this->convertNonUTF8Diff($diff);
  }