| /******************************************************************************* |
| * Copyright (c) 2010, 2014 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.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.AnyObjectId; |
| 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.jgit.util.RawParseUtils; |
| 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; |
| } |
| |
| @Override |
| public void execute(IProgressMonitor monitor) throws CoreException { |
| try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
| final DiffFormatter diffFmt = createDiffFormatter( |
| outputStream, monitor)) { |
| |
| diffFmt.setContext(contextLines); |
| final StringBuilder sb = new StringBuilder(); |
| if (headerFormat != null && headerFormat != DiffHeaderFormat.NONE) { |
| writeGitPatchHeader(sb); |
| } |
| diffFmt.setRepository(repository); |
| diffFmt.setPathFilter(pathFilter); |
| |
| if (commit != null) { |
| List<DiffEntry> diffs = diffFmt.scan(getParentId(), 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)); |
| } |
| diffFmt.flush(); |
| |
| appendOutputStream(sb, outputStream); |
| |
| if (DiffHeaderFormat.WORKSPACE == headerFormat) { |
| updateWorkspacePatchPrefixes(sb, diffFmt); |
| } |
| patchContent = sb.toString(); |
| } catch (IOException e) { |
| Activator.logError( |
| CoreText.CreatePatchOperation_patchFileCouldNotBeWritten, |
| e); |
| } |
| } |
| |
| private DiffFormatter createDiffFormatter( |
| final ByteArrayOutputStream outputStream, |
| IProgressMonitor monitor) { |
| DiffFormatter diffFmt = new DiffFormatter(outputStream) { |
| |
| 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 != null && !p.equals(project)) { |
| project = p; |
| getOutputStream().write( |
| encodeASCII("#P " + project.getName() + "\n")); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| } |
| super.format(ent); |
| } |
| }; |
| diffFmt.setProgressMonitor(new EclipseGitProgressTransformer(monitor)); |
| return diffFmt; |
| } |
| |
| private AnyObjectId getParentId() { |
| 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; |
| } |
| return parentId; |
| } |
| |
| private void appendOutputStream(final StringBuilder sb, |
| final ByteArrayOutputStream outputStream) { |
| try { |
| String encoding = currentEncoding != null ? currentEncoding |
| : RawParseUtils.UTF8_CHARSET.name(); |
| sb.append(outputStream.toString(encoding)); |
| } catch (UnsupportedEncodingException e) { |
| sb.append(outputStream.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(Locale.ROOT).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); |
| } |
| for (String string : evaluated) { |
| sb.append(string); |
| } |
| } |
| |
| 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); |
| if (rm == null) { |
| return path; |
| } |
| 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; |
| } |
| |
| @Override |
| 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; |
| } |
| } |