Package org.eclipse.egit.core.op

Source Code of org.eclipse.egit.core.op.CreatePatchOperation

/*******************************************************************************
* Copyright (c) 2010, 2012 SAP AG and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*    Stefan Lay (SAP AG) - initial implementation
*    Benjamin Muskalla (Tasktop Technologies) - extract into operation
*    Tomasz Zarna (IBM Corporation) - bug 370332
*    Daniel Megert <daniel_megert@ch.ibm.com> - Allow spaces in path
*******************************************************************************/
package org.eclipse.egit.core.op;

import static org.eclipse.jgit.lib.Constants.encodeASCII;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.EclipseGitProgressTransformer;
import org.eclipse.egit.core.internal.CompareCoreUtils;
import org.eclipse.egit.core.internal.CoreText;
import org.eclipse.egit.core.project.RepositoryMapping;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.diff.DiffEntry.Side;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.osgi.util.NLS;

/**
* Creates a patch for a specific commit
*/
public class CreatePatchOperation implements IEGitOperation {

  /**
   * Diff header format
   *
   */
  public enum DiffHeaderFormat {
    /**
     * No header
     */
    NONE(CoreText.DiffHeaderFormat_None, false, null),

    /**
     * Workspace patch
     */
    WORKSPACE(CoreText.DiffHeaderFormat_Workspace, false, "### Eclipse Workspace Patch 1.0\n"), //$NON-NLS-1$

    /**
     * Email header
     */
    EMAIL(CoreText.DiffHeaderFormat_Email, true, "From ${sha1} ${date}\nFrom: ${author}\nDate: ${author date}\nSubject: [PATCH] ${title line}\n${full commit message}\n"), //$NON-NLS-1$

    /**
     * Header designed to be as compact as possible
     */
    ONELINE(CoreText.DiffHeaderFormat_Oneline, true, "${sha1} ${title line}\n"); //$NON-NLS-1$

    private final String description;

    private final boolean commitRequired;

    private final String template;

    private DiffHeaderFormat(final String d, final boolean c, final String t) {
      description = d;
      commitRequired = c;
      template = t;
    }

    /**
     * @return if this format requires a commit.
     */
    public boolean isCommitRequired() {
      return commitRequired;
    }

    /**
     * @return the template
     */
    public String getTemplate() {
      return template;
    }

    /**
     * @return the description
     */
    public String getDescription() {
      return description;
    }
  }

  enum DiffHeaderKeyword{
    SHA1, AUTHOR_DATE, AUTHOR, DATE, TITLE_LINE, FULL_COMMIT_MESSAGE
  }

  /**
   * The default number of lines to use as context
   */
  public static final int DEFAULT_CONTEXT_LINES = 3;

  private final RevCommit commit;

  private final Repository repository;

  private DiffHeaderFormat headerFormat = DiffHeaderFormat.EMAIL;

  // the encoding for the currently processed file
  private String currentEncoding = null;

  private String patchContent;

  private int contextLines = DEFAULT_CONTEXT_LINES;

  private TreeFilter pathFilter = null;

  /**
   * Creates the new operation.
   *
   * @param repository
   * @param commit
   */
  public CreatePatchOperation(Repository repository, RevCommit commit) {
    if (repository == null)
      throw new IllegalArgumentException(
          CoreText.CreatePatchOperation_repoRequired);
    this.repository = repository;
    this.commit = commit;
  }

  public void execute(IProgressMonitor monitor) throws CoreException {
    EclipseGitProgressTransformer gitMonitor;
    if (monitor == null)
      gitMonitor = new EclipseGitProgressTransformer(
          new NullProgressMonitor());
    else
      gitMonitor = new EclipseGitProgressTransformer(monitor);

    final StringBuilder sb = new StringBuilder();
    final DiffFormatter diffFmt = new DiffFormatter(
        new ByteArrayOutputStream() {

          @Override
          public synchronized void write(byte[] b, int off, int len) {
            super.write(b, off, len);
            try {
              if (currentEncoding == null)
                sb.append(toString("UTF-8")); //$NON-NLS-1$
              else
                sb.append(toString(currentEncoding));
            } catch (UnsupportedEncodingException e) {
              sb.append(toString());
            }
            reset();
          }
        }) {
      private IProject project;

      @Override
      public void format(DiffEntry ent) throws IOException,
          CorruptObjectException, MissingObjectException {
        // for "workspace patches" add project header each time project changes
        if (DiffHeaderFormat.WORKSPACE == headerFormat) {
          IProject p = getProject(ent);
          if (!p.equals(project)) {
            project = p;
            getOutputStream().write(
                encodeASCII("#P " + project.getName() + "\n")); //$NON-NLS-1$ //$NON-NLS-2$
          }
        }
        super.format(ent);
      }
    };

    diffFmt.setProgressMonitor(gitMonitor);
    diffFmt.setContext(contextLines);

    if (headerFormat != null && headerFormat != DiffHeaderFormat.NONE)
      writeGitPatchHeader(sb);

    diffFmt.setRepository(repository);
    diffFmt.setPathFilter(pathFilter);

    try {
      if (commit != null) {
        RevCommit[] parents = commit.getParents();
        if (parents.length > 1)
          throw new IllegalStateException(
              CoreText.CreatePatchOperation_cannotCreatePatchForMergeCommit);

        ObjectId parentId;
        if (parents.length > 0)
          parentId = parents[0].getId();
        else
          parentId = null;
        List<DiffEntry> diffs = diffFmt.scan(parentId, commit.getId());
        for (DiffEntry ent : diffs) {
          String path;
          if (ChangeType.DELETE.equals(ent.getChangeType()))
            path = ent.getOldPath();
          else
            path = ent.getNewPath();
          currentEncoding = CompareCoreUtils.getResourceEncoding(repository, path);
          diffFmt.format(ent);
        }
      } else
        diffFmt.format(
            new DirCacheIterator(repository.readDirCache()),
            new FileTreeIterator(repository));
    } catch (IOException e) {
      Activator.logError(CoreText.CreatePatchOperation_patchFileCouldNotBeWritten, e);
    }

    if (DiffHeaderFormat.WORKSPACE == headerFormat)
      updateWorkspacePatchPrefixes(sb, diffFmt);

    // trim newline
    if (sb.charAt(sb.length() - 1) == '\n')
      sb.setLength(sb.length() - 1);

    patchContent = sb.toString();
  }

  private IProject getProject(final DiffEntry ent) {
    Side side = ent.getChangeType() == ChangeType.ADD ? Side.NEW : Side.OLD;
    String path = ent.getPath(side);
    return getProject(path);
  }

  private IProject getProject(String path) {
    URI pathUri = repository.getWorkTree().toURI().resolve(URIUtil.toURI(path));
    IFile[] files = ResourcesPlugin.getWorkspace().getRoot()
        .findFilesForLocationURI(pathUri);
    Assert.isLegal(files.length == 1, NLS.bind(CoreText.CreatePatchOperation_couldNotFindProject, path, repository));
    return files[0].getProject();
  }

  /**
   * Retrieves the content of the requested patch
   *
   * @return the content of the patch
   */
  public String getPatchContent() {
    if (patchContent == null)
      throw new IllegalStateException(
          "#execute needs to be called before this method."); //$NON-NLS-1$
    return patchContent;
  }

  private void writeGitPatchHeader(StringBuilder sb) {
    String template = headerFormat.getTemplate();
    String[] segments = template.split("\\$\\{"); //$NON-NLS-1$
    Stack<String> evaluated = new Stack<String>();
    evaluated.add(segments[0]);

    for (int i = 1; i < segments.length; i++) {
      String segment = segments[i];
      String value = null;
      int brace = segment.indexOf('}');
      if (brace > 0) {
        String keyword = segment.substring(0, brace);
        keyword = keyword.toUpperCase().replaceAll(" ", "_"); //$NON-NLS-1$ //$NON-NLS-2$
        value = processKeyword(commit, DiffHeaderKeyword.valueOf(keyword));
      }

      String trailingCharacters = segment.substring(brace + 1);
      if (value != null) {
        evaluated.add(value);
        evaluated.add(trailingCharacters);
      } else if (!evaluated.isEmpty())
        evaluated.add(trailingCharacters);
    }
    StringBuffer buffer = new StringBuffer();
    for (String string : evaluated)
      buffer.append(string);

    sb.append(buffer);
  }

  private static String processKeyword(RevCommit commit, DiffHeaderKeyword keyword) {
    final SimpleDateFormat dtfmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.US); //$NON-NLS-1$
    switch (keyword) {
    case SHA1:
      return commit.getId().getName();
    case AUTHOR:
      return commit.getAuthorIdent().getName()
          + " <" + commit.getAuthorIdent().getEmailAddress() + ">"; //$NON-NLS-1$ //$NON-NLS-2$
    case AUTHOR_DATE:
      dtfmt.setTimeZone(commit.getAuthorIdent().getTimeZone());
      return dtfmt.format(commit.getAuthorIdent().getWhen());
    case DATE:
      return dtfmt.format(Long.valueOf(System.currentTimeMillis()));
    case TITLE_LINE:
      return commit.getShortMessage();
    case FULL_COMMIT_MESSAGE:
      return commit.getFullMessage().substring(
          commit.getShortMessage().length());
    default:
      return null;
    }
  }

  /**
   * Updates prefixes to workspace paths
   *
   * @param sb
   * @param diffFmt
   */
  public void updateWorkspacePatchPrefixes(StringBuilder sb, DiffFormatter diffFmt) {
    RawText rt;
    try {
      rt = new RawText(sb.toString().getBytes("UTF-8")); //$NON-NLS-1$
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }

    final String oldPrefix = diffFmt.getOldPrefix();
    final String newPrefix = diffFmt.getNewPrefix();

    StringBuilder newSb = new StringBuilder();
    final Pattern diffPattern = Pattern
        .compile("^diff --git (" + oldPrefix + "(.+)) (" + newPrefix + "(.+))$"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    final Pattern oldPattern = Pattern
        .compile("^--- (" + oldPrefix + "(.+))$"); //$NON-NLS-1$ //$NON-NLS-2$
    final Pattern newPattern = Pattern
        .compile("^\\+\\+\\+ (" + newPrefix + "(.+))$"); //$NON-NLS-1$ //$NON-NLS-2$

    int i = 0;
    while (i < rt.size()) {
      String line = rt.getString(i);

      Matcher diffMatcher = diffPattern.matcher(line);
      Matcher oldMatcher = oldPattern.matcher(line);
      Matcher newMatcher = newPattern.matcher(line);
      if (diffMatcher.find()) {
        String group = diffMatcher.group(2); // old path
        IProject project = getProject(group);
        IPath newPath = computeWorkspacePath(new Path(group), project);
        line = line.replaceAll(diffMatcher.group(1), newPath.toString());
        group = diffMatcher.group(4); // new path
        newPath = computeWorkspacePath(new Path(group), project);
        line = line.replaceAll(diffMatcher.group(3), newPath.toString());
      } else if (oldMatcher.find()) {
        String group = oldMatcher.group(2);
        IProject project = getProject(group);
        IPath newPath = computeWorkspacePath(new Path(group), project);
        line = line.replaceAll(oldMatcher.group(1), newPath.toString());
      } else if (newMatcher.find()) {
        String group = newMatcher.group(2);
        IProject project = getProject(group);
        IPath newPath = computeWorkspacePath(new Path(group), project);
        line = line.replaceAll(newMatcher.group(1), newPath.toString());
      }
      newSb.append(line);

      i++;
      if (i < rt.size() || !rt.isMissingNewlineAtEnd())
        newSb.append(rt.getLineDelimiter());
    }
    // reset sb to newSb
    sb.setLength(0);
    sb.append(newSb);
  }

  /**
   * Returns a workspace path
   *
   * @param path
   * @param project
   * @return path
   */
  public static IPath computeWorkspacePath(final IPath path, final IProject project) {
    RepositoryMapping rm = RepositoryMapping.getMapping(project);
    String repoRelativePath = rm.getRepoRelativePath(project);
    // the relative path cannot be determined, return unchanged
    if (repoRelativePath == null)
      return path;
    // repository and project at the same level
    if (repoRelativePath.equals("")) //$NON-NLS-1$
      return path;
    return path.removeFirstSegments(path.matchingFirstSegments(new Path(
        repoRelativePath)));
  }


  /**
   * Change the format of diff header
   *
   * @param format header format
   */
  public void setHeaderFormat(DiffHeaderFormat format) {
    this.headerFormat = format;
  }

  /**
   * Change the number of lines of context to display.
   *
   * @param contextLines line count
   */
  public void setContextLines(int contextLines) {
    this.contextLines = contextLines;
  }

  /**
   * Suggests a file name for the patch given the commit.
   *
   * @param commit
   * @return a file name for a patch
   */
  public static String suggestFileName(RevCommit commit) {
    String name = commit.getShortMessage();

    name = name.trim();
    StringBuilder filteredBuilder = new StringBuilder();
    char[] charArray = name.toCharArray();
    for (char c : charArray) {
      if(Character.isLetter(c) || Character.isDigit(c))
        filteredBuilder.append(c);
      if(Character.isWhitespace(c) || c == '/')
        filteredBuilder.append("-"); //$NON-NLS-1$
    }
    name = filteredBuilder.toString();
    if (name.length() > 52)
      name = name.substring(0, 52);
    while (name.endsWith(".")) //$NON-NLS-1$
      name = name.substring(0, name.length() - 1);
    name = name.concat(".patch"); //$NON-NLS-1$

    return name;
  }

  public ISchedulingRule getSchedulingRule() {
    return null;
  }

  /**
   * Set the filter to produce patch for specified paths only.
   *
   * @param pathFilter the filter
   */
  public void setPathFilter(TreeFilter pathFilter) {
    this.pathFilter = pathFilter;
  }
}
TOP

Related Classes of org.eclipse.egit.core.op.CreatePatchOperation

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.