blob: a5b35c1efdba3f1cf07bdd0b5c1f04e7be326b1e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Matt McCutchen <hashproduct+eclipse@gmail.com> - Bug 128429 [Change Sets] Change Sets with / in name do not get persited
*******************************************************************************/
package org.eclipse.team.internal.core.subscribers;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.mapping.ResourceTraversal;
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.IStatus;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.diff.IDiff;
import org.eclipse.team.core.diff.IDiffChangeEvent;
import org.eclipse.team.core.diff.IDiffChangeListener;
import org.eclipse.team.core.diff.IDiffTree;
import org.eclipse.team.core.diff.IThreeWayDiff;
import org.eclipse.team.core.mapping.IChangeGroupingRequestor;
import org.eclipse.team.core.mapping.IResourceDiffTree;
import org.eclipse.team.internal.core.Messages;
import org.eclipse.team.internal.core.TeamPlugin;
import org.eclipse.team.internal.core.mapping.CompoundResourceTraversal;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
/**
* A change set manager that contains sets that represent collections of
* related local changes.
*/
public abstract class ActiveChangeSetManager extends ChangeSetManager implements IDiffChangeListener, IChangeGroupingRequestor {
private static final String CTX_DEFAULT_SET = "defaultSet"; //$NON-NLS-1$
private ActiveChangeSet defaultSet;
/**
* Return the Change Set whose sync info set is the
* one given.
* @param tree a diff tree
* @return the change set for the given diff tree
*/
protected ChangeSet getChangeSet(IResourceDiffTree tree) {
ChangeSet[] sets = getSets();
for (int i = 0; i < sets.length; i++) {
ChangeSet changeSet = sets[i];
if (((DiffChangeSet)changeSet).getDiffTree() == tree) {
return changeSet;
}
}
return null;
}
@Override
public void add(ChangeSet set) {
Assert.isTrue(set instanceof ActiveChangeSet);
super.add(set);
}
@Override
protected void handleSetAdded(ChangeSet set) {
Assert.isTrue(set instanceof ActiveChangeSet);
((DiffChangeSet)set).getDiffTree().addDiffChangeListener(getDiffTreeListener());
super.handleSetAdded(set);
handleAddedResources(set, ((ActiveChangeSet)set).internalGetDiffTree().getDiffs());
}
@Override
protected void handleSetRemoved(ChangeSet set) {
((DiffChangeSet)set).getDiffTree().removeDiffChangeListener(getDiffTreeListener());
super.handleSetRemoved(set);
}
/**
* Return the listener that is registered with the diff trees associated with
* the sets for this manager.
* @return the listener that is registered with the diff trees associated with
* the sets for this manager
*/
protected IDiffChangeListener getDiffTreeListener() {
return this;
}
@Override
public void diffsChanged(IDiffChangeEvent event, IProgressMonitor monitor) {
IResourceDiffTree tree = (IResourceDiffTree)event.getTree();
handleSyncSetChange(tree, event.getAdditions(), getAllResources(event));
}
@Override
public void propertyChanged(IDiffTree tree, int property, IPath[] paths) {
// ignore
}
@Override
public boolean isModified(IFile file) throws CoreException {
IDiff diff = getDiff(file);
if (diff != null)
return isModified(diff);
return false;
}
/**
* Return whether the given diff represents a local change.
* @param diff the diff
* @return whether the given diff represents a local change
*/
public boolean isModified(IDiff diff) {
if (diff != null) {
if (diff instanceof IThreeWayDiff) {
IThreeWayDiff twd = (IThreeWayDiff) diff;
int dir = twd.getDirection();
return dir == IThreeWayDiff.OUTGOING || dir == IThreeWayDiff.CONFLICTING;
} else {
return diff.getKind() != IDiff.NO_CHANGE;
}
}
return false;
}
/**
* Return the set with the given name.
* @param name the name of the set
* @return the set with the given name
*/
public ActiveChangeSet getSet(String name) {
ChangeSet[] sets = getSets();
for (int i = 0; i < sets.length; i++) {
ChangeSet set = sets[i];
if (set.getName().equals(name) && set instanceof ActiveChangeSet) {
return (ActiveChangeSet)set;
}
}
return null;
}
/**
* Create a change set containing the given files if
* they have been modified locally.
* @param title the title of the commit set
* @param files the files contained in the set
* @return the created set
* @throws CoreException
*/
public ActiveChangeSet createSet(String title, IFile[] files) throws CoreException {
List<IDiff> infos = new ArrayList<>();
for (int i = 0; i < files.length; i++) {
IFile file = files[i];
IDiff diff = getDiff(file);
if (diff != null) {
infos.add(diff);
}
}
return createSet(title, infos.toArray(new IDiff[infos.size()]));
}
/**
* Create a commit set with the given title and files. The created
* set is not added to the control of the commit set manager
* so no events are fired. The set can be added using the
* <code>add</code> method.
* @param title the title of the commit set
* @param diffs the files contained in the set
* @return the created set
*/
public ActiveChangeSet createSet(String title, IDiff[] diffs) {
ActiveChangeSet commitSet = doCreateSet(title);
if (diffs != null && diffs.length > 0) {
commitSet.add(diffs);
}
return commitSet;
}
/**
* Create a change set with the given name.
* @param name the name of the change set
* @return the created change set
*/
protected ActiveChangeSet doCreateSet(String name) {
return new ActiveChangeSet(this, name);
}
public abstract IDiff getDiff(IResource resource) throws CoreException;
/**
* Return whether the manager allows a resource to
* be in multiple sets. By default, a resource
* may only be in one set.
* @return whether the manager allows a resource to
* be in multiple sets.
*/
protected boolean isSingleSetPerResource() {
return true;
}
private IPath[] getAllResources(IDiffChangeEvent event) {
Set<IPath> allResources = new HashSet<>();
IDiff[] addedResources = event.getAdditions();
for (int i = 0; i < addedResources.length; i++) {
IDiff diff = addedResources[i];
allResources.add(diff.getPath());
}
IDiff[] changedResources = event.getChanges();
for (int i = 0; i < changedResources.length; i++) {
IDiff diff = changedResources[i];
allResources.add(diff.getPath());
}
IPath[] removals = event.getRemovals();
for (int i = 0; i < removals.length; i++) {
IPath path = removals[i];
allResources.add(path);
}
return allResources.toArray(new IPath[allResources.size()]);
}
/**
* React to the given diffs being added to the given set.
* @param set the set
* @param diffs the diffs
*/
protected void handleAddedResources(ChangeSet set, IDiff[] diffs) {
if (isSingleSetPerResource() && ((ActiveChangeSet)set).isUserCreated()) {
IResource[] resources = new IResource[diffs.length];
for (int i = 0; i < resources.length; i++) {
resources[i] = ((DiffChangeSet)set).getDiffTree().getResource(diffs[i]);
}
// Remove the added files from any other set that contains them
ChangeSet[] sets = getSets();
for (int i = 0; i < sets.length; i++) {
ChangeSet otherSet = sets[i];
if (otherSet != set && ((ActiveChangeSet)otherSet).isUserCreated()) {
otherSet.remove(resources);
}
}
}
}
private void handleSyncSetChange(IResourceDiffTree tree, IDiff[] addedDiffs, IPath[] allAffectedResources) {
ChangeSet changeSet = getChangeSet(tree);
if (tree.isEmpty() && changeSet != null) {
remove(changeSet);
}
fireResourcesChangedEvent(changeSet, allAffectedResources);
handleAddedResources(changeSet, addedDiffs);
}
/**
* Make the given set the default set into which all new modifications that
* are not already in another set go.
*
* @param set
* the set which is to become the default set or
* <code>null</code> to unset the default set
*/
public void makeDefault(ActiveChangeSet set) {
// The default set must be an active set
if (set != null && !contains(set)) {
add(set);
}
ActiveChangeSet oldSet = defaultSet;
defaultSet = set;
fireDefaultChangedEvent(oldSet, defaultSet);
}
/**
* Return whether the given set is the default set into which all
* new modifications will be placed.
* @param set the set to test
* @return whether the set is the default set
*/
public boolean isDefault(ActiveChangeSet set) {
return set == defaultSet;
}
/**
* Return the set which is currently the default or
* <code>null</code> if there is no default set.
* @return the default change set
*/
public ActiveChangeSet getDefaultSet() {
return defaultSet;
}
/**
* If the given traversals contain any resources in the active change sets, ensure
* that the traversals cover all the resources in the overlapping change set.
* @param traversals the traversals
* @return the traversals adjusted to contain all the resources of intersecting change sets
*/
public ResourceTraversal[] adjustInputTraversals(ResourceTraversal[] traversals) {
CompoundResourceTraversal traversal = new CompoundResourceTraversal();
traversal.addTraversals(traversals);
ChangeSet[] sets = getSets();
for (int i = 0; i < sets.length; i++) {
ChangeSet set = sets[i];
handleIntersect(traversal, set);
}
return traversal.asTraversals();
}
private void handleIntersect(CompoundResourceTraversal traversal, ChangeSet set) {
IResource[] resources = set.getResources();
for (int i = 0; i < resources.length; i++) {
IResource resource = resources[i];
if (traversal.isCovered(resource, IResource.DEPTH_ZERO)) {
traversal.addResources(resources, IResource.DEPTH_ZERO);
return;
}
}
}
/**
* Save the state of this manager including all its contained sets
* into the given preferences node.
* @param prefs a preferences node
*/
protected void save(Preferences prefs) {
// No need to save the sets if the manager has never been initialized
if (!isInitialized())
return;
// Clear the persisted state before saving the new state
try {
String[] oldSetNames = prefs.childrenNames();
for (int i = 0; i < oldSetNames.length; i++) {
String string = oldSetNames[i];
prefs.node(string).removeNode();
}
} catch (BackingStoreException e) {
TeamPlugin.log(IStatus.ERROR, NLS.bind(Messages.SubscriberChangeSetCollector_5, new String[] { getName() }), e);
}
ChangeSet[] sets = getSets();
for (int i = 0; i < sets.length; i++) {
ChangeSet set = sets[i];
if (set instanceof ActiveChangeSet && !set.isEmpty()) {
// Since the change set title is stored explicitly, the name of
// the child preference node doesn't matter as long as it
// doesn't contain / and no two change sets get the same name.
String childPrefName = escapePrefName(((ActiveChangeSet)set).getTitle());
Preferences child = prefs.node(childPrefName);
((ActiveChangeSet)set).save(child);
}
}
if (getDefaultSet() != null) {
prefs.put(CTX_DEFAULT_SET, getDefaultSet().getTitle());
} else {
// unset default changeset
prefs.remove(CTX_DEFAULT_SET);
}
try {
prefs.flush();
} catch (BackingStoreException e) {
TeamPlugin.log(IStatus.ERROR, NLS.bind(Messages.SubscriberChangeSetCollector_3, new String[] { getName() }), e);
}
}
/**
* Escape the given string for safe use as a preference node name by
* translating / to \s (so it's a single path component) and \ to \\ (to
* preserve uniqueness).
*
* @param string
* Input string
* @return Escaped output string
*/
private static String escapePrefName(String string) {
StringBuilder out = new StringBuilder();
for (int i = 0; i < string.length(); i++) {
char c = string.charAt(i);
switch (c) {
case '/':
out.append("\\s"); //$NON-NLS-1$
break;
case '\\':
out.append("\\\\"); //$NON-NLS-1$
break;
default:
out.append(c);
}
}
return out.toString();
}
/**
* Load the manager's state from the given preferences node.
*
* @param prefs
* a preferences node
*/
protected void load(Preferences prefs) {
String defaultSetTitle = prefs.get(CTX_DEFAULT_SET, null);
try {
String[] childNames = prefs.childrenNames();
for (int i = 0; i < childNames.length; i++) {
String string = childNames[i];
Preferences childPrefs = prefs.node(string);
ActiveChangeSet set = createSet(childPrefs);
if (!set.isEmpty()) {
if (getDefaultSet() == null && defaultSetTitle != null && set.getTitle().equals(defaultSetTitle)) {
makeDefault(set);
}
add(set);
}
}
} catch (BackingStoreException e) {
TeamPlugin.log(IStatus.ERROR, NLS.bind(Messages.SubscriberChangeSetCollector_4, new String[] { getName() }), e);
}
}
/**
* Return the name of this change set manager.
* @return the name of this change set manager
*/
protected abstract String getName();
/**
* Create a change set from the given preferences that were
* previously saved.
* @param childPrefs the previously saved preferences
* @return the created change set
*/
protected ActiveChangeSet createSet(Preferences childPrefs) {
// Don't specify a title when creating the change set; instead, let the
// change set read its title from the preferences.
ActiveChangeSet changeSet = doCreateSet(null);
changeSet.init(childPrefs);
return changeSet;
}
@Override
public void ensureChangesGrouped(IProject project, IFile[] files,
String name) throws CoreException {
ActiveChangeSet set = getSet(name);
if (set == null) {
set = createSet(name, files);
set.setUserCreated(false);
add(set);
} else {
set.setUserCreated(false);
set.add(files);
}
}
}