/*******************************************************************************
* Copyright (c) 2006, 2016 Red Hat Inc. 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:
* Kyu Lee <klee@redhat.com> - initial API and implementation
* Jeff Johnston <jjohnstn@redhat.com> - remove CVS bindings, support removal
* Kiu Kwan Leung <kleung@redhat.com> - fixed compatibility issue with Egit
*******************************************************************************/
package org.eclipse.linuxtools.internal.changelog.core.actions;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Vector;
import org.eclipse.compare.rangedifferencer.RangeDifference;
import org.eclipse.compare.rangedifferencer.RangeDifferencer;
import org.eclipse.compare.structuremergeviewer.Differencer;
import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.linuxtools.changelog.core.IFormatterChangeLogContrib;
import org.eclipse.linuxtools.changelog.core.IParserChangeLogContrib;
import org.eclipse.linuxtools.internal.changelog.core.ChangeLogWriter;
import org.eclipse.linuxtools.internal.changelog.core.ChangelogPlugin;
import org.eclipse.linuxtools.internal.changelog.core.LineComparator;
import org.eclipse.linuxtools.internal.changelog.core.Messages;
import org.eclipse.linuxtools.internal.changelog.core.editors.ChangeLogEditor;
import org.eclipse.team.core.RepositoryProvider;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.core.diff.IDiff;
import org.eclipse.team.core.diff.IThreeWayDiff;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.team.core.mapping.IResourceDiff;
import org.eclipse.team.core.subscribers.Subscriber;
import org.eclipse.team.core.synchronize.SyncInfo;
import org.eclipse.team.core.synchronize.SyncInfoSet;
import org.eclipse.team.ui.synchronize.ISynchronizeModelElement;
import org.eclipse.ui.IEditorDescriptor;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.editors.text.FileDocumentProvider;
import org.eclipse.ui.editors.text.StorageDocumentProvider;
import org.eclipse.ui.part.FileEditorInput;
/**
* Action handler for prepare changelog.
*
* @author klee
*
*/
public class PrepareChangeLogAction extends ChangeLogAction {
protected boolean changeLogModified = false;
protected boolean newEntryWritten = false;
protected boolean createChangeLog = true;
private static class MyDocumentProvider extends FileDocumentProvider {
@Override
public IDocument createDocument(Object element) throws CoreException {
return super.createDocument(element);
}
}
private static class MyStorageDocumentProvider extends StorageDocumentProvider {
@Override
public IDocument createDocument(Object element) throws CoreException {
return super.createDocument(element);
}
}
private IStructuredSelection selected;
public PrepareChangeLogAction() {
super();
}
protected void setSelection(IStructuredSelection selection) {
this.selected = selection;
}
private String parseCurrentFunctionAtOffset(String editorName,
IEditorInput input, int offset) {
IParserChangeLogContrib parser = extensionManager
.getParserContributor(editorName);
// return empty string if function parser for editorName is not present
if (parser==null)
return "";
try {
return parser.parseCurrentFunction(input, offset);
} catch (CoreException e) {
ChangelogPlugin.getDefault().getLog().log(
new Status(IStatus.ERROR, ChangelogPlugin.PLUGIN_ID, IStatus.ERROR, e
.getMessage(), e));
}
return "";
}
/**
* @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction)
*/
protected void doRun() {
IRunnableWithProgress code = monitor -> {
monitor.beginTask(Messages.getString("ChangeLog.PrepareChangeLog"), 1000); // $NON-NLS-1$
prepareChangeLog(monitor);
monitor.done();
};
ProgressMonitorDialog pd = new ProgressMonitorDialog(PlatformUI.getWorkbench()
.getActiveWorkbenchWindow().getShell());
try {
pd.run(false /* fork */, false /* cancelable */, code);
} catch (InvocationTargetException e) {
ChangelogPlugin.getDefault().getLog().log(
new Status(IStatus.ERROR, ChangelogPlugin.PLUGIN_ID, IStatus.ERROR, e
.getMessage(), e));
return;
} catch (InterruptedException e) {
ChangelogPlugin.getDefault().getLog().log(
new Status(IStatus.ERROR, ChangelogPlugin.PLUGIN_ID, IStatus.ERROR, e
.getMessage(), e));
}
}
private void extractSynchronizeModelInfo (ISynchronizeModelElement d, IPath path, Vector<PatchFile> newList, Vector<PatchFile> removeList, Vector<PatchFile> changeList) {
// Recursively traverse the tree for children and sort leaf elements into their respective change kind sets.
// Don't add entries for ChangeLog files though.
if (d.hasChildren()) {
IPath newPath = path.append(d.getName());
for (IDiffElement element: d.getChildren()) {
if (element instanceof ISynchronizeModelElement)
extractSynchronizeModelInfo((ISynchronizeModelElement)element, newPath, newList, removeList, changeList);
else {
if (!(d.getName().equals("ChangeLog"))) { //$NON-NLS-1$
PatchFile p = new PatchFile(d.getResource());
int kind = d.getKind() & Differencer.CHANGE_TYPE_MASK;
if (kind == Differencer.CHANGE) {
changeList.add(p);
} else if (kind == Differencer.ADDITION) {
p.setNewfile(true);
newList.add(p);
} else if (kind == Differencer.DELETION) {
p.setRemovedFile(true);
removeList.add(p);
}
} else {
this.changeLogModified = true;
}
}
}
} else {
if (!(d.getName().equals("ChangeLog"))) { //$NON-NLS-1$
PatchFile p = new PatchFile(d.getResource());
int kind = d.getKind() & Differencer.CHANGE_TYPE_MASK;
if (kind == Differencer.CHANGE) {
changeList.add(p);
} else if (kind == Differencer.ADDITION) {
p.setNewfile(true);
newList.add(p);
} else if (kind == Differencer.DELETION) {
p.setRemovedFile(true);
removeList.add(p);
}
} else {
this.changeLogModified = true;
}
}
}
private void getChangedLines(Subscriber s, PatchFile p, IProgressMonitor monitor) {
try {
// For an outgoing changed resource, find out which lines
// differ from the local file and its previous local version
// (i.e. we don't want to force a diff with the repository).
IDiff d = s.getDiff(p.getResource());
if (d instanceof IThreeWayDiff
&& ((IThreeWayDiff)d).getDirection() == IThreeWayDiff.OUTGOING) {
IThreeWayDiff diff = (IThreeWayDiff)d;
monitor.beginTask(null, 100);
IResourceDiff localDiff = (IResourceDiff)diff.getLocalChange();
IResource resource = localDiff.getResource();
if (resource instanceof IFile) {
IFile file = (IFile)resource;
monitor.subTask(Messages.getString("ChangeLog.MergingDiffs")); // $NON-NLS-1$
String osEncoding = file.getCharset();
IFileRevision ancestorState = localDiff.getBeforeState();
IStorage ancestorStorage;
if (ancestorState != null) {
ancestorStorage = ancestorState.getStorage(monitor);
p.setStorage(ancestorStorage);
}
else {
return;
}
try {
// We compare using a standard differencer to get ranges
// of changes. We modify them to be document-based (i.e.
// first line is line 1) and store them for later parsing.
LineComparator left = new LineComparator(ancestorStorage.getContents(), osEncoding);
LineComparator right = new LineComparator(file.getContents(), osEncoding);
for (RangeDifference tmp: RangeDifferencer.findDifferences(left, right)) {
if (tmp.kind() == RangeDifference.CHANGE) {
// Right side of diff are all changes found in local file.
int rightLength = tmp.rightLength() > 0 ? tmp.rightLength() : tmp.rightLength() + 1;
// We also want to store left side of the diff which are changes to the ancestor as it may contain
// functions/methods that have been removed.
int leftLength = tmp.leftLength() > 0 ? tmp.leftLength() : tmp.leftLength() + 1;
// Only store left side changes if the storage exists and we add one to the start line number
if (p.getStorage() != null)
p.addLineRange(tmp.leftStart(), tmp.leftStart() + leftLength, false);
p.addLineRange(tmp.rightStart(), tmp.rightStart() + rightLength, true);
}
}
} catch (UnsupportedEncodingException e) {
// do nothing for now
}
}
monitor.done();
}
} catch (CoreException e) {
// Do nothing if error occurs
}
}
private void prepareChangeLog(IProgressMonitor monitor) {
Object element = selected.getFirstElement();
IResource resource = null;
Vector<PatchFile> newList = new Vector<>();
Vector<PatchFile> removeList = new Vector<>();
Vector<PatchFile> changeList = new Vector<>();
int totalChanges = 0;
if (element instanceof IResource) {
resource = (IResource)element;
} else if (element instanceof ISynchronizeModelElement) {
ISynchronizeModelElement sme = (ISynchronizeModelElement)element;
resource = sme.getResource();
} else if (element instanceof IAdaptable) {
resource = ((IAdaptable)element).getAdapter(IResource.class);
}
if (resource == null)
return;
IProject project = resource.getProject();
// Get the repository provider so we can support multiple types of
// code repositories without knowing exactly which (e.g. CVS, SVN, etc..).
RepositoryProvider r = RepositoryProvider.getProvider(project);
if (r == null)
return;
SyncInfoSet set = new SyncInfoSet();
Subscriber s = r.getSubscriber();
if (s == null)
return;
if (element instanceof ISynchronizeModelElement) {
// We can extract the ChangeLog list from the synchronize view which
// allows us to skip items removed from the view
ISynchronizeModelElement d = (ISynchronizeModelElement)element;
while (d.getParent() != null)
d = (ISynchronizeModelElement)d.getParent();
extractSynchronizeModelInfo(d, new Path(""), newList, removeList, changeList);
totalChanges = newList.size() + removeList.size() + changeList.size();
}
else {
// We can then get a list of all out-of-sync resources.
IResource[] resources = new IResource[] { project };
try {
s.refresh(resources, IResource.DEPTH_INFINITE, monitor);
} catch (TeamException e) {
// Ignore, continue anyways
}
s.collectOutOfSync(resources, IResource.DEPTH_INFINITE, set, monitor);
SyncInfo[] infos = set.getSyncInfos();
totalChanges = infos.length;
// Iterate through the list of changed resources and categorize them into
// New, Removed, and Changed lists.
for (SyncInfo info : infos) {
int kind = SyncInfo.getChange(info.getKind());
PatchFile p = new PatchFile(info.getLocal());
// Check the type of entry and sort into lists. Do not add an entry
// for ChangeLog files.
if (!(p.getPath().lastSegment().equals("ChangeLog"))) { // $NON-NLS-1$
switch (kind) {
case SyncInfo.ADDITION:
p.setNewfile(true);
newList.add(p);
break;
case SyncInfo.DELETION:
p.setRemovedFile(true);
removeList.add(p);
break;
case SyncInfo.CHANGE:
if (info.getLocal().getType() == IResource.FILE) {
changeList.add(p);
}
break;
}
} else {
this.changeLogModified = true;
}
}
}
if (totalChanges == 0)
return; // nothing to parse
PatchFile[] patchFileInfoList = new PatchFile[totalChanges];
// Group like changes together and sort them by path name.
// We want removed files, then new files, then changed files.
// To get this, we put them in the array in reverse order.
int index = 0;
if (changeList.size() > 0) {
// Get the repository provider so we can support multiple types of
// code repositories without knowing exactly which (e.g. CVS, SVN, etc..).
Collections.sort(changeList, new PatchFileComparator());
int size = changeList.size();
for (int i = 0; i < size; ++i) {
PatchFile p = changeList.get(i);
getChangedLines(s, p, monitor);
patchFileInfoList[index+(size-i-1)] = p;
}
index += size;
}
if (newList.size() > 0) {
Collections.sort(newList, new PatchFileComparator());
int size = newList.size();
for (int i = 0; i < size; ++i)
patchFileInfoList[index+(size-i-1)] = newList.get(i);
index += size;
}
if (removeList.size() > 0) {
Collections.sort(removeList, new PatchFileComparator());
int size = removeList.size();
for (int i = 0; i < size; ++i)
patchFileInfoList[index+(size-i-1)] = removeList.get(i);
}
// now, find out modified functions/classes.
// try to use the the extension point. so it can be extended easily
// for all files in patch file info list, get function guesses of each
// file.
monitor.subTask(Messages.getString("ChangeLog.WritingMessage")); // $NON-NLS-1$
int unitwork = 250 / patchFileInfoList.length;
for (PatchFile pf: patchFileInfoList) {
// for each file
if (pf != null) { // any ChangeLog changes will have null entries for them
String[] funcGuessList = guessFunctionNames(pf);
outputMultipleEntryChangeLog(pf, funcGuessList);
}
monitor.worked(unitwork);
}
}
private void outputMultipleEntryChangeLog(PatchFile pf, String[] functionGuess) {
String defaultContent = null;
if (pf.isNewfile())
defaultContent = Messages.getString("ChangeLog.NewFile"); // $NON-NLS-1$
else if (pf.isRemovedFile())
defaultContent = Messages.getString("ChangeLog.RemovedFile"); // $NON-NLS-1$
IPath entryPath = pf.getPath();
String entryFileName = entryPath.toOSString();
ChangeLogWriter clw = new ChangeLogWriter();
// load settings from extensions + user pref.
loadPreferences();
// get file path from target file
clw.setEntryFilePath(entryPath.toOSString());
if (defaultContent != null)
clw.setDefaultContent(defaultContent);
// err check. do nothing if no file is being open/edited
if (clw.getEntryFilePath() == "") {
return;
}
// Check if formatter is internal or inline..if inline, use the
// current active editor part, otherwise, we must find the external
// ChangeLog file.
IEditorPart changelog = null;
// Before accessing the getFormatterConfigElement, the getFormatContibutor
// method must be called to initialize.
extensionManager.getFormatterContributor(clw.getEntryFilePath(),
pref_Formatter);
IConfigurationElement formatterConfigElement = extensionManager
.getFormatterConfigElement();
if (formatterConfigElement.getAttribute("inFile").equalsIgnoreCase( //$NON-NLS-1$
"true")) { //$NON-NLS-1$
try {
changelog = openEditor((IFile)pf.getResource());
clw.setFormatter(extensionManager.getFormatterContributor(
clw.getEntryFilePath(), pref_Formatter));
} catch (Exception e) {
// do nothing changelog will be null
}
} else {
// external changelog
// get formatter
clw.setFormatter(extensionManager.getFormatterContributor(
entryFileName, pref_Formatter));
if (pf.isRemovedFile())
changelog = getChangelogForRemovePath(entryPath);
else
changelog = getChangelog(entryFileName);
// If there isn't a ChangeLog, we will ask for one here.
// We originally avoided this to prevent a problem for rpm
// projects whereby the changelog is inlined in a single file
// and not presented externally. This has been changed in
// response to bug #347703. If the user cancels the ask
// dialog, then the prepare operation doesn't try to create
// one.
if (createChangeLog && changelog == null)
changelog = askChangeLogLocation(entryPath.toOSString());
if (changelog == null) {
createChangeLog = false;
return;
}
}
if ((changelog instanceof ChangeLogEditor) && (!this.newEntryWritten)) {
ChangeLogEditor editor = (ChangeLogEditor) changelog;
// if the editor is dirty (changes added to the editor without
// saving), treat it as a change log modification
if (editor.isDirty())
this.changeLogModified = true;
editor.setForceNewLogEntry(!this.changeLogModified);
this.newEntryWritten = true;
}
// select changelog
clw.setChangelog(changelog);
// write to changelog
IFormatterChangeLogContrib formatter = clw.getFormatter();
clw.setDateLine(formatter.formatDateLine(pref_AuthorName,
pref_AuthorEmail));
clw.setChangelogLocation(getDocumentLocation(clw.getChangelog(), true));
// print multiple changelog entries with different
// function guess names. We default to an empty guessed name
// if we have zero function guess names.
int numFuncs = 0;
clw.setGuessedFName(""); // $NON-NLS-1$
if (functionGuess.length > 0) {
for (String guess : functionGuess) {
if (!guess.trim().equals("")) { // $NON-NLS-1$
++numFuncs;
clw.setGuessedFName(guess);
clw.writeChangeLog();
}
}
}
// Default an empty entry if we did not have any none-empty
// function guesses.
if (numFuncs == 0) {
clw.writeChangeLog();
}
}
/**
* Guesses the function effected/modified by the patch from local file(newer
* file).
*
* @param patchFileInfo
* patch file
* @return array of unique function names
*/
private String[] guessFunctionNames(PatchFile patchFileInfo) {
// if this file is new file or removed file, do not guess function files
// TODO: create an option to include function names on
// new files or not
if (patchFileInfo.isNewfile() || patchFileInfo.isRemovedFile()) {
return new String[]{""};
}
String[] fnames = new String[0];
String editorName = ""; // $NON-NLS-1$
try {
IEditorDescriptor ed = org.eclipse.ui.ide.IDE
.getEditorDescriptor(patchFileInfo.getPath().toOSString(), true, false);
editorName = ed.getId().substring(ed.getId().lastIndexOf(".") + 1); // $NON-NLS-1$
} catch (PartInitException e1) {
ChangelogPlugin.getDefault().getLog().log(
new Status(IStatus.ERROR, ChangelogPlugin.PLUGIN_ID, IStatus.ERROR,
e1.getMessage(), e1));
return new String[0];
}
// check if the file type is supported
// get editor input for target file
IFileEditorInput fei = new FileEditorInput((IFile)patchFileInfo.getResource());
SourceEditorInput sei = new SourceEditorInput(patchFileInfo.getStorage());
MyDocumentProvider mdp = new MyDocumentProvider();
MyStorageDocumentProvider msdp = new MyStorageDocumentProvider();
try {
// get document for target file (one for local file, one for repository storage)
IDocument doc = mdp.createDocument(fei);
IDocument olddoc = msdp.createDocument(sei);
HashMap<String, String> functionNamesMap = new HashMap<>();
ArrayList<String> nameList = new ArrayList<>();
// for all the ranges
for (PatchRangeElement tpre: patchFileInfo.getRanges()) {
for (int j = tpre.fromLine; j <= tpre.toLine; j++) {
String functionGuess = "";
// add func that determines type of file.
// right now it assumes it's java file.
if (tpre.isLocalChange()) {
if ((j < 0) || (j > doc.getNumberOfLines() - 1))
continue; // ignore out of bound lines
functionGuess = parseCurrentFunctionAtOffset(
editorName, fei, doc.getLineOffset(j));
} else {
if ((j < 0) || (j > olddoc.getNumberOfLines() - 1))
continue; // ignore out of bound lines
functionGuess = parseCurrentFunctionAtOffset(
editorName, sei, olddoc.getLineOffset(j));
}
// putting it in hashmap will eliminate duplicate
// guesses. We use a list to keep track of ordering which
// is helpful when trying to document a large set of changes.
if (functionNamesMap.get(functionGuess) == null)
nameList.add(functionGuess);
functionNamesMap.put(functionGuess, functionGuess);
}
}
// dump all unique func. guesses in the order found
fnames = new String[nameList.size()];
fnames = nameList.toArray(fnames);
} catch (CoreException|BadLocationException e) {
ChangelogPlugin.getDefault().getLog().log(
new Status(IStatus.ERROR, ChangelogPlugin.PLUGIN_ID, IStatus.ERROR,
e.getMessage(), e));
}
return fnames;
}
}